This is an automated email from the ASF dual-hosted git repository. hubcio pushed a commit to branch docs/align-with-topic-durability in repository https://gitbox.apache.org/repos/asf/iggy-website.git
commit 4f05859cb81939ed969543f3d74638728207bd7d Author: hubcio <[email protected]> AuthorDate: Fri Sep 11 00:16:49 2026 +0200 fix(docs): align clustering with server 0.9.0 --- content/docs/clustering/client-failover.mdx | 18 ++++++++++-------- content/docs/clustering/configuration.mdx | 28 +++++++++++++++++++++------- content/docs/clustering/deploy.mdx | 21 +++++++++++++++------ content/docs/clustering/durability.mdx | 29 ++++++++++++++++------------- content/docs/clustering/security.mdx | 6 ++++-- content/docs/clustering/vsr.mdx | 8 ++++---- 6 files changed, 70 insertions(+), 40 deletions(-) diff --git a/content/docs/clustering/client-failover.mdx b/content/docs/clustering/client-failover.mdx index ebe9de48..ea86024e 100644 --- a/content/docs/clustering/client-failover.mdx +++ b/content/docs/clustering/client-failover.mdx @@ -7,28 +7,30 @@ A clustered deployment changes what a client can expect from any single node: re ## Where requests are served -**Reads** are served from the local replicated state on whichever node owns the namespace locally, followers included. A client connected to a follower can poll messages and read metadata without touching the primary. +**Reads** use the local replicated state by default on whichever node owns the namespace locally, followers included. A client connected to a follower can poll messages and read metadata without touching the primary. -**Writes** go through consensus and are admitted on the plane's primary *only*: metadata operations on the metadata primary, message and offset writes on the primary of that partition's consensus group. A write returns after the required VSR commit, or returns a retryable error when the replica is changing view or catching up. +**Writes** go through consensus and are admitted on the plane's primary *only*: metadata operations on the metadata primary, message and offset writes on the primary of that partition's consensus group. An acknowledged write returns after the required VSR commit, or returns a retryable error when the replica is changing view or catching up. HTTP `ack=none` returns after dispatch instead. -One caveat on follower reads: server-managed offset auto-commit replicates the polled offset through the partition consensus, which only the partition primary may do. A poll served by a follower **does not advance the durable consumer offset**. Auto-commit is best-effort *at-least-once* delivery either way, so this widens the redelivery window after failover rather than losing data. +One caveat on follower reads: server-managed offset auto-commit replicates the polled offset through the partition consensus, which only the partition primary may do. A poll served by a follower **does not advance the durable consumer offset**. Auto-commit is best-effort and the poll response does not await offset persistence. On a primary, the offset submission precedes delivery of the poll reply, so it does not guarantee that the application received or processed the batch. Store offse [...] ## Leader redirection -Leader-aware SDKs find the primary instead of requiring you to point them at it: +Leader-aware SDKs discover the metadata primary. Rust binary clients follow this sequence: -1. The client connects to any node from its connection string and fetches cluster metadata, which lists every node with its name, client endpoints, role, and status. +1. The client connects and authenticates to its configured node, then fetches cluster metadata, which lists every node with its name, client endpoints, role, and status. 2. When the connected node isn't the leader, the client reconnects to the leader's advertised endpoint for the transport in use. -3. When the cluster is transiently leaderless (for roughly one heartbeat timeout after a primary fails, while the election completes), the client polls the metadata until a leader appears instead of failing. +3. When the cluster is transiently leaderless, the client polls metadata every 250ms for up to 5s. If no healthy leader appears within that window, it continues on the contacted node and lets subsequent requests report their outcome. 4. Redirects are **capped** (three in the Rust SDK) so a flapping roster can't bounce the client forever. The address a client is redirected to is the node's `advertised_address` (or the matching `advertised_addresses` selector), falling back to the roster `ip`. Getting those right matters: the client redials **exactly what the metadata advertises**. See [Configuration](/docs/clustering/configuration) for the selector rules. +Metadata and partition primaries can differ. After a `TransientNotAccepted` response, Rust binary clients check the metadata leader and can then try the remaining advertised endpoints in a bounded roster walk. This fallback is separate from the three-redirect limit. + The Rust implementation lives in [`core/sdk/src/leader_aware.rs`](https://github.com/apache/iggy/blob/master/core/sdk/src/leader_aware.rs). ## Retryable errors -During a view change or while a replica catches up, writes fail with transient errors rather than definitive ones. The SDKs replay those transparently, so a primary failover surfaces as **added latency rather than an error**, as long as it completes within the client's retry budget. Definitive errors (validation failures, permission denials) are **never replayed**. +During a view change or while a replica catches up, writes can return transient errors. Rust binary clients retry within their request budgets. `TransientNotAccepted` proves that the request was never admitted and permits retry on another node. `TransientNotCommitted` has an uncertain outcome and is replayed with the same session and request identity, then returned if the budget expires. Disconnects and exhausted routing attempts can still reach the caller. Definitive validation and perm [...] ## SDK support @@ -50,4 +52,4 @@ Run the suite for one SDK with: ./scripts/run-bdd-tests.sh rust leader_redirection ``` -For SDKs without redirection coverage, connect clients to the current primary for write-heavy workloads, or front the cluster's HTTP transport with a load balancer and rely on [follower HTTP forwarding](/docs/clustering/security) for control-plane operations. +For SDKs without redirection coverage, connect clients to the current primary for write-heavy workloads, or front the cluster's HTTP transport with a load balancer and rely on [follower HTTP forwarding](/docs/clustering/security) for control-plane operations and acknowledged partition writes. diff --git a/content/docs/clustering/configuration.mdx b/content/docs/clustering/configuration.mdx index 1bb5acb0..aea59257 100644 --- a/content/docs/clustering/configuration.mdx +++ b/content/docs/clustering/configuration.mdx @@ -5,7 +5,7 @@ description: Reference for the [cluster] settings, consensus timing, and node ro All clustering settings live in the `[cluster]` section of the server configuration. The authoritative defaults with detailed comments are in [`core/server/config.toml`](https://github.com/apache/iggy/blob/master/core/server/config.toml). Every value can also be set through an `IGGY_`-prefixed environment variable, for example `IGGY_CLUSTER_ENABLED=true` or `IGGY_CLUSTER_NODES_0_IP=10.0.1.5`. -The server validates the whole section at boot and **refuses to start** on an invalid value, printing the exact rule that failed. +With clustering enabled, the server validates the section at boot and **refuses to start** on an invalid value, printing the rule that failed. The repair chunk limit and superblock failure timeout are validated even when clustering is disabled. ## Top-level settings @@ -18,7 +18,7 @@ The server validates the whole section at boot and **refuses to start** on an in ## Consensus timing -Consensus runs on a **fixed 10ms tick**. 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. +Consensus runs on a **fixed 10ms tick**. 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. `repair_gap_debounce_interval` has an additional 500ms floor. | Setting | Default | Purpose | | --- | --- | --- | @@ -30,6 +30,7 @@ Consensus runs on a **fixed 10ms tick**. Every duration below is converted to wh | `request_start_view_retransmit_interval` | `1s` | How often a recovering replica re-requests the current view's `StartView` | | `view_probe_attempts_max` | `5` | Unanswered `RequestStartView` probes a recovering replica tolerates before falling back to an election | | `repair_retry_interval` | `1s` | How long a stalled journal-repair stream waits before re-requesting its remaining window | +| `repair_gap_debounce_interval` | `1s` | How long a committed-history gap waits before opening a repair session; floored at 500ms | | `repair_chunk_max` | `128` | Prepares a peer serves per repair round | Raise `heartbeat_timeout` on oversubscribed hosts where scheduling stalls fake primary death. Otherwise keep the defaults unless tests show a specific scheduling or network problem. @@ -44,9 +45,22 @@ Boot-time validation enforces these constraints: - `view_probe_attempts_max` must be between 1 and 100. - `repair_chunk_max` must be between 1 and 1024, and strictly below `message_bus.peer_queue_capacity`: each repair frame rides the per-peer message-bus queue, and a full round above that capacity would overrun the queue and drop frames. +## Superblock failure handling + +`superblock_wedged_fatal_timeout` defaults to `2m`. It controls how long repeated superblock persistence failures can continue before the process exits. The affected consensus group stays fenced while persistence is failing. `0`, `disabled`, or `unlimited` disables the process exit, leaving the group fenced; a nonzero value must be at least `30s`. + +## Coordinator placement + +These settings live under `[cluster.coordinator]` and apply when the server has more than one shard: + +| Setting | Default | Purpose | +| --- | --- | --- | +| `skip_shard_zero_for_replicas` | `true` | Places replica connections on peer shards instead of shard 0 | +| `skip_shard_zero_for_clients` | `false` | When enabled, excludes shard 0 from client connection placement | + ## Node roster -`cluster.nodes` is the full roster of cluster members and is intended to be **byte-identical on every node**. The running node finds its own entry through the `--replica-id` CLI flag. +`cluster.nodes` is the full roster of cluster members and is intended to be **byte-identical on every node**. The running node finds its own entry through the `--replica-id` CLI flag. Environment variables replace the entire `cluster.nodes` array rather than merging individual fields into TOML entries. If you configure any roster entry through environment variables, provide the complete roster that way. ```toml [[cluster.nodes]] @@ -63,7 +77,7 @@ Node name, unique within the roster and non-empty. ### `ip` -The node's roster address. Replica-to-replica consensus traffic and follower-to-primary HTTP forwarding dial it, so it must be a **literal IPv4 or IPv6 address**. Hostnames are rejected when the server resolves the roster at startup. +The node's roster address. Replica-to-replica consensus traffic and follower-to-primary HTTP forwarding dial it, so it must be a **literal IPv4 or IPv6 address**. Hostnames and unspecified addresses (`0.0.0.0` or `::`) are rejected when the server resolves the roster at startup. `ip` is **not the bind interface** for the client transports. `tcp`, `quic`, `http`, and `websocket` bind whatever their own `address` settings say. The roster supplies only their ports. The defaults bind loopback, so a cluster spread across hosts needs each transport's `address` set to `0.0.0.0` or the routable interface. The server warns at startup when a bind cannot serve the advertised `ip`. @@ -73,7 +87,7 @@ Numeric replica id for VSR consensus, `0`-based. Ids must be unique and strictly ### `ports` -Per-node listener ports: `tcp`, `quic`, `http`, `websocket`, and `tcp_replica`. 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. `tcp_replica` carries consensus traffic and is always required. Port `0` is rejected, and no two roster entries may claim the same `ip:port`. +Per-node listener ports: `tcp`, `quic`, `http`, `websocket`, and `tcp_replica`. 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. `tcp` and `tcp_replica` are always required, even when client TCP is disabled. `tcp_replica` carries consensus traffic. Port `0` is rejected, and no two roster entries may claim the same `ip:port`. ### `advertised_address` @@ -89,7 +103,7 @@ name = "iggy-node-1" ip = "10.0.1.5" # replica plane + last-resort fallback advertised_address = "203.0.113.10" # catch-all for unmatched clients replica_id = 0 -ports = { tcp = 8090, http = 3000, tcp_replica = 9090 } +ports = { tcp = 8090, quic = 8080, http = 3000, websocket = 8093, tcp_replica = 9090 } [[cluster.nodes.advertised_addresses]] client_cidr = "10.0.0.0/16" # in-VPC clients stay private @@ -105,7 +119,7 @@ Rules and caveats: - 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 clients redial whatever address metadata advertises, and a selector pointing at an unreachable host strands them mid-redirect. - Prefer literal IPs over hostnames. SDKs differ in how they compare an advertised hostname against the address they dialed, and a mismatch costs a reconnect on every fresh connect. -- Rolling upgrades: older server binaries reject a TOML config containing `advertised_addresses` but **silently ignore** the equivalent `IGGY_CLUSTER_NODES_*_ADVERTISED_ADDRESSES_*` env vars. Upgrade every binary first, then add selectors. +- Rolling upgrades: VSR builds predating selector support reject a TOML config containing `advertised_addresses`. Their release builds warn and ignore the equivalent `IGGY_CLUSTER_NODES_*_ADVERTISED_ADDRESSES_*` env vars; debug builds can panic on them. Upgrade every binary first, then add selectors. ## Authentication and TLS diff --git a/content/docs/clustering/deploy.mdx b/content/docs/clustering/deploy.mdx index 72d0d8c1..2d7afeb7 100644 --- a/content/docs/clustering/deploy.mdx +++ b/content/docs/clustering/deploy.mdx @@ -30,7 +30,7 @@ The default `core/server/config.toml` has clustering disabled, so a plain run st cargo run --bin iggy-server -- --with-default-root-credentials ``` -`--with-default-root-credentials` sets the root user to `iggy`/`iggy` unless `IGGY_ROOT_USERNAME` and `IGGY_ROOT_PASSWORD` are already exported. **Only the first boot** on an empty data directory reads these values. +`--with-default-root-credentials` sets the root user to `iggy`/`iggy` unless `IGGY_ROOT_USERNAME` and `IGGY_ROOT_PASSWORD` are already exported. These values initialize the root user **only on its first creation**. Later starts recover the stored user; supplied environment values are still validated at boot. When clustering is disabled, `--replica-id 0` is still accepted. Any other id is rejected because it would have to match a `cluster.nodes` entry. @@ -79,7 +79,7 @@ export IGGY_ROOT_PASSWORD=iggy export IGGY_CLUSTER_AUTH_SHARED_SECRET="replace-with-at-least-32-random-bytes" ``` -Root credentials are **mandatory** once the cluster is enabled. The shared secret must be **at least 32 bytes**. See [Security](/docs/clustering/security) for how it's used and rotated. +Root credentials are **mandatory on the first boot of each replica** when clustering is enabled. Restarts recover the stored root user. The shared secret must be **at least 32 bytes**. See [Security](/docs/clustering/security) for how it's used and rotated. Start each replica in a separate terminal. Use a different data path for every process: @@ -101,7 +101,7 @@ In cluster mode: - `--replica-id` is required and must match exactly one `cluster.nodes` entry - `replica_id` values must be unique and contiguous from `0` - `ports` is the single source of listener ports: every enabled transport needs an explicit per-node port, otherwise the server **refuses to start** -- `tcp_replica` carries replica-to-replica consensus traffic and is **always required** +- `tcp` and `tcp_replica` ports are **always required**, even when client TCP is disabled; `tcp_replica` carries replica-to-replica consensus traffic - `ip` must be a **literal IP address**. Use `advertised_address` when clients can't reach it (see [Configuration](/docs/clustering/configuration)) - use a different root `path` for each process on the same host @@ -126,7 +126,16 @@ IGGY_CLUSTER_NODES_0_PORTS_WEBSOCKET=8093 IGGY_CLUSTER_NODES_0_PORTS_TCP_REPLICA=9090 IGGY_CLUSTER_NODES_1_NAME=node-2 IGGY_CLUSTER_NODES_1_IP=172.28.0.102 -# ... and so on for every node and every enabled transport +IGGY_CLUSTER_NODES_1_REPLICA_ID=1 +IGGY_CLUSTER_NODES_1_PORTS_TCP=8090 +IGGY_CLUSTER_NODES_1_PORTS_QUIC=8080 +IGGY_CLUSTER_NODES_1_PORTS_HTTP=3000 +IGGY_CLUSTER_NODES_1_PORTS_WEBSOCKET=8093 +IGGY_CLUSTER_NODES_1_PORTS_TCP_REPLICA=9090 +IGGY_TCP_ADDRESS=0.0.0.0:8090 +IGGY_QUIC_ADDRESS=0.0.0.0:8080 +IGGY_HTTP_ADDRESS=0.0.0.0:3000 +IGGY_WEBSOCKET_ADDRESS=0.0.0.0:8093 IGGY_CLUSTER_AUTH_ENABLED=true IGGY_CLUSTER_AUTH_SHARED_SECRET=replace-with-at-least-32-random-bytes IGGY_ROOT_USERNAME=iggy @@ -155,7 +164,7 @@ The response lists the cluster name, nodes, client endpoints, and current roles. ## Run the cluster test suites -The cross-SDK BDD suites run against a real cluster started from `bdd/docker-compose.cluster.yml`: +The cross-SDK BDD suites use a standalone server for basic features and add the two-node cluster from `bdd/docker-compose.cluster.yml` for `leader_redirection` or `all`: ```bash ./scripts/run-bdd-tests.sh rust @@ -163,4 +172,4 @@ The cross-SDK BDD suites run against a real cluster started from `bdd/docker-com ./scripts/run-bdd-tests.sh rust leader_redirection ``` -The first argument selects the SDK (`rust`, `python`, `php`, `go`, `go-race`, `node`, `csharp`, `java`, `cpp`, or `all`) and the optional second argument selects one feature (`basic_messaging`, `leader_redirection`, `raw_command`, or `all`). `leader_redirection` is the clustered feature. The Rust, Go, C#, and Java suites support it. +The first argument selects the SDK (`rust`, `python`, `php`, `go`, `go-race`, `node`, `csharp`, `java`, `cpp`, or `all`) and the optional second argument selects one feature (`basic_messaging`, `leader_redirection`, `raw_command`, `stream_crud`, or `all`). `leader_redirection` is the clustered feature. The Rust, Go, C#, and Java suites support it. diff --git a/content/docs/clustering/durability.mdx b/content/docs/clustering/durability.mdx index ec1c261e..479df2ed 100644 --- a/content/docs/clustering/durability.mdx +++ b/content/docs/clustering/durability.mdx @@ -39,7 +39,7 @@ Iggy's replication quorum is not a strict majority for every group size: Except for two replicas, the replication quorum is `min(ceil(n / 2), 3)` and the view-change quorum is `n - replication_quorum + 1`. Two replicas require both for either quorum. The quorums therefore intersect. These are -the [implemented quorum rules](https://github.com/apache/iggy/blob/97f7b0c0335f81691bef34f923f21d783016a6a4/core/consensus/src/impls.rs), +the [implemented quorum rules](https://github.com/apache/iggy/blob/4a38798a506c9231afdfc83a34ebed394d41dac2/core/consensus/src/impls.rs), not a configurable acknowledgement count. ## Replicated completion @@ -63,15 +63,17 @@ Replication count alone does not establish a bound on that loss. ## Persisted completion When either policy is `persisted`, each multi-replica partition uses a -bounded on-disk prepare WAL. The WAL records full prepares, including message -payloads. A prepare requiring persistence cannot release its `PrepareOk` -until its history and durable frontier are recoverable. +bounded on-disk prepare WAL. Message prepares contain headers and segment +references; their payloads live in segment files retained by hard links until +the WAL history can be reclaimed. Other operations are stored inline. A prepare +requiring persistence cannot release its `PrepareOk` until its message bodies, +WAL history, and durable frontier are recoverable. Prepares can be forwarded while local persistence is pending. Once enough replicas have met the required barrier, the operation can commit and the -primary can apply it and reply. An acknowledged message may still be in -the prepare WAL rather than in a segment file; its recovery does not depend -on first reaching a segment flush threshold. +primary can apply it and reply. An acknowledged message can be recovered +through the WAL before ordinary segment materialization and index updates +finish; recovery does not depend on first reaching a segment flush threshold. The WAL also retains predecessors across message and offset operations. Consequently: @@ -79,7 +81,7 @@ Consequently: - `durability=persisted` does not make an offset response persisted when `consumer_offset_durability=replicated`. - `consumer_offset_durability=persisted` with replicated messages still - journals message payloads. A durable offset's predecessor history must + retains message payloads through WAL segment references. A durable offset's predecessor history must remain recoverable. - A shared barrier can also persist co-batched operations with the weaker policy. That incidental persistence does not strengthen what their earlier @@ -89,8 +91,9 @@ Consequently: WAL history is reclaimed only after the materialized segment and offset state needed to replace it has been synchronized. The server's -`[partition] wal_bytes_max` setting, default `256 MiB`, bounds active WAL -and queued/in-flight prepare bytes per partition. Capacity pressure causes +`[partition] wal_bytes_max` setting, default `256 MiB`, bounds retained +and queued/in-flight prepare bytes per partition. The accounting includes +message bodies even when the WAL stores only references to them. Capacity pressure causes checkpointing and backpressure; it does not downgrade a persisted operation. Temporary rewrites need additional disk space. @@ -130,8 +133,8 @@ Neither policy turns HTTP `ack=none` or a poll's auto-commit into an awaited durable result. See [Which responses prove completion](/docs/server/durability#which-responses-prove-completion). The storage mechanisms are implemented in the -[partition persistence worker](https://github.com/apache/iggy/blob/97f7b0c0335f81691bef34f923f21d783016a6a4/core/partitions/src/persistence.rs) -and [prepare journal](https://github.com/apache/iggy/blob/97f7b0c0335f81691bef34f923f21d783016a6a4/core/journal/src/partition_journal.rs). -The [crash-recovery tests](https://github.com/apache/iggy/blob/97f7b0c0335f81691bef34f923f21d783016a6a4/core/integration/tests/cluster/crash_durability.rs) +[partition persistence worker](https://github.com/apache/iggy/blob/4a38798a506c9231afdfc83a34ebed394d41dac2/core/partitions/src/persistence.rs) +and [prepare journal](https://github.com/apache/iggy/blob/4a38798a506c9231afdfc83a34ebed394d41dac2/core/journal/src/partition_journal.rs). +The [crash-recovery tests](https://github.com/apache/iggy/blob/4a38798a506c9231afdfc83a34ebed394d41dac2/core/integration/tests/cluster/crash_durability.rs) exercise acknowledged persisted messages and offsets below ordinary flush thresholds, on both a singleton and a three-replica cluster. diff --git a/content/docs/clustering/security.mdx b/content/docs/clustering/security.mdx index 7391a382..d66882e8 100644 --- a/content/docs/clustering/security.mdx +++ b/content/docs/clustering/security.mdx @@ -62,6 +62,8 @@ Follower nodes forward control-plane HTTP requests (stream, topic, user, and sim - explicitly configured `http.jwt` signing secrets, identical on every node, or - the cluster PSK: when HTTP is enabled, cluster auth is enabled, and no `http.jwt` secrets are configured, the signing key is derived from `shared_secret`. -Either way, a bearer token minted on any node verifies on every node, which is the invariant the forwarding depends on. Forwarded requests dial the primary at its roster `ip` and `ports.http`. The follower verifies the caller's bearer locally before forwarding, and the primary re-authenticates the request through its normal stack. +Either way, matching JWT key material lets a bearer token minted on one node verify on another. If JWT keys are derived from the PSK, rotating the PSK also changes the JWT key: `previous_shared_secret` applies only to replica authentication. Use independently configured, shared JWT keys when HTTP tokens must remain valid across PSK rotation. Forwarded requests dial the primary at its roster `ip` and `ports.http`. The follower verifies the caller's bearer locally before forwarding, and th [...] -The forwarding covers the control plane only. Partition-plane writes over HTTP (producing messages, storing consumer offsets) are **not forwarded**: each partition is its own consensus group whose primary can diverge from the metadata primary, so those requests must land on the right node. See [Client failover](/docs/clustering/client-failover) for how clients find it. +Acknowledged partition-plane writes over HTTP (producing messages, storing or deleting consumer offsets) first run on the contacted node. If it returns `TransientNotAccepted`, the server tries the other configured HTTP nodes at most once each, within a bounded retry window. That response proves the operation was never admitted. Ambiguous outcomes are returned without replay, and `ack=none` does not provide an acknowledgement for this fallback. Each partition is its own consensus group, s [...] + +When HTTP TLS is enabled, forwarding uses HTTPS and pins the destination certificate to the forwarding node's own HTTP leaf certificate. All nodes therefore need the same HTTP certificate for forwarding; distinct per-node HTTP certificates fail verification. Replica TLS is configured separately and does not encrypt the HTTP forwarding hop. diff --git a/content/docs/clustering/vsr.mdx b/content/docs/clustering/vsr.mdx index bfc89051..24ec8359 100644 --- a/content/docs/clustering/vsr.mdx +++ b/content/docs/clustering/vsr.mdx @@ -56,23 +56,23 @@ VSR uses three main flows: 2. **View change**: replicas exchange `StartViewChange` and `DoViewChange`, then the new primary sends `StartView`. 3. **Recovery**: a restarted or lagging replica requests the current view and repairs missing WAL ranges before serving current state. -A cluster of `2f + 1` replicas tolerates `f` unavailable replicas. Use **at least three replicas** for one-node fault tolerance. A two-node cluster is useful for development, but it **cannot make progress** after either node fails. +Three replicas tolerate one unavailable replica; five tolerate two. Larger groups use a capped replication quorum and a growing view-change quorum, so the usual `2f + 1` rule does not apply to every group size. See [Cluster durability](/docs/clustering/durability#what-an-acknowledgement-means) for the quorum rules. Use **at least three replicas** for one-node fault tolerance. A two-node cluster is useful for development, but it **cannot make progress** after either node fails. ## Wire protocol and SDKs -VSR framing is the *only* wire protocol. Every SDK (Rust, Go, Java, C#, C++, Node.js, Python) speaks it, and every BDD suite runs against the clustered-capable `iggy-server`. Clients built before the VSR migration **cannot talk to current servers**. +VSR framing is the *only binary* wire protocol, used by TCP, QUIC, and WebSocket connections. HTTP uses the REST API. Every SDK (Rust, Go, Java, C#, C++, Node.js, Python) speaks it, and every BDD suite runs against the clustered-capable `iggy-server`. Binary clients built before the VSR migration **cannot talk to current servers**. The Rust SDK needs no feature flags. Depend on the published crate: ```toml [dependencies] -iggy = "0.11.0-edge.4" +iggy = "0.11.0-edge.7" tokio = { version = "1", features = ["full"] } ``` or track the repository directly with `iggy = { git = "https://github.com/apache/iggy" }`. -The client API is the same in single-node and cluster mode: +The client API is the same in single-node and cluster mode. This example uses the `iggy` / `iggy` credentials explicitly provisioned in [Getting started](/docs/introduction/getting-started): ```rust use iggy::prelude::*;
