| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| # Configuration for consumer group cooperative partition rebalancing. |
| [consumer_group] |
| # Maximum time a partition can remain in pending revocation before being force-transferred to the target member. |
| rebalancing_timeout = "30s" |
| |
| [data_maintenance.messages] |
| # Enables or disables the segment cleaner. It deletes the oldest sealed segments of |
| # topics with a finite message_expiry or max_topic_size, per partition, best-effort. |
| cleaner_enabled = true |
| |
| # Interval for running the message cleaner. |
| interval = "1 m" |
| |
| # This node's own client-facing identity, used when 'cluster.enabled' is false. |
| [node] |
| # A literal IP or a DNS hostname, without a port. Named to match its roster |
| # counterpart 'cluster.nodes.advertised_address', which answers the same |
| # question per node. The unspecified address ("0.0.0.0", "::") is rejected: it |
| # is the bind address's answer to a question clients are not asking. Commented |
| # out here because the shipped 'tcp.address' binds a concrete address and needs |
| # no declaration. |
| #advertised_address = "broker-1.example.com" |
| |
| # HTTP server configuration |
| [http] |
| # Determines if the HTTP server is active. |
| # `true` enables the server, allowing it to handle HTTP requests. |
| # `false` disables the server, preventing it from handling HTTP requests. |
| # In cluster mode, followers forward control-plane requests (streams, topics, |
| # users, ...) to the current primary when a cluster-wide JWT key exists (see |
| # http.jwt / cluster.auth below). |
| # TODO: forwarding does not cover the partition-plane APIs yet - message |
| # produce and consumer-offset writes are never forwarded and must reach the |
| # partition's primary node directly (message polls read locally on any node). |
| enabled = true |
| |
| # Specifies the network address and port for the HTTP server. |
| # The format is "HOST:PORT". For example, "127.0.0.1:3000" listens on localhost only on port 3000. |
| # In cluster mode the HOST still picks the bind interface, while the port |
| # comes from this node's cluster.nodes ports.http entry. |
| address = "127.0.0.1:3000" |
| |
| # Maximum size of the request body in bytes. For security reasons, the default limit is 2 MB. |
| # HTTP produce requests are not framed by the message bus, so this body cap is |
| # what bounds the widest batch record the HTTP path can persist. Boot refuses a |
| # value above the frozen 256 MiB recovery ceiling (see |
| # message_bus.max_message_size), and a body above message_bus.max_message_size |
| # can still admit a batch that no peer accepts a replication frame for -- keep |
| # this at or below that cap unless replica_count = 1. |
| max_request_size = "2 MB" |
| |
| # Enables the embedded Web UI dashboard at '/ui'. |
| # When set to `true` and the server is compiled with the 'iggy-web' feature, |
| # the Svelte dashboard will be served at the '/ui' endpoint, providing a |
| # browser-based interface for managing streams, topics, and viewing messages. |
| # If the server is compiled without 'iggy-web' feature and this is set to `true`, |
| # a warning will be logged at startup but the server will continue to run. |
| # `true` enables the embedded Web UI (requires server built with 'iggy-web' feature). |
| # `false` disables the embedded Web UI (default). |
| web_ui = false |
| |
| # Configuration for Cross-Origin Resource Sharing (CORS). |
| [http.cors] |
| # Controls whether CORS is enabled for the HTTP server. |
| # `true` allows handling cross-origin requests with specified rules. |
| # `false` blocks cross-origin requests, enhancing security. |
| enabled = true |
| |
| # Specifies which HTTP methods are allowed when CORS is enabled. |
| # For example, ["GET", "POST"] would allow only GET and POST requests. |
| allowed_methods = ["GET", "POST", "PUT", "DELETE"] |
| |
| # Defines which origins are permitted to make cross-origin requests. |
| # An asterisk "*" as the first entry allows all origins (any entries after it |
| # are ignored); "*" in any other position fails the config. Specific domains |
| # can be listed to restrict access. |
| allowed_origins = ["*"] |
| |
| # Lists allowed headers that can be used in CORS requests. |
| # For example, ["content-type"] permits only the content-type header. |
| allowed_headers = ["content-type", "authorization"] |
| |
| # Headers that browsers are allowed to access in CORS responses. |
| # `iggy-view` carries the current VSR view number; exposing it lets browser |
| # clients read it on cross-origin responses. |
| exposed_headers = ["iggy-view"] |
| |
| # Determines if credentials like cookies or HTTP auth can be included in CORS requests. |
| # `true` allows credentials to be included, useful for authenticated sessions; |
| # it requires explicit (non-wildcard) allowed_origins, allowed_headers, and |
| # exposed_headers. |
| # `false` prevents credentials, enhancing privacy and security. |
| allow_credentials = false |
| |
| # Allows or blocks requests from private networks in CORS. |
| # `true` permits requests from private networks. |
| # `false` disallows such requests, providing additional security. |
| allow_private_network = false |
| |
| # JWT (JSON Web Token) configuration for HTTP. |
| [http.jwt] |
| # Specifies the algorithm used for signing JWTs. |
| # For example, "HS256" indicates HMAC with SHA-256. |
| algorithm = "HS256" |
| |
| # The issuer of the JWT, typically a URL or an identifier of the issuing entity. |
| issuer = "iggy.apache.org" |
| |
| # Intended audience for the JWT, usually the recipient or system intended to process the token. |
| audience = "iggy.apache.org" |
| |
| # Lists valid issuers for JWT validation to ensure tokens are from trusted sources. |
| valid_issuers = ["iggy.apache.org"] |
| |
| # Lists valid audiences for JWT validation to confirm tokens are for the intended recipient. |
| valid_audiences = ["iggy.apache.org"] |
| |
| # Expiry time for access tokens. |
| access_token_expiry = "1 h" |
| |
| # Tolerance for timing discrepancies during token validation. |
| clock_skew = "5 s" |
| |
| # Time before which the token should not be considered valid. |
| not_before = "0 s" |
| |
| # Secret key for encoding JWTs. |
| # If left empty, a secure random secret will be generated on each server start. |
| # In cluster mode a configured secret (identical on every node) makes bearers |
| # valid cluster-wide and activates follower-to-primary HTTP forwarding; with |
| # cluster.auth enabled the key is instead derived from the shared PSK. Without |
| # either, tokens are node-local and forwarding stays disabled. |
| encoding_secret = "" |
| |
| # Secret key for decoding JWTs. |
| # If left empty, a secure random secret will be generated on each server start. |
| decoding_secret = "" |
| |
| # Indicates if the secret key is base64 encoded. |
| # `true` means the secret is base64 encoded. |
| # `false` means the secret is in plain text. |
| use_base64_secret = false |
| |
| # Trusted issuers for A2A (Application-to-Application) authentication. Opt-in: |
| # with none configured the listener accepts only self-issued HS256 tokens. |
| # `issuer`, `audience` and `jwks_url` are required per entry; `user_id` is |
| # optional but defaults to 0 (root), which is rejected - set it to the non-zero |
| # iggy user every token from that issuer is remapped onto. |
| # |
| # Operational note: enabling an issuer opens an outbound JWKS fetch that is |
| # reachable before a token's signature is verified - a token naming this issuer |
| # with an unknown key id can trigger a fetch to `jwks_url`. The target is fixed |
| # (not attacker-chosen); concurrent misses coalesce onto one fetch and repeats |
| # are rate-limited to at most one outbound request per issuer per short window, |
| # so an unknown-key-id flood cannot amplify. The same window bounds how quickly a |
| # freshly rotated signing key is picked up. |
| # [[http.jwt.trusted_issuers]] |
| # issuer = "test-issuer" |
| # jwks_url = "http://127.0.0.1:8081/.well-known/jwks.json" |
| # audience = "iggy.apache.org" |
| # user_id = 1 |
| |
| # Metrics configuration for HTTP. |
| [http.metrics] |
| # Enable or disable the metrics endpoint. |
| # `true` makes metrics available at the specified endpoint. |
| # `false` disables metrics collection. |
| enabled = true |
| |
| # Specifies the endpoint for accessing metrics, e.g., "/metrics". |
| # This route requires authentication like every other read: scrapers must |
| # present a bearer credential (JWT or personal access token). |
| endpoint = "/metrics" |
| |
| # TLS (Transport Layer Security) configuration for HTTP. |
| [http.tls] |
| # Controls the use of TLS for encrypted HTTP connections. |
| # `true` enables TLS, enhancing security. |
| # `false` disables TLS, which may be appropriate in secure internal networks. |
| enabled = false |
| |
| # Path to the TLS certificate file. |
| cert_file = "core/certs/iggy_cert.pem" |
| |
| # Path to the TLS key file. |
| key_file = "core/certs/iggy_key.pem" |
| |
| # TCP server configuration. |
| [tcp] |
| # Determines if the TCP server is active. |
| # `true` enables the TCP server for handling TCP connections. |
| # `false` disables it, preventing any TCP communication. |
| enabled = true |
| |
| # Defines the network address and port for the TCP server. |
| # For example, "127.0.0.1:8090" listens on localhost only on port 8090. |
| address = "127.0.0.1:8090" |
| |
| # TLS configuration for the TCP server. |
| [tcp.tls] |
| # Enables or disables TLS for TCP connections. |
| # `true` secures TCP connections with TLS. |
| # `false` leaves TCP connections unencrypted. |
| enabled = false |
| |
| # Certificate source for the TCP TLS listener. |
| # `true` generates an ephemeral loopback certificate, but only while cert_file |
| # does not exist on disk: an existing PEM pair is loaded instead (the server |
| # logs which way it went). |
| # `false` loads cert_file / key_file, which must both exist. |
| self_signed = true |
| |
| # Path to the TLS certificate file. Loaded when the file exists or |
| # self_signed = false. |
| cert_file = "core/certs/iggy_cert.pem" |
| |
| # Path to the TLS key file. Read together with cert_file. |
| key_file = "core/certs/iggy_key.pem" |
| |
| # QUIC protocol configuration. |
| [quic] |
| # Controls whether the QUIC server is enabled. |
| # `true` enables QUIC for fast, secure connections. |
| # `false` disables QUIC, possibly for compatibility or simplicity. |
| enabled = true |
| |
| # Network address and port for the QUIC server. |
| # For example, "127.0.0.1:8080" binds to localhost on port 8080. |
| address = "127.0.0.1:8080" |
| |
| # How many bidirectional streams a client may keep open at once on a single |
| # QUIC connection. The SDK opens a fresh stream per command, so 1 caps a |
| # connection at one in-flight command: commands do not pipeline, and the next |
| # one waits for the current reply. The known cost is one stream-credit round |
| # trip between back-to-back requests on the same connection. Raising it is a |
| # transport decision only: the server's accept loop takes one bidi at a time |
| # and waits for its reply before accepting the next, which is what keeps |
| # partition operations (they all share the connection's current request id) |
| # distinguishable, and a higher cap does not make the handlers concurrent. |
| max_concurrent_bidi_streams = 1 |
| |
| # Initial path MTU for QUIC connections, before MTU discovery has run. |
| # 1200 bytes is the QUIC minimum and quinn's own default: safe on every |
| # path, and MTU discovery probes upward from it automatically. Values |
| # above the real path MTU cause packet loss until black-hole detection |
| # resets the connection back to 1200. |
| initial_mtu = "1200 B" |
| |
| # Server-side flow-control budget for unacknowledged data the server may have |
| # in flight on one connection. A ceiling, not a reservation, and rarely the |
| # operative limit: the receiver's own window binds first, and the Rust SDK's |
| # QUIC client sets both of its windows to about 100 KB. |
| send_window = "64 MiB" |
| |
| # Server-side flow-control budget for data the server will buffer from one |
| # connection, shared by all of its streams. Symmetric with `send_window`. |
| receive_window = "64 MiB" |
| |
| # Per-stream slice of `receive_window`. Equal to it, because |
| # `max_concurrent_bidi_streams` is 1: with a single stream per connection a |
| # smaller carve-out protects nothing, it only caps the one stream early. |
| # Lower it below `receive_window` if that cap is ever raised, so one unread |
| # stream cannot pin the whole connection window. Must not exceed |
| # `receive_window`; equality is allowed. |
| stream_receive_window = "64 MiB" |
| |
| # Interval for sending QUIC keep-alive PINGs. One third of |
| # `max_idle_timeout` so up to two consecutive losses fit before the |
| # idle timer closes the connection. Set to "0 s" to disable. |
| keep_alive_interval = "10 s" |
| |
| # Maximum idle time before a QUIC connection is closed. Set to |
| # "0 s" to disable (not recommended). |
| max_idle_timeout = "30 s" |
| |
| # QUIC certificate configuration. |
| [quic.certificate] |
| # Certificate source for the QUIC listener. |
| # `true` always generates an ephemeral loopback certificate and ignores |
| # cert_file / key_file even when those files exist on disk; the server logs a |
| # warning naming the files it skipped. |
| # `false` loads cert_file / key_file, which must both exist. |
| self_signed = true |
| |
| # Path to the QUIC TLS certificate file. Required when self_signed = false, |
| # left empty here so the shipped self-signed default has nothing to skip. |
| cert_file = "" |
| |
| # Path to the QUIC TLS key file. Required when self_signed = false. |
| key_file = "" |
| |
| # Personal access token configuration. |
| [personal_access_token] |
| # Sets the maximum number of active tokens allowed per user. |
| max_tokens_per_user = 100 |
| |
| # Personal access token cleaner configuration. |
| [personal_access_token.cleaner] |
| # Enables or disables the token cleaner process. |
| # `true` activates periodic token cleaning. |
| # `false` disables it, tokens remain active until manually revoked or expired. |
| enabled = true |
| |
| # Interval for running the token cleaner. |
| interval = "1 m" |
| |
| # Heartbeat configuration |
| [heartbeat] |
| # Enables or disables the client heartbeat verification process. When |
| # enabled, a connection that sends nothing (no request, no PING) for |
| # 1.2 x interval has its session released so its consumer groups |
| # rebalance off it. Only a connection that still holds a group |
| # membership is evicted; one that holds none is left alone and is |
| # reaped when its socket closes. |
| enabled = true |
| # Interval for expected client heartbeats. The Rust, Go, Python, Node and |
| # async Java SDKs ping automatically every 5 s, well inside the resulting 36 s |
| # staleness window, but only from a connected high-level client: the Rust and |
| # async Java pingers are armed by connect(), so a session that logs in without |
| # it never pings. The blocking Java and the C# SDK have no automatic heartbeat |
| # at all, only a manual ping. Wherever nothing pings, an idle consumer-group |
| # member is evicted, and only the application can keep it alive (ping) or |
| # bring it back afterwards (reconnect). |
| interval = "30 s" |
| |
| # OpenTelemetry configuration |
| [telemetry] |
| # Enables or disables telemetry. |
| enabled = false |
| # Service name for telemetry. |
| service_name = "iggy" |
| |
| # OpenTelemetry logs configuration |
| [telemetry.logs] |
| # Transport for sending logs. Options: "grpc", "http". |
| transport = "grpc" |
| # Endpoint for sending logs. |
| endpoint = "http://localhost:7281/v1/logs" |
| |
| # OpenTelemetry traces configuration |
| [telemetry.traces] |
| # Transport for sending traces. Options: "grpc", "http". |
| transport = "grpc" |
| # Endpoint for sending traces. |
| endpoint = "http://localhost:7281/v1/traces" |
| |
| # System configuration. |
| [system] |
| # Base path for system data storage. |
| path = "local_data" |
| |
| # Runtime configuration. |
| [system.runtime] |
| # Path for storing runtime data. |
| # Specifies the directory where any runtime data is stored, relative to `system.path`. |
| path = "runtime" |
| |
| # Logging configuration. |
| [system.logging] |
| # Path for storing log files. |
| path = "logs" |
| |
| # Log filtering directive using the same syntax as the RUST_LOG environment variable. |
| # Supports simple levels ("trace", "debug", "info", "warn", "error", "off" or "none") |
| # as well as complex directives like "warn,server=debug,iggy=trace". |
| # Note: RUST_LOG environment variable always takes precedence over this setting. |
| level = "info" |
| |
| # Whether to write logs to file. When false, logs are only written to stdout. |
| # When enabled, logs are stored in {system.path}/{system.logging.path} (default: local_data/logs). |
| file_enabled = true |
| |
| # Maximum size of a single log file before rotation occurs. When a log |
| # file reaches this size, it will be rotated (closed and a new file |
| # created). This setting works together with max_total_size to control |
| # log storage. You can set it to 0 to enable unlimited size of single |
| # log, but all logs will be written to a single file, thus disabling |
| # log rotation. Please configure 0 with caution, esp. RUST_LOG > debug |
| max_file_size = "500 MB" |
| |
| # Maximum total size of all log files. When this size is reached, |
| # the oldest log files will be deleted first. Set it to 0 to allow |
| # an unlimited number of archived logs. This does not disable time |
| # based log rotation or per-log-file size limits. |
| max_total_size = "4 GB" |
| |
| # Time interval for checking log rotation status. Avoid less than 1s. |
| rotation_check_interval = "1 h" |
| |
| # Time to retain log files before deletion. Avoid less than 1s, too. |
| retention = "7 days" |
| |
| # Encryption configuration |
| [system.encryption] |
| # Determines whether server-side data encryption for the messages payloads and state commands is enabled (boolean). |
| # `true` enables encryption for stored data using AES-256-GCM. |
| # `false` means data is stored without encryption. |
| enabled = false |
| |
| # The encryption key used when encryption is enabled (string). |
| # Should be a 32 bytes length key, provided as a base64 encoded string. |
| # This key is required and used only if encryption is enabled. |
| key = "" |
| |
| # Stream configuration |
| [system.stream] |
| # Path for storing stream-related data (string). |
| # Specifies the directory where stream data is stored, relative to `system.path`. |
| path = "streams" |
| |
| # Topic configuration |
| [system.topic] |
| # Path for storing topic-related data, relative to `stream.path`. |
| path = "topics" |
| |
| # Retention is per topic, set at CreateTopic and readable on GetTopic: |
| # max_topic_size - delete oldest sealed segments past this size |
| # ("unlimited" when unset) |
| # message_expiry - delete sealed segments older than this ("none" when unset) |
| # Both policies can be active at once; the active segment is never touched. |
| # Call GET /options/topic (or the SDK's describe_options) for the full catalog |
| # with this server's defaults. |
| |
| # Partition configuration |
| [system.partition] |
| # Path for storing partition-related data (string). |
| # Specifies the directory where partition data is stored, relative to `topic.path`. |
| path = "partitions" |
| |
| # Enables checksum validation for data integrity (boolean). |
| # `true` re-hashes every batch a disk poll reads and fails the poll closed on a |
| # mismatch, so a segment damaged at rest is reported instead of served. |
| # `false` skips the re-hash and serves whatever decodes, which hands a consumer |
| # bytes provably not the ones written. Only turn it off with a corruption guard |
| # somewhere else in the stack. |
| validate_checksum = true |
| |
| # Durability and flush cadence are per topic, set at CreateTopic: |
| # enforce_fsync - fdatasync each flush (false when unset) |
| # messages_required_to_save - flush after this many messages (1024) |
| # size_of_messages_required_to_save - flush after this many bytes (1 MiB) |
| # enforce_fsync decides whether a flush syncs, never whether one happens. A |
| # write is acknowledged when it commits, which precedes its flush unless that |
| # commit trips a threshold, and the partition journal holding the unflushed |
| # remainder is in memory and is not replayed at boot. Acks are therefore |
| # durability-gated only at messages_required_to_save = 1. |
| |
| # Segment configuration |
| [system.segment] |
| # Segment size is per topic, set at CreateTopic as `segment_size` (1 GiB when |
| # unset). It is a soft limit: a segment may close one whole batch past it. |
| # Bounds are enforced at creation - a 512 B multiple, at least 1 MiB, and no |
| # larger than 1 GiB. |
| # |
| # `preallocate_segments` is per topic too: it reserves exactly `segment_size` |
| # up front where the filesystem supports it. Off unless a topic asks for it -- |
| # with the default 1 GiB segment size it costs 1 GiB of real disk per |
| # partition at creation. |
| |
| # Configures whether expired segments are archived (boolean) or just deleted without archiving. |
| # Unsupported: setting this to `true` aborts boot. |
| archive_expired = false |
| |
| # Recovery configuration in case of lost data |
| [system.recovery] |
| # Controls whether streams/topics/partitions should be recreated if the expected data for existing state is missing (boolean). |
| # Unsupported: setting this to `true` aborts boot. |
| recreate_missing_state = false |
| |
| # At boot, segment recovery walks each partition's segments: bytes after the |
| # last decodable batch of a genuinely torn tail are physically truncated from |
| # the .log/.index files. Every index entry is derived from a batch the log |
| # already held, so an index is never evidence about the log: one the log |
| # contradicts is repaired, not believed. It is DROPPED whole and rebuilt from |
| # a byte-0 walk when it holds no whole 24-byte entry, when its entries |
| # regress in offset or position, when an entry does not match the log batch's |
| # offset, timestamp, partition and position, when its first entry is not the |
| # segment's own start offset at position 0, or when the log cannot back its |
| # LAST entry. Only an index whose |
| # last entry the log proves anchors a walk, which then runs from that entry |
| # to the file end. Under the topic's enforce_fsync = true an index the log |
| # cannot back is measured against the log first: a gap of more than ONE entry |
| # refuses the partition instead of rebuilding, because fsync lets the index |
| # outrun the log by at most the chunk in flight at the crash, so a wider gap |
| # is previously durable data the log lost. |
| # |
| # The byte-0 walk is deliberately not narrowed to the entries the log still |
| # proves. An anchor picked that way can sit ABOVE damage, and the residue |
| # probe only ever looks forward from where a walk stopped, so a hole under |
| # the anchor would go unread while end_offset still advertised the offsets |
| # over it. Reaching that branch means the index and the log already disagree, |
| # so the extra bytes read are bounded by the segments a crash actually tore. |
| # |
| # Every batch a walk accepts passes its own checksum. With enforce_fsync = |
| # false, the index only locates data because page-cache writeback can preserve |
| # a later log page while losing an earlier one, so recovery walks every segment |
| # from byte 0. A clean walk preserves the existing index and performs no disk |
| # mutation. With enforce_fsync = true, completed serialized log fdatasyncs |
| # prove the prefix before the last index entry, so a healthy boot starts at |
| # that last entry and leaves earlier at-rest damage to the read path's |
| # validate_checksum. Wherever a recovery walk finds damage, it truncates only |
| # when nothing decodable follows and refuses when something does. |
| # |
| # The non-fsync verification pass therefore reads and checksums every log byte |
| # at boot, although it keeps a clean index in place. A byte-0 repair walk also |
| # rebuilds a missing or contradicted index durably, adding two fsyncs per |
| # repaired segment. Either path can take minutes over thousands of segments. |
| # A restore that dropped every .index repairs every segment; an ordinary |
| # fsynced crash usually repairs only its tail segment. A slow boot is therefore |
| # not by itself evidence of a stall. Before, the repair shapes refused boot. |
| # |
| # Either walk refuses a verifying batch whose base offset does not continue |
| # the chain, or whose partition_id stamp is not this partition's. The one |
| # exception is the first batch at an index-derived anchor: the offset it |
| # fails to match is the ENTRY's, not the log's, so a mismatch there breaks |
| # the walk as a stale entry, leaving the verdict to the byte-0 rebuild. A |
| # batch that fails its own checksum is damage rather than data |
| # whatever its fields claim: the walk stops there and the residue goes |
| # through the damage probe like any other torn tail, so a bit flip in a tail |
| # batch's offset truncates instead of refusing the partition, regardless of |
| # the flip's direction. |
| # |
| # Damage in the middle of a segment is never silently truncated: the |
| # partition is refused, as is trailing residue whose verification cost |
| # exhausts the probe's residue-derived work budgets while walked batches |
| # exist (residue after a walk that proved nothing recovers the segment as |
| # empty instead, fencing the file pair aside whole). With peer replicas the |
| # refused files are moved to a .fenced.N directory beside the partition, |
| # which is rebuilt empty and refilled from a peer. With replica_count = 1 |
| # there is no peer, so nothing is moved and nothing is rebuilt: the refused |
| # files stay at their original paths, the partition is tombstoned (unrouted, |
| # clients get the retriable transient status, never an empty poll that would |
| # read as a healthy empty partition), and the same refusal is re-derived and |
| # re-logged on every boot. The tombstone lifts in exactly two ways: a |
| # chain-shape refusal whose planned chain provably holds ZERO recoverable |
| # bytes rebuilds through .fenced.N at boot, and deleting the |
| # stream/topic/partition removes the refused files and clears the fence, so |
| # a recreate of the same ids starts clean. One refusal is not durable: a |
| # length divergence found when a writer reopens a file recovery just |
| # truncated clears on the next boot, so that tombstone lasts only for the |
| # life of the process. |
| # |
| # .fenced.N has a second producer, on a partition that recovers FINE: a tail |
| # segment holding bytes that decode to nothing anywhere is moved there and |
| # the segment recovered empty, so the partition serves without those bytes. |
| # One fresh directory per such segment, so .fenced.0, .fenced.1, ... can |
| # accumulate under a healthy partition. .fenced.N alone therefore does not |
| # mean a partition was refused -- grep the boot log for "refusing the |
| # recovered segment chain", which names the directory and the reason. |
| # |
| # The metadata WAL truncates genuinely torn tails too; interior WAL damage |
| # or oversized trailing bytes refuse boot instead. |
| |
| # Memory pool configuration |
| [system.memory_pool] |
| # Enables or disables the memory pool (boolean). |
| # `true` enables the memory pool. |
| # `false` disables the memory pool. |
| enabled = true |
| |
| # Size of the memory pool (string). |
| # Example: "512 MiB" or "1 GiB". |
| # This defines the maximum, total memory allocated for the memory pool. |
| # Note: This number has to be multiplication of 4096 (default linux page size). |
| # Minimum size is 512 MiB due to internal implementation details. |
| size = "4 GiB" |
| |
| # Maximum number of buffers in each bucket (u32). |
| # There are 32 buckets in the memory pool. Each bucket can hold up to this number of buffers |
| # and holds different buffer sizes, from 256 B to 512 MiB. |
| # Note: This number has to be a power of 2. Minimum value is 128 due to internal implementation details. |
| bucket_capacity = 8192 |
| |
| # Cluster configuration |
| [cluster] |
| # Enables or disables cluster mode (boolean). |
| # When enabled, this node will participate in the cluster and coordinate with other nodes. |
| enabled = false |
| |
| # Unique cluster name (string). |
| # All nodes in the same cluster must share the same name. |
| # This prevents accidental cross-cluster communication. |
| # Permanent on-disk identity: the name is hashed into the cluster id stamped |
| # into every metadata and partition superblock on first boot. Changing it later |
| # makes the server refuse to start against existing data, since the recovered id |
| # no longer matches the configured one. Renaming means starting from an empty |
| # data directory. |
| name = "iggy-cluster" |
| |
| # Consensus runs on a fixed 10ms tick, so every duration below is converted to |
| # whole ticks: values are rounded down to a multiple of 10ms, and anything under |
| # 10ms is raised to a single tick rather than firing sooner. |
| |
| # Backup-side liveness window for a consensus plane's primary (duration). |
| # A replica that sees no primary traffic for this long starts a view change. |
| # Raise it on oversubscribed hosts where scheduling stalls fake primary |
| # death. Must be at least "2s" and at least 4x commit_broadcast_interval: the |
| # primary signals liveness through its commit broadcast, and the window must |
| # span several broadcasts so one delayed broadcast never trips an election. |
| heartbeat_timeout = "5s" |
| |
| # How often the primary broadcasts its commit point to every backup (duration). |
| # This is the cluster's liveness signal: each broadcast resets every backup's |
| # heartbeat_timeout window and carries the latest commit point forward. Must be |
| # nonzero and, with heartbeat_timeout, satisfy heartbeat_timeout >= 4x this |
| # value. Drives the consensus CommitMessage timer. |
| commit_broadcast_interval = "500ms" |
| |
| # How often the primary retransmits prepares that backups have not yet acked |
| # (duration). Lower values recover faster from a dropped prepare at the cost of |
| # more replica traffic; must be nonzero. Drives the consensus Prepare timer. |
| prepare_retransmit_interval = "250ms" |
| |
| # How often a replica retransmits its StartViewChange / DoViewChange while a |
| # view change is in progress (duration). Lower values converge a healthy |
| # election faster at the cost of more replica traffic; must be nonzero. Drives |
| # both consensus view-change retransmit timers. |
| view_change_retransmit_interval = "500ms" |
| |
| # Backstop for a stalled view change (duration): one that does not conclude |
| # within this window escalates to a fresh cluster-wide election. Must be nonzero |
| # and at least 4x view_change_retransmit_interval, so a few dropped view-change |
| # messages retransmit rather than prematurely escalate. |
| view_change_status_timeout = "5s" |
| |
| # How often a recovering or view-change backup re-requests the current view's |
| # StartView from its primary (duration); must be nonzero. Drives the consensus |
| # RequestStartView timer. |
| request_start_view_retransmit_interval = "1s" |
| |
| # How many consecutive unanswered RequestStartView probes a recovering replica |
| # tolerates before falling back to an election (integer). A full-cluster restart |
| # leaves nobody settled to answer, so the replica elects on its recovered log. |
| # Must be between 1 and 100. |
| view_probe_attempts_max = 5 |
| |
| # How long a stalled journal-repair stream waits before re-requesting its |
| # remaining window from the serving peer (duration). Repair frames are |
| # fire-and-forget over the lossy bus, so a session with no retry wedges forever |
| # on a single dropped frame. Paces both the metadata and partition repair loops; |
| # must be nonzero. |
| repair_retry_interval = "1s" |
| |
| # Prepares a peer serves per repair round before the requester walks to the next |
| # chunk (integer). Each frame rides the per-peer message-bus queue, so this must |
| # stay strictly below message_bus.peer_queue_capacity or a full round overruns |
| # the queue and drops frames. Must be > 0 and <= 1024. |
| repair_chunk_max = 128 |
| |
| # How long the metadata superblock may stay unwritable before the replica |
| # fail-stops (duration). A replica that cannot persist its view is already |
| # fenced quorum-invisible and retries with capped backoff; past this window the |
| # process exits with a distinct status so a supervisor restarts or replaces it |
| # instead of an operator finding the wedge in logs. "0" disables the fail-stop |
| # and leaves the replica fenced indefinitely. Nonzero values must be at least |
| # 30s so a transient disk hiccup cannot kill the process. |
| superblock_wedged_fatal_timeout = "2m" |
| |
| # Replica-to-replica authentication (PSK + BLAKE3 keyed-MAC handshake). |
| [cluster.auth] |
| # When true, every replica peer must complete the authenticated handshake or be |
| # rejected, and shared_secret becomes mandatory. Off by default = legacy |
| # unauthenticated replica traffic. Enabling it is a coordinated-restart change. |
| # With http enabled and no http.jwt secrets configured, the PSK also becomes |
| # the JWT key source, making bearers valid cluster-wide and activating |
| # follower-to-primary HTTP forwarding. |
| enabled = false |
| |
| # Cluster-wide pre-shared key, >= 32 bytes of CSPRNG output, byte-identical on |
| # every node. Prefer the IGGY_CLUSTER_AUTH_SHARED_SECRET env var (masked in |
| # logs, never persisted) over storing it on disk. Ignored when enabled = false. |
| shared_secret = "" |
| |
| # Retiring pre-shared key, accepted for verification only during a rolling key |
| # rotation (this node keeps signing with shared_secret). Rotate in three rolls: |
| # 1) shared_secret = old + previous_shared_secret = new on every node, |
| # 2) shared_secret = new + previous_shared_secret = old on every node, |
| # 3) shared_secret = new alone. Leave empty outside a rotation. Same length |
| # floor and env-var preference as shared_secret |
| # (IGGY_CLUSTER_AUTH_PREVIOUS_SHARED_SECRET). |
| previous_shared_secret = "" |
| |
| # Replica-to-replica TLS for the consensus (tcp_replica) port. |
| [cluster.tls] |
| # When true every replica connection is wrapped in TLS 1.3 (ALPN |
| # "iggy-replica") before the replica handshake runs. Requires |
| # cluster.auth.enabled: TLS carries no client certificates, so it |
| # authenticates the acceptor only; the PSK handshake authenticates the |
| # peer, TLS supplies confidentiality. Off by default = plaintext replica |
| # traffic. Enabling it is a coordinated-restart change: a TLS dialer |
| # cannot talk to a plaintext acceptor or vice versa. |
| enabled = false |
| |
| # When true the node auto-generates a self-signed certificate at boot and |
| # the dialer accepts ANY peer certificate. When false (default), |
| # cert_file / key_file / ca_file are all required. |
| self_signed = false |
| |
| # PEM certificate chain presented by this node's acceptor side. |
| cert_file = "" |
| |
| # PEM private key matching cert_file. |
| key_file = "" |
| |
| # PEM trust anchor(s) the dialer verifies peer certificates against. |
| # Unused when self_signed = true. |
| # The dialer verifies each peer against the 'ip' string from that peer's |
| # cluster.nodes entry, and that string is the TLS server name as well as a |
| # literal IP, so every peer certificate needs a matching IP SAN. A |
| # certificate carrying only DNS SANs fails verification. |
| ca_file = "" |
| |
| # Shard-0 coordinator placement. |
| [cluster.coordinator] |
| # When the server runs more than one shard, exclude shard 0 from replica |
| # placement. Shard 0 already hosts the coordinator, the metadata writer, and |
| # both listeners; replica connections are long-lived steady flows, so they are |
| # offloaded to peer shards by default. |
| skip_shard_zero_for_replicas = true |
| |
| # When the server runs more than one shard, exclude shard 0 from client |
| # placement. Off by default: client connections are short-lived and benefit |
| # from shard-0 parallelism more than replicas do. |
| skip_shard_zero_for_clients = false |
| |
| # Full roster of cluster members. Byte-identical on every node. The running |
| # node's identity is resolved at launch from the '--replica-id <N>' CLI |
| # flag, which selects the entry in this list that describes the current |
| # node. All other entries are remote peers. |
| # |
| # 'ip' is the node's roster address. Replica-to-replica traffic and |
| # follower-to-primary HTTP forwarding use it. It is not the bind interface for |
| # tcp/quic/http/websocket, which comes from each transport's own 'address' |
| # setting above; the roster supplies those transports their port only. A |
| # cluster spread across hosts therefore needs each transport's 'address' set to |
| # '0.0.0.0' or the routable NIC; the defaults below listen on loopback only, |
| # and a bind that cannot serve the advertised 'ip' is warned about at startup. |
| # |
| # Each node may also set 'advertised_address': the client-facing address |
| # handed out in cluster metadata and leader redirects. Set it when 'ip' is |
| # a private replica-network address unreachable by clients (Docker, |
| # Kubernetes, NAT). Accepts a literal IPv4/IPv6 address or a DNS hostname |
| # (RFC 1123: ASCII letters, digits, '-' and '.'; no port, no trailing dot). |
| # When unset, clients receive 'ip'. |
| # |
| # When different client networks need different addresses (a public |
| # 'advertised_address' would route in-VPC clients out through the public |
| # side), add per-network 'advertised_addresses' selectors: clients whose |
| # peer IP falls inside 'client_cidr' are handed 'address' instead of the |
| # catch-all. 'address' takes the same forms as 'advertised_address' |
| # (literal IP or RFC 1123 hostname, never a port - ports always come from |
| # 'ports'). At most 16 selectors per node; boot also rejects duplicate |
| # 'client_cidr' entries on one node (compared truncated, so '10.0.1.0/16' |
| # duplicates '10.0.0.0/16') and any two nodes advertising one host:port |
| # to overlapping client sets - reusing a host:port across nodes is legal |
| # only when no client would resolve both nodes to it. |
| # |
| # The longest matching prefix wins; clients matching no selector fall |
| # back to 'advertised_address', then 'ip'. Matching is per address |
| # family: '0.0.0.0/0' matches no IPv6 client and '::/0' matches no IPv4 |
| # client, so covering both families takes one selector per family (or the |
| # catch-all). IPv4-mapped IPv6 CIDRs ('::ffff:10.0.0.0/104') match like |
| # their IPv4 form only at prefix length 96 or longer; shorter ones match |
| # native IPv6 clients only. Matching sees the transport-level peer |
| # address, so clients behind a proxy or load balancer match the proxy's |
| # network, not their own. |
| # |
| # Every 'address' must be routable from inside its own 'client_cidr': |
| # leader-aware SDK clients redial whatever address metadata advertises, |
| # so a selector pointing at a host its own clients cannot reach strands |
| # them mid-redirect. Prefer literal IPs over hostnames - the SDKs differ |
| # in how they compare an advertised hostname against the address they |
| # dialed, and a mismatch costs a reconnect on every fresh connect. |
| # |
| # Note for rolling upgrades: older server binaries reject a TOML config |
| # containing 'advertised_addresses' but silently ignore the equivalent |
| # 'IGGY_CLUSTER_NODES_*_ADVERTISED_ADDRESSES_*' env vars; either way, |
| # upgrade every binary first, then add selectors. Mid-upgrade, an env-var |
| # roster would serve selector addresses from upgraded nodes and the |
| # catch-all from the rest. |
| # |
| # [[cluster.nodes]] |
| # name = "iggy-node-1" |
| # ip = "10.0.1.5" # replica plane, literal IP only |
| # advertised_address = "203.0.113.10" # catch-all for unmatched clients |
| # replica_id = 0 |
| # ports = { tcp = 8090, http = 3000, tcp_replica = 9090 } |
| # |
| # [[cluster.nodes.advertised_addresses]] |
| # client_cidr = "10.0.0.0/16" # in-VPC clients stay private |
| # address = "10.0.1.5" |
| # |
| # In cluster mode, 'ports' is the single source of listener ports: every |
| # enabled transport needs an explicit per-node port, otherwise the server |
| # refuses to start. |
| [[cluster.nodes]] |
| name = "iggy-node-1" |
| ip = "127.0.0.1" |
| replica_id = 0 |
| ports = { tcp = 8090, quic = 8080, http = 3000, websocket = 8092, tcp_replica = 9090 } |
| |
| [[cluster.nodes]] |
| name = "iggy-node-2" |
| ip = "127.0.0.1" |
| replica_id = 1 |
| ports = { tcp = 8091, quic = 8081, http = 3001, websocket = 8093, tcp_replica = 9091 } |
| |
| # Example additional node (commented out). tcp skips 8092-8094: those are the |
| # websocket ports of the three nodes, which collide once nodes share a host. |
| # [[cluster.nodes]] |
| # name = "iggy-node-3" |
| # ip = "192.168.1.100" |
| # advertised_address = "iggy-node-3.example.com" |
| # replica_id = 2 |
| # ports = { tcp = 8095, quic = 8082, http = 3002, websocket = 8094, tcp_replica = 9092 } |
| |
| # Sharding configuration |
| [system.sharding] |
| # CPU allocation - controls the number of shards and their CPU affinity. |
| # Possible values: |
| # - "all": Use all available CPU cores (default) |
| # - numeric value (e.g. 4): Use 4 shards (4 threads pinned to cores 0, 1, 2, 3) |
| # - range (e.g. "5..8"): Use 3 shards with affinity to cores 5, 6, 7 |
| # - numa settings: |
| # + "numa:auto": Use all available numa node, cores |
| # + "numa:nodes=0,1;cores=4;no_ht=true": Use NUMA node 0 and 1, each nodes use 4 cores, and no hyperthreads |
| cpu_allocation = "numa:auto" |
| |
| # Whether shard threads are pinned to dedicated CPU cores (default: true). |
| # Pinned cores are drawn from the process's allowed CPU set (affinity/cpuset |
| # mask), so the server cooperates with systemd `AllowedCPUs=` and container |
| # cpusets. Set to false when the server shares cores with other workloads |
| # (e.g. a multi-tenant host slicing CPU via cgroup quotas): unpinned shards |
| # let the kernel scheduler place threads freely instead of piling every |
| # process onto the same low-numbered cores. |
| pin_cores = true |
| |
| # Per-shard inter-shard inbox capacity (the main lane: consensus frames, |
| # connection setup, reconcile wakes). Bounded by design; consensus-frame |
| # drops recover via VSR retransmit. Size for the consensus working set: |
| # ~ the prepare queue depth of the planes the shard hosts ([metadata] on |
| # shard 0, [partition] elsewhere) times replica_count times directions. |
| # Both depths are tunable, so raising either raises the capacity needed here. |
| inbox_capacity = 1024 |
| |
| # Capacity of the reply lane: a separate bounded channel for cross-shard |
| # client-reply forwards, whose drops are terminal (no in-protocol |
| # retransmit - the client never receives the reply). Split from |
| # inbox_capacity so a consensus burst cannot evict reply forwards; size for |
| # peak client-reply fan-out per shard. |
| reply_inbox_capacity = 1024 |
| |
| # Wall-clock budget for a single shard's bus drain on shutdown. Drives |
| # the per-shard watchdog and the parallel-join survivor path; sized |
| # larger than typical TCP RTT times in-flight write-batch so writers |
| # receive their full last `write_vectored_all` budget before the |
| # connection registry force-tears the bus. Slow-fsync hosts may need |
| # to extend this past the default. |
| shutdown_drain_timeout = "10 s" |
| |
| # Poll cadence for the cross-thread shutdown flag and for the |
| # metadata-handoff loops. Trades off Ctrl-C latency against idle wakeup |
| # cost; the default keeps shutdown observably prompt without measurable |
| # scheduler overhead. Must be less than or equal to shutdown_drain_timeout. |
| shutdown_poll_interval = "50 ms" |
| |
| # Hard wall-clock deadline for joining shard threads at process exit. A |
| # shard whose pump or listener wedges past this budget is abandoned with |
| # an error log instead of blocking exit forever. Must be at least |
| # shutdown_drain_timeout, or shards would be abandoned mid-drain. |
| shutdown_join_timeout = "30 s" |
| |
| # Safety-tick cadence for the partition reconciliation loop. The reconciler |
| # also wakes on every metadata commit from shard 0, so this only covers |
| # dropped wake-ups and the initial post-bootstrap convergence window. |
| reconcile_periodic_interval = "1 s" |
| |
| # WebSocket listener configuration. The frame-tuning knobs below are the |
| # live source for the server's WS / WSS plane; they are folded into a |
| # compio-ws WebSocketConfig once at bus construction. Each size knob is |
| # optional: commenting it out keeps the compio-ws (tungstenite) default |
| # noted next to it. A malformed size string fails config load. |
| [websocket] |
| enabled = true |
| address = "127.0.0.1:8092" |
| |
| # Target minimum size of the frame read buffer. compio-ws default: "128 KiB". |
| # read_buffer_size = "128 KiB" |
| |
| # Target buffer size for batched writes before flush. compio-ws |
| # default: "128 KiB". |
| # write_buffer_size = "128 KiB" |
| |
| # Hard ceiling on the write buffer; writes past it error instead of |
| # buffering, so it must exceed write_buffer_size by at least one message. |
| # compio-ws default: unlimited. |
| # max_write_buffer_size = "128 MiB" |
| |
| # Hard upper bound on a single inbound WebSocket message |
| # (post-fragment-reassembly). Must not exceed message_bus.max_message_size. |
| # compio-ws default: "64 MiB". |
| # max_message_size = "64 MiB" |
| |
| # Hard upper bound on a single inbound WebSocket frame |
| # (pre-fragment-reassembly). Must not exceed max_message_size. |
| # compio-ws default: "16 MiB". |
| # max_frame_size = "16 MiB" |
| |
| # Whether to accept unmasked frames from clients in violation of |
| # RFC 6455 client-to-server framing rules. Strict (false) by default. |
| accept_unmasked_frames = false |
| |
| [websocket.tls] |
| enabled = false |
| |
| # Certificate source for the WSS listener. |
| # `true` generates an ephemeral loopback certificate, but only while cert_file |
| # does not exist on disk: an existing PEM pair is loaded instead (the server |
| # logs which way it went). |
| # `false` loads cert_file / key_file, which must both exist. |
| self_signed = true |
| |
| # Loaded when the file exists or self_signed = false. |
| cert_file = "core/certs/iggy_cert.pem" |
| key_file = "core/certs/iggy_key.pem" |
| |
| # Metadata consensus plane tunables (shard 0's VSR replica: users, |
| # streams, topics, sessions). Size these together: a deeper prepare queue |
| # admits more concurrent in-flight metadata ops (e.g. login storms), and |
| # the journal must hold enough slots that a forced checkpoint (triggered |
| # when remaining slots fall to the checkpoint margin, which itself is |
| # max(64, prepare_queue_depth)) stays rare. Validation enforces |
| # journal_slots >= 4 * max(64, prepare_queue_depth). |
| [metadata] |
| # Depth of the metadata prepare queue: how many uncommitted metadata ops |
| # may be in flight at once. Submits beyond it are rejected with the |
| # transient "metadata prepare queue is full" and retried by the SDK. |
| # Capped at 127 by the view-change wire format: a DoViewChange describes the |
| # uncommitted suffix with one nack bit and one present bit per entry in a u128 each, |
| # so a deeper queue produces entries a view change can neither adopt nor prove dead. |
| prepare_queue_depth = 32 |
| |
| # Size of the metadata WAL's in-memory index, in slots (one committed but |
| # not-yet-snapshotted op per slot). Larger values buy more headroom |
| # between forced checkpoints at the cost of memory and bigger WAL |
| # rewrites per checkpoint. Reducing this for an existing data directory is |
| # refused at boot if the live WAL suffix collides in the smaller index; |
| # restore the previous value to recover. |
| journal_slots = 1024 |
| |
| # Slot count of the VSR client table: how many distinct clients (TCP/QUIC/WS |
| # virtual clients and HTTP sessions together) hold live session state at once. |
| # When full, the client whose last commit is oldest is evicted and its next |
| # request re-registers. The HTTP session cap tracks this at half, so raising |
| # it lifts both. Must be between 2 and 65536. |
| clients_table_max = 8192 |
| |
| # Per-partition consensus plane tunables. Unlike [metadata] (one shard-0 |
| # plane), a pipeline exists per partition, so raising this multiplies pinned |
| # request-buffer memory by the partition count. Keep it modest. |
| [partition] |
| # Depth of a partition's prepare queue: how many uncommitted produce / |
| # consumer-offset ops may be in flight at once for that partition. Submits past |
| # it spill into a request queue of twice this depth; once both are full the |
| # server drops the request without a reply and the client retries on its own |
| # request timeout. Must be > 0 and <= 127: the ceiling is the view-change wire, not |
| # memory. A DoViewChange describes the uncommitted suffix with one bit per op in a |
| # u128 bitset, and this depth bounds that suffix. |
| prepare_queue_depth = 32 |
| |
| # Distinct clients each partition group tracks request watermarks for, so a |
| # retried produce or consumer-offset write is answered instead of committing a |
| # second time. At capacity the client whose newest commit is oldest is evicted; |
| # that client's next replay re-executes, exactly as it would have before dedup |
| # existed, so under-sizing degrades rather than breaks. Must be > 0 and <= 65536. |
| # |
| # Unlike [metadata] clients_table_max, this budget is PER GROUP, so worst-case |
| # memory scales with partition count. Size it to the producers one partition |
| # actually sees, not the node's client total. |
| dedup_clients_max = 4096 |
| |
| # Entries the evicted ring retains per multi-replica partition for journal |
| # repair after a peer rejoins. Larger widens the window a restarting peer can be |
| # served from the ring before falling back to bulk sync, at the cost of pinned |
| # memory per partition. Must be > 0 and <= 65536. Single-replica partitions |
| # retain nothing regardless. |
| evicted_ring_capacity = 4096 |
| |
| # Byte ceiling for the evicted ring per partition; whichever ring cap (this or |
| # evicted_ring_capacity) trips first evicts. Bounds the ring memory a burst of |
| # large batches can pin. Must be > 0 and <= "256 MiB". |
| evicted_ring_bytes_max = "16 MiB" |
| |
| # Byte budget for segment payloads a SERVING shard keeps resident to answer |
| # state-transfer chunk requests. PER SHARD, and shard count defaults to core |
| # count, so the process-wide high-water is this times the core count on top of |
| # page cache -- keep that product in mind before raising it. The default is a |
| # FIXED 2176 MiB: two sealed segments at the built-in 1 GiB segment ceiling, |
| # each of which can close one whole message_bus.max_message_size past its |
| # target, which is why it is not 2 GiB. That ceiling is a compile-time |
| # constant: no config key moves it, and a per-topic segment_size is refused |
| # above it. How many groups this shard serves at once is |
| # floor(this / max(partition.transfer_artifact_bytes_max, 1 GiB + 64 MiB)), |
| # minimum one. So raising transfer_artifact_bytes_max without raising this |
| # lowers concurrency and can take it to one, serialising rejoins; boot warns |
| # when it drops below two. |
| # Below one segment a single rejoining node thrashes the cache by itself and |
| # every miss re-reads and re-hashes a whole segment to serve one 256 KiB chunk. |
| # Running under the budget costs re-reads, not failures. |
| # Must be > 0 and <= "64 GiB". |
| transfer_served_cache_bytes_max = "2176 MiB" |
| |
| # Alloc ceiling for ONE received state-transfer artifact, per shard. The |
| # receiver holds it resident through verify, walk and staging write, and up to |
| # four transfers run at once. MUST cover the largest segment any topic may be |
| # created with (a fixed 1 GiB, since segment_size is a per-topic option now) |
| # plus message_bus.max_message_size, because a segment may close one whole batch |
| # past its cap: under that, a legal segment is refused, the whole manifest with |
| # it, and the partition livelocks re-requesting it from every peer. Boot |
| # validates the floor. |
| # |
| # The shipped value sits EXACTLY at that floor: 1024 MiB + the shipped 64 MiB |
| # max_message_size. Raising max_message_size alone therefore refuses boot -- |
| # raise this one by the same amount in the same edit (max_message_size itself |
| # tops out at the frozen 256 MiB recovery ceiling; see its own note). Raising |
| # this above the floor for headroom also DIVIDES the serving concurrency |
| # derived from transfer_served_cache_bytes_max above, so raise that in step. |
| # Must be > 0 and <= "64 GiB". |
| transfer_artifact_bytes_max = "1088 MiB" |
| |
| # Message bus configuration. |
| # Tunables for the inter-shard / inter-replica internal bus that ships |
| # consensus traffic between replicas and SDK-client traffic between |
| # shards. These knobs are consensus-liveness-critical (max_batch gates |
| # throughput under backpressure). Defaults match |
| # core::message_bus::config::MessageBusConfig::default(). |
| |
| [message_bus] |
| # Maximum number of BusMessage entries coalesced into a single writev(2) |
| # call. Hard upper bound: IOV_MAX/2 = 512 on Linux. |
| max_batch = 256 |
| |
| # Wire-level cap on a single framed message. Boot refuses a value above the |
| # frozen 256 MiB ceiling: segment recovery derives fixed scan and allocation |
| # limits from the widest LEGAL record, so batches admitted above the ceiling |
| # would be refused as implausible on a later boot. Raising it past 64 MiB |
| # also breaks Go clients, whose frame cap is a hard 64 MiB constant. |
| max_message_size = "64 MiB" |
| |
| # Bound on the per-peer mpsc queue. The writer task drains; the |
| # send_to_* path enqueues. |
| peer_queue_capacity = 256 |
| |
| # Interval between outbound reconnect attempts to peers with peer_id > self_id. |
| reconnect_period = "5 s" |
| |
| # Timeout for per-peer close drain (flush writer, tear down reader) |
| # before force-cancellation. |
| close_peer_timeout = "2 s" |
| |
| # Wall-clock bound on a single stream.shutdown() / ws.close() in the |
| # safe-shutdown sequence of the TLS-family transports. |
| close_grace = "2 s" |
| |
| # Wall-clock bound on a single connection's handshake phase. Threaded |
| # into compio::time::timeout(handshake_grace, ...) at each accept site |
| # (TCP-TLS rustls accept, WS HTTP-Upgrade, WSS combined TLS+WS, QUIC |
| # connecting.await + accept_bi.await) so a slowloris peer cannot pin |
| # per-conn channels + registry slot + spawned task indefinitely. |
| handshake_grace = "10 s" |