This is an automated email from the ASF dual-hosted git repository. spetz pushed a commit to branch iggy_0.9.0 in repository https://gitbox.apache.org/repos/asf/iggy-website.git
commit 5cdc88e721db24dbb50d2f698bd6088483072ab2 Author: spetz <[email protected]> AuthorDate: Sat Sep 19 21:53:40 2026 +0200 blog: add post about Iggy 0.9.0 release --- content/blog/release-0.9.0.mdx | 464 +++++++++++++++++++++++++++++++++++++++++ scripts/diagram-text.mjs | 2 + src/components/bench-0-9-0.tsx | 401 +++++++++++++++++++++++++++++++++++ src/mdx-components.tsx | 2 + 4 files changed, 869 insertions(+) diff --git a/content/blog/release-0.9.0.mdx b/content/blog/release-0.9.0.mdx new file mode 100644 index 000000000..9783541c6 --- /dev/null +++ b/content/blog/release-0.9.0.mdx @@ -0,0 +1,464 @@ +--- +title: Apache Iggy 0.9.0 Release +author: piotr +tags: ["release", "0.9.0", "announcement", "clustering", "vsr", "benchmarks"] +date: 2026-09-21 +--- +## Release 0.9.0 + +Five months after [0.8.0](https://iggy.apache.org/blogs/2026/04/22/release-0.8.0/), we're proud to release **[Apache Iggy 0.9.0](https://github.com/apache/iggy/releases/tag/server-0.9.0)**. It is our first release as an **Apache Top-Level Project**, following the [graduation from the Incubator](https://iggy.apache.org/blogs/2026/08/24/apache-iggy-top-level-project-tlp-graduation/) on August 19, 2026, and the first one without the `-incubating` suffix. It is also the release we have been [...] + +**Iggy now runs as a cluster.** 0.9.0 ships **Viewstamped Replication Revisited (VSR)** consensus in the standard `iggy-server` binary. Start three nodes, lose one, and the cluster keeps serving reads and writes. There is no separate build, feature flag, or clustered edition. The same server runs single-node or replicated, and `cluster.enabled` in the configuration decides which one you get. + +This is the biggest release in the project's history: **over 600 merged pull requests from 60 contributors, 34 of them contributing to Iggy for the first time.** Alongside the server, we're releasing the Rust SDK 0.11.0, the CLI 0.14.0, the connectors runtime 0.5.0, the MCP server 0.5.0, the Web UI 0.4.0, the bench dashboard 0.8.0, and the Python 0.9.0, Node.js 0.10.0, Java 0.9.0, C# 0.9.0 and Go 0.9.0 SDKs, all from the same repository. Beyond clustering, 0.9.0 brings **per-topic durabi [...] + +Before the changelog, two topics deserve their own space: **clustering** and **performance**. + +--- + +## Clustering with VSR consensus + +For three releases the roadmap section of these posts has ended the same way: complete the clustering implementation with VSR consensus. 0.7.0 laid the consensus foundations. 0.8.0 added the shard crate, the plane abstractions, the persistent WAL journal, and the experimental `iggy-server-ng` binary. In 0.9.0 that work became the server. **[server-ng was promoted to the Apache Iggy server](https://github.com/apache/iggy/pull/3856)**, the `vsr` feature flag is gone, the legacy server and [...] + +### What a cluster gives you + +Apache Iggy replicates with **[Viewstamped Replication Revisited](https://github.com/apache/iggy/blob/master/assets/vsr.pdf)**. VSR keeps an ordered state machine consistent across replicas and elects a new primary when the current one fails: + +- **Normal operation**: the primary assigns an operation number, sends `Prepare`, and commits after a quorum answers `PrepareOk`. +- **View change**: when replicas stop hearing from the primary, they exchange `StartViewChange` and `DoViewChange`, and the new primary sends `StartView`. Failover needs no operator. +- **Recovery**: a restarted or lagging replica probes the current view and repairs the missing WAL ranges from its peers before it serves current state. + +**Three replicas tolerate one unavailable replica, five tolerate two.** Three nodes is the recommended minimum for a highly available deployment. A two-node cluster is useful for development, but it cannot make progress after either node fails. + +Replication is split by namespace rather than run as one global log: + +| Plane | Consensus group | Replicated state | +| --- | --- | --- | +| Metadata | One group on shard 0 | Streams, topics, users, permissions, consumer groups, access tokens | +| Partition | **One group per partition** | Messages and consumer offsets | + +Each group runs its own consensus and elects its own primary. Metadata writes route through the metadata primary, and partition writes route through the primary of that partition. Reads are served from the local replicated state on any node, followers included. + +### How it was built + +The consensus work spans well over a hundred pull requests. The milestones, in roughly the order they landed: + +- **[Consensus per partition](https://github.com/apache/iggy/pull/3071)** and the **[consensus group mechanism](https://github.com/apache/iggy/pull/3138)** made every partition its own replicated state machine +- **[Session registration with combined login and register](https://github.com/apache/iggy/pull/3108)**, later **[reworked so sessions resume across restarts](https://github.com/apache/iggy/pull/3753)**, backed by the client table for at-most-once request handling +- **[Async fire-and-forget message bus](https://github.com/apache/iggy/pull/3134)** for consensus traffic, plus **[QUIC, TCP-TLS, WebSocket and WSS transports](https://github.com/apache/iggy/pull/3192)** for clients +- **[Replica bootstrap](https://github.com/apache/iggy/pull/3163)** and **[multi-shard cross-shard communication](https://github.com/apache/iggy/pull/3269)**, so all shards of a node take part in consensus +- **[Partition reconciliation loop](https://github.com/apache/iggy/pull/3436)**, **[partition background tasks](https://github.com/apache/iggy/pull/3564)**, and **[replica rejoin via view probe and message repair](https://github.com/apache/iggy/pull/3667)** +- **[Metadata VSR state persisted in a durable superblock](https://github.com/apache/iggy/pull/3767)**, **[state transfer for the metadata snapshot and client table](https://github.com/apache/iggy/pull/3765)**, and **[state transfer for partitions](https://github.com/apache/iggy/pull/3808)**, which let a wiped replica refill itself from its peers +- **[Committed sends confirmed with partition and offset](https://github.com/apache/iggy/pull/3781)**, so producers learn exactly where their batch landed +- **[Deduplication of partition writes with per-group client table slices](https://github.com/apache/iggy/pull/3959)**, so a produce retried after a timeout does not write twice +- **[Follower HTTP requests forwarded to the primary](https://github.com/apache/iggy/pull/3744)**, so any node can answer any control-plane request +- **[Inbox split into consensus and client-reply lanes](https://github.com/apache/iggy/pull/3915)**, so a burst of client traffic cannot starve replication +- **[Metadata reads served at or above the client's own writes](https://github.com/apache/iggy/pull/4024)** + +Then came the correctness fixes found by the simulator, the crash tests and real cluster runs: **[view change no longer discards committed ops](https://github.com/apache/iggy/pull/3824)**, **[a replica is kept off a hole in its committed prefix](https://github.com/apache/iggy/pull/4073)**, **[repaired prepares can't rewind the WAL parent](https://github.com/apache/iggy/pull/3978)**, **[the dedup fence survives capacity eviction](https://github.com/apache/iggy/pull/3934)**, **[writes reco [...] + +### Durability you choose per topic + +Replication forced a precise answer to a simple question: what does an acknowledgement mean? 0.9.0 answers it with two create-only topic options, introduced in **[let topics require durable acks](https://github.com/apache/iggy/pull/4092)**: + +| Policy | Required before success | +| --- | --- | +| `replicated` (default) | VSR quorum commit and local application, without an additional stable-storage barrier | +| `persisted` | VSR quorum commit backed by recoverable stable-storage copies at the required quorum | + +`durability` governs message production and `consumer_offset_durability` governs explicit offset stores and deletes. They default independently and neither inherits the other. Both policies write data to disk. With `persisted`, each multi-replica partition keeps a bounded on-disk prepare WAL, sized by `partition.wal_bytes_max` (256 MiB by default), and a replica cannot release its `PrepareOk` until its message bodies, WAL history and durable frontier are recoverable. The old `enforce_fsy [...] + +The details are in [Durability](/docs/server/durability) and [Cluster Durability](/docs/clustering/durability). + +### Secure replica traffic + +The replica-to-replica port is authenticated and, optionally, encrypted: + +- **[PSK + BLAKE3 handshake](https://github.com/apache/iggy/pull/3425)**: every peer proves possession of a cluster-wide pre-shared key of at least 32 bytes through a three-message keyed-MAC handshake before it can register as a replica. A `previous_shared_secret` enables rolling key rotation without an authentication outage. +- **[TLS 1.3 on the replica plane with PSK channel binding](https://github.com/apache/iggy/pull/3461)**: the opt-in `[cluster.tls]` table wraps replica connections in TLS 1.3 (ALPN `iggy-replica`) with CA and self-signed modes. The PSK MAC absorbs the TLS exporter value, so a relay that terminates both legs fails the handshake. +- **[Cluster metadata is auth-gated and registration is forwarded to the primary](https://github.com/apache/iggy/pull/3872)**: unauthenticated clients can no longer read the roster, and a client that dials a backup completes its login there. Credentials never cross the replica interconnect. +- **[Incompatible clients are rejected at login via protocol version](https://github.com/apache/iggy/pull/3454)** + +See [Cluster Security](/docs/clustering/security). + +### Client failover + +Every SDK speaks the VSR wire protocol and can connect to any node. Leader-aware clients fetch cluster metadata after login, reconnect to the advertised leader when they landed on a follower, poll for a new leader through an election, and cap the number of redirects so a flapping roster cannot bounce them forever: + +- Rust: **[fail over to a surviving node when the current one dies](https://github.com/apache/iggy/pull/3944)**, **[rejoin consumer groups after membership loss](https://github.com/apache/iggy/pull/3703)**, **[heartbeat handle](https://github.com/apache/iggy/pull/3852)** +- Java: **[cluster metadata and leader redirection in the TCP client](https://github.com/apache/iggy/pull/3745)** +- C#: **[leader redirection BDD scenario](https://github.com/apache/iggy/pull/3193)** +- Rust, Go, C# and Java run the shared `leader_redirection` BDD suite against a real cluster + +Writes return after the required VSR commit, or return a **retryable** error while a replica is changing view or catching up. `TransientNotAccepted` proves the request was never admitted and permits a retry on another node. `TransientNotCommitted` has an uncertain outcome and is replayed with the same session and request identity, and the server-side dedup takes care of the rest. Details in [Client Failover](/docs/clustering/client-failover). + +Per-node **[advertised addresses](https://github.com/apache/iggy/pull/3757)** and **[per-client-network address selectors](https://github.com/apache/iggy/pull/3818)** let a node publish different endpoints to clients on different networks, which matters for containers and cloud deployments. + +### Running a cluster + +Every node loads the same configuration and is told apart only by `--replica-id`: + +```toml +[cluster] +enabled = true +name = "iggy-vsr-dev" + +[cluster.auth] +enabled = true +shared_secret = "" + +[[cluster.nodes]] +name = "iggy-node-1" +ip = "127.0.0.1" +replica_id = 0 +ports = { tcp = 8090, quic = 8080, http = 3000, websocket = 8093, 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 = 8094, tcp_replica = 9091 } + +[[cluster.nodes]] +name = "iggy-node-3" +ip = "127.0.0.1" +replica_id = 2 +ports = { tcp = 8092, quic = 8082, http = 3002, websocket = 8095, tcp_replica = 9092 } +``` + +```bash +IGGY_PATH=local_data/node-0 iggy-server --replica-id 0 +IGGY_PATH=local_data/node-1 iggy-server --replica-id 1 +IGGY_PATH=local_data/node-2 iggy-server --replica-id 2 +``` + +Every roster field can also be set through typed `IGGY_CLUSTER_*` environment variables, which is what the new **[Helm chart cluster mode](https://github.com/apache/iggy/pull/4035)** renders. The **[Deploy a Cluster](/docs/clustering/deploy)** guide walks through the whole setup, and the **[Configuration](/docs/clustering/configuration)** reference covers every `[cluster]` key, including consensus timing (`heartbeat_timeout`, view change and repair intervals, all on a fixed 10ms tick). + +### Testing a consensus protocol + +We trust this implementation because of how much effort went into breaking it. Most of that effort is deterministic simulation: + +- **[Seed-based workload fuzz harness](https://github.com/apache/iggy/pull/3272)** and the **[workload fuzzer with consensus oracles](https://github.com/apache/iggy/pull/3591)**: randomized, fully seed-deterministic workloads run against a predictive shadow, crash replicas, and assert consensus invariants on every tick and at quiesce. Every failure reproduces from its seed. +- **[Deterministic future executor and server dispatch shell](https://github.com/apache/iggy/pull/3671)**: the real server dispatch code runs inside the simulator +- **[Workloads under network and replica faults](https://github.com/apache/iggy/pull/3942)**: message loss, delays, partitions, crashes with stability windows, client resends with target rotation. This one alone found two real bugs before any user could. +- **[Durability failures no existing test could reach](https://github.com/apache/iggy/pull/4132)**: a storage model injects crashes, power loss and torn writes into the WAL tests +- **[SIGKILL crash-durability cluster tests](https://github.com/apache/iggy/pull/3905)** and **[client table durability across node restart](https://github.com/apache/iggy/pull/3736)** on real processes +- **[On-disk storage format breaks caught pre-merge](https://github.com/apache/iggy/pull/4003)**: a baseline data directory boots against every pull request +- **[Miri undefined behavior detection](https://github.com/apache/iggy/pull/3284)** for the `binary_protocol` and `consensus` crates +- Cross-SDK BDD suites running against a real two-node cluster + +The [Viewstamped Replication](/docs/clustering/vsr) page is the entry point to all of this. + +--- + +## Performance + +Efficiency has always been the point of Iggy: thread-per-core, shared-nothing, `io_uring`, and no copies where the format allows it. 0.9.0 carried that into the replicated server and shipped a series of hot-path improvements: + +- **[Poll replies served without copying record bytes](https://github.com/apache/iggy/pull/4025)**: a poll reply used to be three copies plus a memset, roughly four times the payload in memory traffic. It is now a 272-byte head fragment followed by the journal or disk fragments as they are, written with a single `writev`. +- **[Persisted-mode disk traffic per acknowledgment cut](https://github.com/apache/iggy/pull/4146)**: publishing the WAL frontier took three barriers on three inodes per batch. It is now two slots in one file, overwritten in place, and the body and WAL barriers land in one journal window. +- **[Log and index persisted concurrently](https://github.com/apache/iggy/pull/3970)**: the two serialized `fdatasync` calls per flush became one `join`. On the fsync-gated benchmark used in that pull request, throughput went from 13.4k to 26.1k msg/s and p50 from 5.83 to 3.01 ms. +- **[Batch checksum computed in a single produce pass](https://github.com/apache/iggy/pull/3746)**, **[user headers decoded without copying the blob](https://github.com/apache/iggy/pull/3708)**, **[message header written directly into the target buffer](https://github.com/apache/iggy/pull/3707)** +- **[Sealed-segment read handles capped with a per-partition LRU](https://github.com/apache/iggy/pull/3806)**, and the active segment no longer pays one `openat` per poll +- **[16 MiB per active segment index no longer reserved](https://github.com/apache/iggy/pull/3974)**, **[consumer offset state bounded per partition](https://github.com/apache/iggy/pull/4063)** +- **[Unnecessary backpressure under concurrent clients eliminated](https://github.com/apache/iggy/pull/3692)**, **[produce and poll overhead reduced in cluster mode](https://github.com/apache/iggy/pull/4169)** +- **[TCP-TLS and WSS connections distributed across shards](https://github.com/apache/iggy/pull/4193)** +- **[mimalloc restored as the global allocator](https://github.com/apache/iggy/pull/3971)** +- **[CPU affinity and cgroup limits respected](https://github.com/apache/iggy/pull/3615)** in shards and stats, so a container gets the shard count it was given +- Foreign SDKs **[generate message ids from random bytes instead of UUIDs](https://github.com/apache/iggy/pull/4076)**, and the Go SDK **[pools its request-path wire payload](https://github.com/apache/iggy/pull/3442)** + +### Benchmarks: 0.9.0 and what comes next + +We're not done. A dedicated performance effort started right after the 0.9.0 code freeze, and the first results are in. The tables below compare **the 0.9.0 release against the current development branch**, both measured with `iggy-bench` on the same host with identical workload parameters. Each cell reads `0.9.0 -> next`. Latencies are in milliseconds. + +A short legend. *Pinned producer* and *pinned consumer* are the `iggy-bench` kinds where every actor owns its own stream and partition. *Actors* is the number of concurrent producers or consumers. Rate-limited rows hold the aggregate throughput at 800 MB/s or 500 MB/s, so the latency columns compare like with like. *No rate limit* rows push as hard as the client can. *Persisted* rows use topics created with `durability=persisted`, so every acknowledgement waits for stable storage at the [...] + +<Bench090LatencyChart /> + +**Single node** + +| Workload | MB/s | p50 ms | p99 ms | p999 ms | +| --- | --- | --- | --- | --- | +| pinned producer, 20 actors, 800 MB/s | 800.1 -> 800.1 | 0.65 -> 0.588 | 1.313 -> 1.145 | 11.087 -> 2.956 | +| pinned producer, 20 actors, persisted, 800 MB/s | 800.0 -> 800.1 | 1.232 -> 1.098 | 1.601 -> 1.424 | 2.249 -> 1.967 | +| pinned consumer, 20 actors, cold, 800 MB/s | 800.1 -> 800.1 | 0.498 -> 0.447 | 4.698 -> 3.313 | 5.156 -> 3.777 | +| pinned consumer, 20 actors, warm, 800 MB/s | 800.1 -> 800.1 | 0.521 -> 0.544 | 1.022 -> 0.923 | 1.573 -> 3.92 | +| pinned producer, 1 actor, 500 MB/s | 500 -> 500 | 0.347 -> 0.352 | 0.679 -> 0.683 | 1.356 -> 1.046 | +| pinned consumer, 1 actor, warm, 500 MB/s | 500 -> 500 | 0.272 -> 0.296 | 0.468 -> 0.478 | 0.491 -> 0.502 | +| pinned producer, 20 actors, no rate limit | 2062.1 -> 2096.5 | 1.086 -> 0.792 | 26.597 -> 46.431 | 40.869 -> 78.082 | +| pinned consumer, 20 actors, no rate limit | 2713.4 -> 2532.5 | 0.679 -> 0.619 | 6.151 -> 5.869 | 6.476 -> 23.418 | +| pinned producer, 1 actor, persisted, 500 MB/s | 372.1 -> 335.8 | 0.665 -> 0.733 | 0.825 -> 0.901 | 0.995 -> 1.151 | + +**3-node cluster** + +| Workload | MB/s | p50 ms | p99 ms | p999 ms | +| --- | --- | --- | --- | --- | +| pinned producer, 20 actors, persisted, 800 MB/s | 800.0 -> 800.0 | 2.905 -> 2.243 | 7.073 -> 2.938 | 12.403 -> 3.889 | +| pinned producer, 1 actor, persisted, 500 MB/s | 234.4 -> 233.6 | 1.047 -> 1.062 | 1.2 -> 1.223 | 10.857 -> 1.759 | +| pinned consumer, 20 actors, cold, 800 MB/s | 800.1 -> 800.0 | 0.477 -> 0.436 | 3.831 -> 2.346 | 4.241 -> 2.675 | +| pinned consumer, 20 actors, warm, 800 MB/s | 800.1 -> 797.9 | 0.484 -> 0.451 | 0.933 -> 0.812 | 1.233 -> 1.216 | +| pinned producer, 20 actors, 800 MB/s | 800.0 -> 800.0 | 1.782 -> 1.679 | 3.186 -> 3.25 | 3.683 -> 3.66 | +| pinned producer, 20 actors, no rate limit | 1134.5 -> 1132.5 | 4.366 -> 4.357 | 5.159 -> 5.296 | 6.299 -> 5.929 | +| pinned consumer, 20 actors, no rate limit | 2710.1 -> 2594.3 | 0.682 -> 0.657 | 6.12 -> 5.758 | 6.445 -> 23.362 | +| pinned producer, 1 actor, 500 MB/s | 337.2 -> 325.4 | 0.716 -> 0.735 | 0.973 -> 1.016 | 1.057 -> 1.13 | +| pinned consumer, 1 actor, warm, 500 MB/s | 500.0 -> 500.3 | 0.297 -> 0.326 | 0.493 -> 0.536 | 0.533 -> 0.574 | + +The tails are where the work shows. On a three-node cluster with `persisted` topics, the **p99 of a 20-producer workload dropped from 7.07 ms to 2.94 ms and the p999 from 12.4 ms to 3.9 ms**. The single-producer p999 on the same cluster went from 10.9 ms to 1.8 ms. On a single node, the 20-producer **p999 fell from 11.1 ms to 3.0 ms**. Cold consumer reads improved on both setups. A replicated, persisted write on a three-node cluster now completes in about 2.2 ms at the median while the c [...] + +Not every row moved in the right direction. The unthrottled rows show higher tails on the development branch, and a few single-actor rows are slightly slower. **These numbers are a snapshot of work in progress, not a final result.** The branch is being profiled and tuned as we write this, and the next version will carry further changes aimed at both the throughput and the tail columns. We're publishing the snapshot anyway, because we'd rather show where the work stands than wait for a pe [...] + +What is in the pipeline, in the order we expect it to land: + +- **[Deferred polling](https://github.com/apache/iggy/issues/3470)**: a poll carries a wait timeout, and when a partition has nothing new the server holds the request and completes it as soon as messages arrive or the timeout expires, instead of answering empty and making the consumer ask again. Fewer wasted round trips, less CPU churn on both sides, lower end-to-end latency. +- **More concurrency in I/O**: one task per read and per write, so disk and network operations overlap instead of waiting on each other within a shard. The first piece is already in review: **[read-ahead on replica sockets](https://github.com/apache/iggy/pull/4224)**, so one socket read serves a whole burst of replication frames instead of two reads per frame. +- **Explicit group commit**: several acknowledgements share one disk barrier by design, rather than by whatever happened to queue up during the previous one +- **Direct I/O** for the storage engine, writing past the page cache instead of paying for `fsync` barriers +- **Multi-leader clusters**, further out: a leader per partition, with leaders spread across all nodes of the cluster, so write traffic and replication work are shared by every machine instead of concentrating on one + +And a long tail of smaller changes in the same direction. Efficiency is the reason Iggy exists, and it stays the priority now that clustering is in. + +A like-for-like comparison of 0.8.0 against 0.9.0 with this parameter set is planned for the [benchmarking platform](https://benchmarks.iggy.apache.org), together with the final numbers for the next release. The [benchmarking guide](/docs/server/benchmarking) has the commands to reproduce these workloads on your own hardware, and the [Linux tuning guide](/docs/server/linux-tuning) covers the host preparation. + +--- + +## Iggy Server + +### One server, one wire protocol + +- **[server-ng promoted to the Apache Iggy server](https://github.com/apache/iggy/pull/3856)**: the thread-per-core, `io_uring` server is now the only server. The `iggy-server` binary, the `server` package, the configs tree and the systemd integration are the former server-ng, and all transports are kept: TCP with TLS, QUIC, WebSocket and HTTP. The legacy server, its compat lane and the classic TCP, QUIC and WebSocket framing were deleted (**BREAKING**). +- **[Legacy message wire format and v2 bridge APIs removed](https://github.com/apache/iggy/pull/3904)** from the SDK. The new message wire format is encoded on the client side (**BREAKING**). +- **[Namespace removed from client headers](https://github.com/apache/iggy/pull/3836)**, a smaller header on every request +- **[Server-only types extracted into `server_common`](https://github.com/apache/iggy/pull/3298)**, **[shard allocator extracted into `cpu_allocation`](https://github.com/apache/iggy/pull/3578)**, **[compio executor and io_uring diagnostics extracted into a shared crate](https://github.com/apache/iggy/pull/3331)**, the server's **[module graph turned into an enforced DAG](https://github.com/apache/iggy/pull/4023)**, and the dispatch spine **[split into plane-named leaves](https://github. [...] + +### Topic options + +- **[Extensible key-value options for topics, streams and users](https://github.com/apache/iggy/pull/3880)** (**BREAKING**): retention, durability and segment layout are per-topic decisions made at creation. `segment_size`, `messages_required_to_save`, `size_of_messages_required_to_save` and the new `preallocate_segments` moved from server-wide `[system.*]` keys to the topic. Unknown keys are rejected at the edge, never silently ignored, and `iggy options topic` (or `GET /options/topic`) [...] +- **[Durability policies](https://github.com/apache/iggy/pull/4092)** as described above (**BREAKING**: `enforce_fsync` is rejected, `[system]` is flattened into the root, and `IGGY_SYSTEM_*` variables lose the `SYSTEM_` segment) + +### Storage and recovery + +- **[Log and index persisted concurrently, recovery hardened](https://github.com/apache/iggy/pull/3970)**: an index is derived from the log and is never evidence about the log. Recovery drops a divergent index whole and rebuilds it from a byte-0 walk, checksumming every batch it accepts. A local persist failure now stops the whole server instead of silently killing one shard. +- **[Torn segment tails truncated instead of resurrected](https://github.com/apache/iggy/pull/3946)**, **[WAL suffix truncated above the state-transfer floor](https://github.com/apache/iggy/pull/3918)**, **[recovery slot collisions rejected](https://github.com/apache/iggy/pull/4009)** +- **[Offsets reserved before they are confirmed](https://github.com/apache/iggy/pull/3975)**, **[polls completed on the owning shard](https://github.com/apache/iggy/pull/4119)**, **[producer batches preserved across replication](https://github.com/apache/iggy/pull/3859)** +- **[Data persisted on VSR backups, with a data integrity test](https://github.com/apache/iggy/pull/3512)** +- **[Deleted partitions and topics rolled out of parent stats](https://github.com/apache/iggy/pull/4046)**, **[namespace caps enforced at admission](https://github.com/apache/iggy/pull/3907)** +- **[Expired personal access token cleaner](https://github.com/apache/iggy/pull/3448)** + +### HTTP, security and operations + +- **[Cluster-correct HTTP listener with per-operation RBAC](https://github.com/apache/iggy/pull/3622)**, **[HTTPS on the REST listener](https://github.com/apache/iggy/pull/3672)**, **[CORS honored](https://github.com/apache/iggy/pull/3646)**, **[Web UI served by the server](https://github.com/apache/iggy/pull/3676)** +- **[PAT limits enforced and Prometheus metrics exposed](https://github.com/apache/iggy/pull/3773)** +- **[Wildcard bind without an advertised address rejected](https://github.com/apache/iggy/pull/3923)** (**BREAKING**): a `0.0.0.0` bind says where a node listens, not where a client can reach it. Deployments that bind a wildcard must set `node.advertised_address`. The Helm chart derives it from the Service DNS name. +- **[Trusted-issuer A2A JWT verification with SDK token refresh](https://github.com/apache/iggy/pull/3626)**, **[message encryption in the replicated server](https://github.com/apache/iggy/pull/3644)** +- **[`read_servers` permission required for the system snapshot](https://github.com/apache/iggy/pull/3579)**, **[`get_user` self-read restored without `read_users`](https://github.com/apache/iggy/pull/3897)** +- **[systemd watchdog integration](https://github.com/apache/iggy/pull/3233)** for the server and the MCP server +- **[Listener, auth, shutdown and disk-poll paths hardened](https://github.com/apache/iggy/pull/3774)**, **[configuration surface hardened](https://github.com/apache/iggy/pull/3756)**, and the **[server config cleaned up after a full field audit](https://github.com/apache/iggy/pull/3895)** +- **[Sibling `IGGY` environment variables allowed](https://github.com/apache/iggy/pull/4149)**, **[deterministic environment variable name suggestions](https://github.com/apache/iggy/pull/3142)** +- **[io_uring panic from an unsupported opcode diagnosed](https://github.com/apache/iggy/pull/3589)**, **[shard startup kept off blocking fallbacks](https://github.com/apache/iggy/commit/8c4986ac545c1b75afd40b11c425d35b7fbbe058)** +- **[Partitions distributed evenly in cooperative rebalancing](https://github.com/apache/iggy/pull/3374)** +- **[Non-numeric filenames in consumer offset directories skipped instead of panicking](https://github.com/apache/iggy/pull/3135)** +- **[Iggy banner on server startup](https://github.com/apache/iggy/pull/4085)** +- Rust toolchain moved to **[1.98](https://github.com/apache/iggy/pull/3962)**, with an **[MSRV of 1.95 declared and enforced in CI](https://github.com/apache/iggy/pull/3558)** + +### Breaking changes + +This is a major upgrade. Read this list before moving an existing deployment: + +- **Data directory**: the on-disk format changed, and there is no migration of 0.8.x data directories. Until 1.0.0, an upgrade means starting from a fresh data directory. +- **[Wire protocol](https://github.com/apache/iggy/pull/3856)**: VSR framing is the only binary wire protocol. 0.8.x clients cannot talk to a 0.9.0 server and vice versa. Upgrade the server and the SDKs together. +- **[Legacy message wire format and v2 bridge APIs removed](https://github.com/apache/iggy/pull/3904)** from the Rust SDK +- **[Server configuration](https://github.com/apache/iggy/pull/3856)**: the `[cluster]` and sharding sections were reworked. Review your `config.toml`. +- **[Topic options](https://github.com/apache/iggy/pull/3880)**: `segment_size`, `messages_required_to_save`, `size_of_messages_required_to_save` and related keys are per-topic. The server refuses to boot on the old `[system.*]` keys. +- **[Durability](https://github.com/apache/iggy/pull/4092)**: `enforce_fsync` and `consumer_offset_enforce_fsync` are rejected. `[system]` is flattened into the root of the config and `IGGY_SYSTEM_*` variables drop `SYSTEM_`. The HTTP `Iggy-Durability` header reports `replicated` or `persisted`. +- **[Cluster metadata requires authentication](https://github.com/apache/iggy/pull/3872)**: `PING` is the only pre-auth command +- **[Wildcard bind requires an advertised address](https://github.com/apache/iggy/pull/3923)**, and `cluster.nodes[*].ip` must be a literal IP +- **[Connector SDK `ConnectivityConfig` replaced by `RetryPolicy`](https://github.com/apache/iggy/pull/4104)** (Rust API only, built plugins are unaffected) + +Docker images are available on **[Docker Hub](https://hub.docker.com/u/apache?page=1&search=iggy)**: `apache/iggy:0.9.0` for the server and `apache/iggy-connect:0.5.0` for the connectors runtime. + +--- + +## SDKs + +Every SDK migrated to the VSR wire protocol and runs its test suite against the replicated server only. All of them expose the new durability options and a raw command API for custom commands. + +### Rust + +- **Rust client bumped to 0.11.0** +- **[VSR header framing](https://github.com/apache/iggy/pull/3254)**, the client side of the new protocol +- **[Fail over to a surviving node when the current one dies](https://github.com/apache/iggy/pull/3944)**, **[rejoin consumer groups after membership loss](https://github.com/apache/iggy/pull/3703)**, **[late offset stores no longer hit a left group](https://github.com/apache/iggy/pull/3668)** +- **[Raw requests for custom commands](https://github.com/apache/iggy/pull/3373)** and **[unknown command codes forwarded instead of rejected](https://github.com/apache/iggy/pull/3766)**, so the protocol can be extended without forking the client +- **[Consumer group polling and commits corrected in `IggyConsumer`](https://github.com/apache/iggy/pull/4034)**, **[consumer progress and error handling preserved](https://github.com/apache/iggy/pull/4151)**, **[stream terminates after consumer shutdown](https://github.com/apache/iggy/pull/3964)** +- **[`max_buffer_size` respected when merging batches](https://github.com/apache/iggy/pull/3933)**, **[error callback awaits shard flush on producer shutdown](https://github.com/apache/iggy/pull/3953)** +- **[`NonZeroIggyDuration` for retries and heartbeats](https://github.com/apache/iggy/pull/3891)** +- Documentation for **[`IggyClient`](https://github.com/apache/iggy/pull/3809)**, **[`IggyProducer`](https://github.com/apache/iggy/pull/3989)** and **[`IggyConsumer`](https://github.com/apache/iggy/pull/3913)** + +### PHP (new) + +A brand new SDK. **[The PHP SDK](https://github.com/apache/iggy/pull/3235)** is a native PHP extension built in Rust with `ext-php-rs`. It wraps the Rust SDK and exposes a synchronous `Iggy\Client` covering streams, topics, messages and consumer groups. It **[maps Iggy errors to typed exceptions](https://github.com/apache/iggy/pull/3382)**, ships a **[consumer message iterator](https://github.com/apache/iggy/pull/3463)** and **[examples](https://github.com/apache/iggy/pull/3368)**, and r [...] + +### Python + +Python received more pull requests than any other SDK in this release and closed most of its API gap with the Rust client: + +- **[High-level producer API](https://github.com/apache/iggy/pull/4156)** and **[message partitioning strategies](https://github.com/apache/iggy/pull/3927)** +- **[TCP](https://github.com/apache/iggy/pull/3776)**, **[QUIC](https://github.com/apache/iggy/pull/3991)**, **[HTTP](https://github.com/apache/iggy/pull/3992)** and **[WebSocket](https://github.com/apache/iggy/pull/4000)** transport configuration +- Stream and topic management: **[stream listing, update, delete, purge](https://github.com/apache/iggy/pull/3701)**, **[topic listing, update, delete, purge](https://github.com/apache/iggy/pull/3572)**, **[remaining Topic fields and partitions](https://github.com/apache/iggy/pull/3623)**, **[partition management](https://github.com/apache/iggy/pull/4017)** +- Consumer groups: **[create and get](https://github.com/apache/iggy/pull/3581)**, **[delete, join and leave](https://github.com/apache/iggy/pull/3607)**, **[`poll_messages` takes a consumer](https://github.com/apache/iggy/pull/3877)** +- Users: **[user management](https://github.com/apache/iggy/pull/3695)**, **[permissions and remaining auth methods](https://github.com/apache/iggy/pull/3727)**, **[`update_user` options](https://github.com/apache/iggy/pull/4173)** +- **[User headers and origin timestamp](https://github.com/apache/iggy/pull/3613)**, **[`get_stats`](https://github.com/apache/iggy/pull/4018)** +- **[ruff configured, modern coding standards](https://github.com/apache/iggy/pull/3318)**, **[pyrefly type checking](https://github.com/apache/iggy/pull/3323)**, **[Windows wheels restored](https://github.com/apache/iggy/pull/4049)** + +### Java + +- **[TCP client migrated to the VSR wire protocol](https://github.com/apache/iggy/pull/3841)** +- **[Cluster metadata and leader redirection](https://github.com/apache/iggy/pull/3745)** +- **[TCP clients run one I/O thread or share a group](https://github.com/apache/iggy/pull/4096)** +- **[Wire strings sized by UTF-8 byte length](https://github.com/apache/iggy/pull/4068)** +- Java benchmark: **[CLI and resource provisioner](https://github.com/apache/iggy/pull/3159)**, **[actor and data batch generator](https://github.com/apache/iggy/pull/3218)**, **[reporting suite](https://github.com/apache/iggy/pull/3420)** +- **[Pinot connector E2E coverage](https://github.com/apache/iggy/pull/3922)** + +### C\# + +- **[Migrated to the VSR wire protocol](https://github.com/apache/iggy/pull/3800)**, with the **[TCP connection cleaned up afterwards](https://github.com/apache/iggy/pull/3858)** +- **[Heartbeat mechanism](https://github.com/apache/iggy/pull/3894)** +- **[Rented message polling](https://github.com/apache/iggy/pull/3250)** and **[rented payloads](https://github.com/apache/iggy/pull/3471)** for fewer allocations +- **[Encryption moved to the raw client](https://github.com/apache/iggy/pull/3639)**, **[missing metrics in `StatsResponse`](https://github.com/apache/iggy/pull/3221)**, **[integer properties made unsigned](https://github.com/apache/iggy/pull/4027)** +- **[UTF-8 byte count for string validation](https://github.com/apache/iggy/pull/4072)**, **[OS default socket buffer size](https://github.com/apache/iggy/pull/4013)** + +### Go + +- **[Migrated to the VSR wire protocol](https://github.com/apache/iggy/pull/3834)** +- **[`context.Context` on client methods](https://github.com/apache/iggy/pull/2964)** +- **[Transport state separated from session state](https://github.com/apache/iggy/pull/3652)**, **[structured logger](https://github.com/apache/iggy/pull/3379)** aligned with the Rust SDK +- **[Request-path wire payload pooled](https://github.com/apache/iggy/pull/3442)**, **[delete segments](https://github.com/apache/iggy/pull/3191)** +- Fixed **[bounds checking for payloads over 64 KB](https://github.com/apache/iggy/pull/3165)**, **[S2 decode returning an error instead of panicking](https://github.com/apache/iggy/pull/3166)**, **[teardown of only the failed connection](https://github.com/apache/iggy/pull/3986)**, **[`MaxPayloadSize` raised to 64 MB](https://github.com/apache/iggy/pull/4080)** + +### Node.js (TypeScript) + +- **[VSR framing](https://github.com/apache/iggy/pull/3763)** and **[classic framing dropped](https://github.com/apache/iggy/pull/3843)** +- **[Connection string syntax](https://github.com/apache/iggy/pull/3917)** +- **[TLS with VSR](https://github.com/apache/iggy/pull/3813)**, **[scoped permissions wire codec](https://github.com/apache/iggy/pull/3642)**, **[tokens no longer dropped in list responses](https://github.com/apache/iggy/pull/4171)** +- **[npm prereleases published under a channel tag, not `latest`](https://github.com/apache/iggy/pull/3370)** +- Node.js client bumped to 0.10.0 + +### C++ + +The low-level bindings from 0.8.0 grew into a client: + +- **[Messaging FFI functions](https://github.com/apache/iggy/pull/3046)**, **[system functions](https://github.com/apache/iggy/pull/3102)**, **[consumer groups and topics](https://github.com/apache/iggy/pull/3506)**, **[offsets](https://github.com/apache/iggy/pull/3559)**, **[client lifecycle and message headers](https://github.com/apache/iggy/pull/3610)** +- **[User management and an initial high-level client](https://github.com/apache/iggy/pull/3733)**, extended with **[stream, topic and partition functions](https://github.com/apache/iggy/pull/4022)** +- **[BDD tests for basic messaging](https://github.com/apache/iggy/pull/3554)**, **[Bazel upgraded to 9.1.1](https://github.com/apache/iggy/pull/3511)** + +### CLI + +- **CLI bumped to 0.14.0**, **[with VSR support](https://github.com/apache/iggy/pull/3679)** +- **[`context show` and `session status` commands](https://github.com/apache/iggy/pull/3096)** +- **[Pure-Rust zbus keyring instead of libdbus](https://github.com/apache/iggy/pull/3326)**, one system dependency fewer +- **[Bounded ping retries](https://github.com/apache/iggy/pull/3903)**, **[CLI input validated](https://github.com/apache/iggy/pull/4150)** + +--- + +## Connectors + +The connectors runtime, now 0.5.0, gained eight new sinks, a new source, a new payload format, and a long list of reliability fixes. + +### New connectors + +- **[S3 sink](https://github.com/apache/iggy/pull/3103)** +- **[ClickHouse sink](https://github.com/apache/iggy/pull/2886)** +- **[Delta Lake sink](https://github.com/apache/iggy/pull/2889)** +- **[Apache Doris sink](https://github.com/apache/iggy/pull/3215)**, with **[in-request retries](https://github.com/apache/iggy/pull/3574)** and an **[opt-in CSV output format](https://github.com/apache/iggy/pull/3575)** +- **[Meilisearch sink](https://github.com/apache/iggy/pull/3497)** +- **[SurrealDB sink](https://github.com/apache/iggy/pull/3453)** +- **[Redshift sink](https://github.com/apache/iggy/pull/3654)** +- **[RabbitMQ sink](https://github.com/apache/iggy/pull/3973)** +- **[HTTP source webhook gateway](https://github.com/apache/iggy/pull/3798)**: Iggy can now receive webhooks directly. One listener serves many providers, each routed to its own topic, with bearer or HMAC authentication, secret URL endpoints, and a token-guarded admin API to register, re-key and revoke endpoints at runtime. Delivery is best-effort, and the connector's README says so up front. +- **[InfluxDB v2 and v3 connectors](https://github.com/apache/iggy/pull/3140)** rebuilt as separate source and sink crates + +### Runtime and SDK + +- **[Avro payload support](https://github.com/apache/iggy/pull/3141)** with separate encoder, decoder and transform crates +- **[`unwrap_envelope` transform and envelope detection](https://github.com/apache/iggy/pull/3197)**, fixing source-to-sink format mismatches +- **[Source batch acknowledgments](https://github.com/apache/iggy/pull/3855)**: a source's checkpoint is saved only after Iggy accepted the batch. **[Source checkpoints are deferred and sink failures surfaced](https://github.com/apache/iggy/pull/4153)** instead of swallowed, and the **[PostgreSQL source defers progress until ack](https://github.com/apache/iggy/pull/3957)**. +- **[Source state stored on an HTTP state server](https://github.com/apache/iggy/pull/3940)** as an alternative to a local file +- **[Agent docs, per-batch observability and atomic state](https://github.com/apache/iggy/pull/3321)** +- **[Shared `retry_async` helper](https://github.com/apache/iggy/pull/4104)**, replacing seven hand-rolled retry loops that all waited twice the configured delay before the first retry (**BREAKING** for the Rust connector SDK: `ConnectivityConfig` became `RetryPolicy`) +- **[Per-connector init failures isolated](https://github.com/apache/iggy/pull/3244)**, **[duplicate `iggy_sink_open` and `iggy_source_open` rejected](https://github.com/apache/iggy/pull/3179)**, **[connector key validated before it becomes a path](https://github.com/apache/iggy/pull/4083)**, **[warning when the runtime API is exposed without a key](https://github.com/apache/iggy/pull/3804)** +- **[Sink writes and startup readiness validated](https://github.com/apache/iggy/pull/4154)**, **[failures reported and format conversion corrected](https://github.com/apache/iggy/pull/4152)** +- **[Distinct error variants instead of an overloaded `InvalidRecord`](https://github.com/apache/iggy/pull/3194)**, **[FlatBuffer conversions no longer fail silently](https://github.com/apache/iggy/pull/3431)**, **[protobuf field lengths validated consistently](https://github.com/apache/iggy/pull/4069)** +- Fixed **[PostgreSQL CDC producing no messages](https://github.com/apache/iggy/pull/3640)**, **[Iceberg sink partitions on iceberg 0.10](https://github.com/apache/iggy/pull/3969)**, **[InfluxDB v3 cursor rollback](https://github.com/apache/iggy/pull/3434)**, **[doubled `_total` on counter metrics](https://github.com/apache/iggy/pull/3921)** +- **[Flink sink uses the TCP client](https://github.com/apache/iggy/pull/3657)** +- **[CPU and cgroup limits respected in connector stats](https://github.com/apache/iggy/pull/3647)** + +Learn more in the [connectors documentation](/docs/connectors/introduction). + +--- + +## Kafka gateway + +One of the questions we hear most often is some form of "can I point my Kafka clients at Iggy?". Our answer has two parts. Iggy is not built on Kafka and will not reimplement the Kafka protocol inside the server: the Iggy wire protocol, storage engine and replication are their own design, and that is where the performance comes from. Interoperability instead lives in a **separate gateway process** that speaks the Kafka wire protocol on one side and Iggy's protocol on the other, so existi [...] + +0.9.0 ships the first layer of that gateway. **[The Kafka wire protocol gateway](https://github.com/apache/iggy/pull/3519)** is a new `gateways/kafka` workspace crate: a TCP listener on the Kafka port that decodes requests, enforces a version firewall, and answers `ApiVersions`, `Metadata`, `Produce`, `Fetch`, `ListOffsets` and `CreateTopics`, backed by 184 regression tests over golden wire fixtures. The community-driven rollout plan is public in the **[Kafka to Iggy bridge discussion](h [...] + +To be clear about the current state: **in 0.9.0 this is the foundation layer, not a working bridge yet.** No API persists or reads Iggy data. `Produce`, `Fetch` and `ListOffsets` return a retriable error so Kafka clients keep their data and retry, and `CreateTopics` does not create topics. The work has not stopped at the tag, though. The **[bridge core](https://github.com/apache/iggy/pull/4043)**, an Iggy SDK client with stream and topic mapping, provisioning, high watermarks and an Iggy [...] + +--- + +## Benchmark Dashboard + +- **[Redesigned dashboard with a landing page and compare mode](https://github.com/apache/iggy/pull/3145)**: the [benchmarking platform](https://benchmarks.iggy.apache.org) now opens on a showcase pick and compares any two runs side by side +- **[Unified benchmarks list and best-pick selection](https://github.com/apache/iggy/pull/3146)**, **[mobile layout and compare URLs](https://github.com/apache/iggy/pull/3148)** fixed +- **[Cluster topology recorded in reports and surfaced in the UI](https://github.com/apache/iggy/pull/3737)**, so a result says whether it came from one node or three +- `iggy-bench` **[exposes both durability policies](https://github.com/apache/iggy/pull/4092)** as `--durability` and `--consumer-offset-durability` flags +- Bench dashboard bumped to 0.8.0 + +--- + +## Web UI and MCP + +- Web UI bumped to **0.4.0** and **[served directly by the server](https://github.com/apache/iggy/pull/3676)** +- **[Explicit CSS import for the latest Vite build](https://github.com/apache/iggy/pull/3502)**, frontend dependencies updated +- MCP server bumped to **0.5.0**, **[runs against the replicated server](https://github.com/apache/iggy/pull/3617)** and supports the **[systemd watchdog](https://github.com/apache/iggy/pull/3233)** + +--- + +## Helm Charts + +- **[Cluster deployment with one release per node](https://github.com/apache/iggy/pull/4035)**: `server.cluster` mirrors the server's `[[cluster.nodes]]` roster one for one and renders into `IGGY_CLUSTER_*` variables plus `--replica-id`. Each node is its own release overriding only `selfReplicaId`, with a three-node example to start from. Roster mistakes fail at render time. `server.replicaCount > 1` and autoscaling are now refused, because three independent servers behind one Service sh [...] +- Fixed QUIC published as TCP, a missing WebSocket port, Kubernetes service links leaking `IGGY_*` variables into the server config, and the default image moved off 0.7.0 +- **[Gateway API and Envoy Gateway replace ingress-nginx](https://github.com/apache/iggy/pull/3386)** in the smoke cluster + +--- + +## CI/CD & Infrastructure + +- **[Incubating references removed after TLP graduation](https://github.com/apache/iggy/pull/3928)** +- **[SECURITY.md with the ASF reporting process](https://github.com/apache/iggy/pull/3787)**, **[package tokens replaced with OIDC](https://github.com/apache/iggy/pull/3892)**, **[read-only permissions declared on reusable workflows](https://github.com/apache/iggy/pull/3243)** +- **[Miri undefined behavior detector](https://github.com/apache/iggy/pull/3284)** for `binary_protocol` and `consensus`, **[build fails on rustdoc and rustc warnings](https://github.com/apache/iggy/pull/3925)** +- **[Integration tests run when a binary they launch changes](https://github.com/apache/iggy/pull/4131)**, **[a plugin's integration suite runs when it changes](https://github.com/apache/iggy/pull/4077)**, **[Docker `:edge` refresh gated by the cargo-rail DAG](https://github.com/apache/iggy/pull/3369)** +- **[HawkEye replaces addlicense](https://github.com/apache/iggy/pull/3325)** for license header checks, later **[migrated to v7](https://github.com/apache/iggy/pull/3952)** +- PR triage automation: **[review state labels via slash commands](https://github.com/apache/iggy/pull/3231)**, **[`/pin` and `/unpin`](https://github.com/apache/iggy/pull/4060)**, **[welcome comment](https://github.com/apache/iggy/pull/3261)**, **[auto-assign volunteering issue authors](https://github.com/apache/iggy/pull/3400)**, **[fork-PR commands via `workflow_run` handoff](https://github.com/apache/iggy/pull/3268)**, and a stale bot that **[leaves PRs waiting on review alone](https [...] +- **[AI assistance expectations added to CONTRIBUTING](https://github.com/apache/iggy/pull/4081)** and a **[repo-wide team-review agent skill](https://github.com/apache/iggy/pull/4004)** +- **[Devcontainer for reproducible development](https://github.com/apache/iggy/pull/3656)** +- Most triage and status jobs moved to **[ARM runners](https://github.com/apache/iggy/pull/3270)**, runner disk cleanup **[shared across PHP, Rust, BDD and coverage](https://github.com/apache/iggy/pull/3330)**, **[apt-get bounded so a dead mirror cannot wedge a job](https://github.com/apache/iggy/pull/3924)** +- BDD runners **[fail on undefined steps](https://github.com/apache/iggy/pull/3680)**, **[stream CRUD coverage](https://github.com/apache/iggy/pull/3875)** added, **[integration suites duplicating existing coverage removed](https://github.com/apache/iggy/pull/3823)** +- **[Source release tarball includes gateways](https://github.com/apache/iggy/pull/4126)**, **[DEPENDENCIES.md dropped per ASF policy](https://github.com/apache/iggy/pull/3222)** + +--- + +## What's Next + +Clustering is in. The next releases are about making it faster and complete: + +- **Performance**: the effort behind the benchmark tables above continues. Deferred polling so consumers stop paying for empty round trips, a task per read and write for more I/O concurrency, explicit group commit, Direct I/O instead of `fsync`, and more copy-free paths of the kind 0.9.0 started. Further out, multi-leader clusters with a leader per partition spread across all nodes. A full 0.8.0 vs 0.9.0 vs next comparison is planned for the benchmarking platform. +- **Kafka gateway**: the bridge from the protocol listener into Iggy streams, so Kafka producers and consumers can talk to Iggy through the gateway. Consumer groups follow, as laid out in the [rollout discussion](https://github.com/apache/iggy/discussions/3253). +- **SDK parity**: leader redirection coverage for Python, Node.js and C++, and the PHP SDK on its way to a published package +- **Connectors**: more sources, bounded backpressure end to end, and runtime hardening +- **Agentic AI**: building on the A2A support and the MCP server + +--- + +Thanks to our amazing community and contributors for making Apache Iggy better with every release. Sixty people contributed to 0.9.0, and 34 of them landed their first Iggy pull request in this cycle. Welcome aboard. + +**Join us on [Discord](https://discord.gg/apache-iggy) and help shape the future of the project!** diff --git a/scripts/diagram-text.mjs b/scripts/diagram-text.mjs index 1c45bb8e6..79858f3b4 100644 --- a/scripts/diagram-text.mjs +++ b/scripts/diagram-text.mjs @@ -30,6 +30,8 @@ const SITE = "https://iggy.apache.org"; export const DIAGRAM_TEXT = { + Bench090LatencyChart: `**Chart: latency, 0.9.0 to the next version, per iggy-bench workload.** Three headline tiles (3-node cluster, persisted, 20 producers at p999 and p99, and single node, 20 producers at p999), then an interactive chart with one row per workload and two dots per row on a log scale, one for the 0.9.0 release and one for the development branch that follows it. It switches between single node and 3-node cluster, between p50, p99 and p999, and between table order and bi [...] + AppendOnlyLogViz: `**Diagram: Append-only log.** Messages are appended in order, each with the next offset (0, 1, 2 and so on). New messages are always written at the end. A consumer tracks its own position in the log with an offset, independently of other consumers.`, BenchmarkChart: `**Diagram: Latency improvements, Tokio vs thread-per-core.** Selected historical results comparing v0.5.0 (Tokio) with v0.7.0 (thread-per-core) at about 1,000 MB/s per node. Lower latency is better. diff --git a/src/components/bench-0-9-0.tsx b/src/components/bench-0-9-0.tsx new file mode 100644 index 000000000..9e51dffba --- /dev/null +++ b/src/components/bench-0-9-0.tsx @@ -0,0 +1,401 @@ +/** + * 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. + */ + +"use client"; + +import { useId, useState } from "react"; + +/** + * Latency comparison for the 0.9.0 release post: the 0.9.0 release against + * the development branch that follows it, per iggy-bench workload. Three + * headline tiles on top, then one row per workload with two dots on a log + * axis joined by a line. The tables in the post carry the same numbers, so + * the chart never gates a value. + */ + +type Pair = [before: number, after: number]; + +type Row = { + workload: string; + mbps: Pair; + p50: Pair; + p99: Pair; + p999: Pair; +}; + +type Percentile = "p50" | "p99" | "p999"; +type SetupKey = "single" | "cluster"; +type Order = "table" | "change"; + +type Setup = { + key: SetupKey; + label: string; + rows: Row[]; +}; + +const SERIES = { + before: { label: "0.9.0", color: "#8b93a1" }, + after: { label: "next", color: "#fb4800" }, +} as const; + +const PERCENTILES: { key: Percentile; label: string }[] = [ + { key: "p50", label: "p50" }, + { key: "p99", label: "p99" }, + { key: "p999", label: "p999" }, +]; + +const ORDERS: { key: Order; label: string }[] = [ + { key: "table", label: "Table order" }, + { key: "change", label: "Biggest change" }, +]; + +const SETUPS: Setup[] = [ + { + key: "single", + label: "Single node", + rows: [ + { workload: "pinned producer, 20 actors, 800 MB/s", mbps: [800.1, 800.1], p50: [0.65, 0.588], p99: [1.313, 1.145], p999: [11.087, 2.956] }, + { workload: "pinned producer, 20 actors, persisted, 800 MB/s", mbps: [800.0, 800.1], p50: [1.232, 1.098], p99: [1.601, 1.424], p999: [2.249, 1.967] }, + { workload: "pinned consumer, 20 actors, cold, 800 MB/s", mbps: [800.1, 800.1], p50: [0.498, 0.447], p99: [4.698, 3.313], p999: [5.156, 3.777] }, + { workload: "pinned consumer, 20 actors, warm, 800 MB/s", mbps: [800.1, 800.1], p50: [0.521, 0.544], p99: [1.022, 0.923], p999: [1.573, 3.92] }, + { workload: "pinned producer, 1 actor, 500 MB/s", mbps: [500, 500], p50: [0.347, 0.352], p99: [0.679, 0.683], p999: [1.356, 1.046] }, + { workload: "pinned consumer, 1 actor, warm, 500 MB/s", mbps: [500, 500], p50: [0.272, 0.296], p99: [0.468, 0.478], p999: [0.491, 0.502] }, + { workload: "pinned producer, 20 actors, no rate limit", mbps: [2062.1, 2096.5], p50: [1.086, 0.792], p99: [26.597, 46.431], p999: [40.869, 78.082] }, + { workload: "pinned consumer, 20 actors, no rate limit", mbps: [2713.4, 2532.5], p50: [0.679, 0.619], p99: [6.151, 5.869], p999: [6.476, 23.418] }, + { workload: "pinned producer, 1 actor, persisted, 500 MB/s", mbps: [372.1, 335.8], p50: [0.665, 0.733], p99: [0.825, 0.901], p999: [0.995, 1.151] }, + ], + }, + { + key: "cluster", + label: "3-node cluster", + rows: [ + { workload: "pinned producer, 20 actors, persisted, 800 MB/s", mbps: [800.0, 800.0], p50: [2.905, 2.243], p99: [7.073, 2.938], p999: [12.403, 3.889] }, + { workload: "pinned producer, 1 actor, persisted, 500 MB/s", mbps: [234.4, 233.6], p50: [1.047, 1.062], p99: [1.2, 1.223], p999: [10.857, 1.759] }, + { workload: "pinned consumer, 20 actors, cold, 800 MB/s", mbps: [800.1, 800.0], p50: [0.477, 0.436], p99: [3.831, 2.346], p999: [4.241, 2.675] }, + { workload: "pinned consumer, 20 actors, warm, 800 MB/s", mbps: [800.1, 797.9], p50: [0.484, 0.451], p99: [0.933, 0.812], p999: [1.233, 1.216] }, + { workload: "pinned producer, 20 actors, 800 MB/s", mbps: [800.0, 800.0], p50: [1.782, 1.679], p99: [3.186, 3.25], p999: [3.683, 3.66] }, + { workload: "pinned producer, 20 actors, no rate limit", mbps: [1134.5, 1132.5], p50: [4.366, 4.357], p99: [5.159, 5.296], p999: [6.299, 5.929] }, + { workload: "pinned consumer, 20 actors, no rate limit", mbps: [2710.1, 2594.3], p50: [0.682, 0.657], p99: [6.12, 5.758], p999: [6.445, 23.362] }, + { workload: "pinned producer, 1 actor, 500 MB/s", mbps: [337.2, 325.4], p50: [0.716, 0.735], p99: [0.973, 1.016], p999: [1.057, 1.13] }, + { workload: "pinned consumer, 1 actor, warm, 500 MB/s", mbps: [500.0, 500.3], p50: [0.297, 0.326], p99: [0.493, 0.536], p999: [0.533, 0.574] }, + ], + }, +]; + +/** The three results the post leads with. Clicking a tile opens that row in the chart. */ +const HIGHLIGHTS: { setup: SetupKey; workload: string; percentile: Percentile; label: string }[] = [ + { setup: "cluster", workload: "pinned producer, 20 actors, persisted, 800 MB/s", percentile: "p999", label: "3-node cluster, persisted, 20 producers, p999" }, + { setup: "cluster", workload: "pinned producer, 20 actors, persisted, 800 MB/s", percentile: "p99", label: "3-node cluster, persisted, 20 producers, p99" }, + { setup: "single", workload: "pinned producer, 20 actors, 800 MB/s", percentile: "p999", label: "Single node, 20 producers, p999" }, +]; + +/** Candidate tick positions for the log axis, in milliseconds. */ +const TICKS = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100]; + +function axisTicks(values: number[]): number[] { + const min = Math.min(...values); + const max = Math.max(...values); + let lo = 0; + let hi = TICKS.length - 1; + for (let i = 0; i < TICKS.length; i++) { + if (TICKS[i] <= min) lo = i; + } + for (let i = TICKS.length - 1; i >= 0; i--) { + if (TICKS[i] >= max) hi = i; + } + return TICKS.slice(lo, hi + 1); +} + +function logPosition(value: number, lo: number, hi: number): number { + const a = Math.log10(lo); + const b = Math.log10(hi); + return ((Math.log10(value) - a) / (b - a)) * 100; +} + +function formatMs(value: number): string { + return value.toLocaleString("en-US", { maximumFractionDigits: 3 }); +} + +function formatMbps(value: number): string { + return value.toLocaleString("en-US", { maximumFractionDigits: 0 }); +} + +function deltaPercent([before, after]: Pair): number { + return ((after - before) / before) * 100; +} + +function formatDelta(pct: number): string { + const rounded = Math.round(pct); + if (rounded === 0) return "0%"; + return `${rounded > 0 ? "+" : "-"}${Math.abs(rounded)}%`; +} + +function deltaClass(pct: number): string { + const rounded = Math.round(pct); + if (rounded < 0) return "text-emerald-700 dark:text-emerald-400"; + if (rounded > 0) return "text-red-700 dark:text-red-400"; + return "text-fd-muted-foreground"; +} + +function findSetup(key: SetupKey): Setup { + return SETUPS.find((s) => s.key === key) ?? SETUPS[0]; +} + +function Segmented<T extends string>({ + label, + value, + options, + onChange, +}: { + label: string; + value: T; + options: { key: T; label: string }[]; + onChange: (key: T) => void; +}) { + return ( + <div role="group" aria-label={label} className="inline-flex rounded-md border border-fd-border p-0.5"> + {options.map((option) => { + const active = option.key === value; + return ( + <button + key={option.key} + type="button" + aria-pressed={active} + onClick={() => onChange(option.key)} + className={`rounded px-2.5 py-1 text-xs font-medium transition-colors ${ + active + ? "bg-fd-primary/15 text-fd-foreground" + : "text-fd-muted-foreground hover:text-fd-foreground" + }`} + > + {option.label} + </button> + ); + })} + </div> + ); +} + +export function Bench090LatencyChart() { + const [setupKey, setSetupKey] = useState<SetupKey>("cluster"); + const [percentile, setPercentile] = useState<Percentile>("p999"); + const [order, setOrder] = useState<Order>("table"); + const [hovered, setHovered] = useState<string | null>(null); + const [pinned, setPinned] = useState<string | null>(null); + const titleId = useId(); + + const setup = findSetup(setupKey); + const values = setup.rows.flatMap((row) => row[percentile]); + const ticks = axisTicks(values); + const lo = ticks[0]; + const hi = ticks[ticks.length - 1]; + const x = (value: number) => logPosition(value, lo, hi); + + const rows = + order === "change" + ? [...setup.rows].sort((a, b) => deltaPercent(a[percentile]) - deltaPercent(b[percentile])) + : setup.rows; + + const active = hovered ?? pinned; + + const openHighlight = (h: (typeof HIGHLIGHTS)[number]) => { + setSetupKey(h.setup); + setPercentile(h.percentile); + setPinned(h.workload); + }; + + const gridColumns = "grid-cols-[minmax(0,8.5rem)_minmax(0,1fr)_3rem] sm:grid-cols-[minmax(0,15rem)_minmax(0,1fr)_3.25rem]"; + + return ( + <div className="not-prose my-8 rounded-xl border border-fd-border bg-fd-card p-4 sm:p-6" aria-labelledby={titleId}> + <h3 id={titleId} className="m-0 text-lg font-semibold text-fd-foreground"> + Latency, 0.9.0 to the next version + </h3> + <p className="m-0 mt-1 text-xs text-fd-muted-foreground"> + Milliseconds on a log scale, lower is better. One row per iggy-bench workload, both arms on the same host with the same parameters. The tables below carry the same numbers. + </p> + + {/* Headline tiles */} + <div className="mt-5 grid gap-3 sm:grid-cols-3"> + {HIGHLIGHTS.map((h) => { + const row = findSetup(h.setup).rows.find((r) => r.workload === h.workload); + if (!row) return null; + const [before, after] = row[h.percentile]; + const pct = deltaPercent(row[h.percentile]); + const selected = setupKey === h.setup && percentile === h.percentile && pinned === h.workload; + return ( + <button + key={h.label} + type="button" + onClick={() => openHighlight(h)} + aria-pressed={selected} + className={`rounded-lg border p-3 text-left transition-colors hover:border-fd-primary/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-fd-primary/60 ${ + selected ? "border-fd-primary/50 bg-fd-primary/5" : "border-fd-border bg-fd-muted/30" + }`} + > + <span className="block text-[11px] leading-snug text-fd-muted-foreground">{h.label}</span> + <span className="mt-1.5 flex items-baseline gap-2"> + <span className="text-2xl font-semibold text-fd-foreground">{formatMs(after)}</span> + <span className="text-xs text-fd-muted-foreground">ms</span> + <span className={`ml-auto text-sm font-semibold tabular-nums ${deltaClass(pct)}`}>{formatDelta(pct)}</span> + </span> + <span className="mt-0.5 block text-[11px] text-fd-muted-foreground">from {formatMs(before)} ms on 0.9.0</span> + </button> + ); + })} + </div> + + {/* Filters and legend */} + <div className="mt-6 flex flex-wrap items-center gap-x-3 gap-y-2"> + <Segmented + label="Deployment" + value={setupKey} + options={SETUPS.map((s) => ({ key: s.key, label: s.label }))} + onChange={(key) => { + setSetupKey(key); + setPinned(null); + }} + /> + <Segmented label="Percentile" value={percentile} options={PERCENTILES} onChange={setPercentile} /> + <Segmented label="Order" value={order} options={ORDERS} onChange={setOrder} /> + <ul className="m-0 ml-auto flex list-none items-center gap-4 p-0 text-xs text-fd-muted-foreground"> + {(["before", "after"] as const).map((key) => ( + <li key={key} className="flex items-center gap-1.5"> + <span className="inline-block h-2.5 w-2.5 rounded-full" style={{ backgroundColor: SERIES[key].color }} aria-hidden="true" /> + {SERIES[key].label} + </li> + ))} + </ul> + </div> + + {/* Axis */} + <div className={`mt-5 grid ${gridColumns} items-end gap-x-3 px-1`}> + <div className="text-[11px] text-fd-muted-foreground"> + {setup.label}, {percentile} + </div> + <div className="relative h-4"> + {ticks.map((tick) => ( + <span + key={tick} + className="absolute bottom-0 -translate-x-1/2 text-[11px] tabular-nums text-fd-muted-foreground" + style={{ left: `${x(tick)}%` }} + > + {tick} + </span> + ))} + </div> + <div className="text-right text-[11px] text-fd-muted-foreground">change</div> + </div> + + {/* Rows */} + <ul className="m-0 mt-1 list-none p-0"> + {rows.map((row) => { + const [before, after] = row[percentile]; + const pct = deltaPercent(row[percentile]); + const isActive = active === row.workload; + const unthrottled = row.workload.endsWith("no rate limit"); + const left = Math.min(x(before), x(after)); + const width = Math.abs(x(after) - x(before)); + const tooltipLeft = Math.min(Math.max(x(after), 18), 82); + + return ( + <li key={row.workload} className="m-0 p-0"> + <button + type="button" + onMouseEnter={() => setHovered(row.workload)} + onMouseLeave={() => setHovered(null)} + onFocus={() => setHovered(row.workload)} + onBlur={() => setHovered(null)} + onClick={() => setPinned(pinned === row.workload ? null : row.workload)} + aria-pressed={pinned === row.workload} + aria-label={`${row.workload}: ${percentile} ${formatMs(before)} ms on 0.9.0, ${formatMs(after)} ms on next, ${formatDelta(pct)}`} + className={`grid ${gridColumns} w-full items-center gap-x-3 rounded-md px-1 py-1 text-left outline-none transition-colors hover:bg-fd-muted/40 focus-visible:bg-fd-muted/40 focus-visible:ring-1 focus-visible:ring-fd-primary/60 ${ + isActive ? "bg-fd-muted/40" : "" + }`} + > + <span className="block"> + <span className="block text-xs leading-snug text-fd-foreground/85">{row.workload}</span> + {unthrottled && ( + <span className="block text-[10px] leading-snug tabular-nums text-fd-muted-foreground"> + {formatMbps(row.mbps[0])} to {formatMbps(row.mbps[1])} MB/s + </span> + )} + </span> + + <span className="relative block h-7"> + {ticks.map((tick) => ( + <span + key={tick} + aria-hidden="true" + className="absolute inset-y-0 w-px bg-fd-border" + style={{ left: `${x(tick)}%` }} + /> + ))} + <span + aria-hidden="true" + className="absolute top-1/2 h-0.5 -translate-y-1/2 rounded-full bg-fd-muted-foreground/40 motion-safe:transition-[left,width] motion-safe:duration-300" + style={{ left: `${left}%`, width: `${width}%` }} + /> + <span + aria-hidden="true" + className="absolute top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full ring-2 ring-fd-card motion-safe:transition-[left] motion-safe:duration-300" + style={{ left: `${x(before)}%`, backgroundColor: SERIES.before.color }} + /> + <span + aria-hidden="true" + className={`absolute top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full ring-2 ring-fd-card motion-safe:transition-[left,transform] motion-safe:duration-300 ${ + isActive ? "scale-125" : "" + }`} + style={{ left: `${x(after)}%`, backgroundColor: SERIES.after.color }} + /> + + {isActive && ( + <span + role="tooltip" + className="pointer-events-none absolute bottom-full z-10 mb-2 w-max max-w-[16rem] -translate-x-1/2 rounded-md border border-fd-border bg-fd-popover px-3 py-2 text-left text-xs shadow-lg" + style={{ left: `${tooltipLeft}%` }} + > + <span className="block font-medium text-fd-foreground">{row.workload}</span> + <span className="mt-1 flex items-center gap-2 tabular-nums"> + <span className="inline-block h-0.5 w-3 rounded-full" style={{ backgroundColor: SERIES.before.color }} aria-hidden="true" /> + <span className="font-semibold text-fd-foreground">{formatMs(before)} ms</span> + <span className="text-fd-muted-foreground">0.9.0, {formatMbps(row.mbps[0])} MB/s</span> + </span> + <span className="mt-0.5 flex items-center gap-2 tabular-nums"> + <span className="inline-block h-0.5 w-3 rounded-full" style={{ backgroundColor: SERIES.after.color }} aria-hidden="true" /> + <span className="font-semibold text-fd-foreground">{formatMs(after)} ms</span> + <span className="text-fd-muted-foreground">next, {formatMbps(row.mbps[1])} MB/s</span> + </span> + </span> + )} + </span> + + <span className={`text-right text-xs font-medium tabular-nums ${deltaClass(pct)}`}>{formatDelta(pct)}</span> + </button> + </li> + ); + })} + </ul> + + <p className="m-0 mt-4 text-[11px] text-fd-muted-foreground"> + Change is the {percentile} latency on the development branch relative to 0.9.0. Green is lower, red is higher. Click a row to keep its values open. Snapshot of work in progress, not a final result. + </p> + </div> + ); +} diff --git a/src/mdx-components.tsx b/src/mdx-components.tsx index d321daeee..d3b972cbd 100644 --- a/src/mdx-components.tsx +++ b/src/mdx-components.tsx @@ -36,6 +36,7 @@ import { QuickStartSnippet, DocsHero, } from "@/components/architecture-diagrams"; +import { Bench090LatencyChart } from "@/components/bench-0-9-0"; import type { MDXComponents } from "mdx/types"; export function getMDXComponents(components?: MDXComponents): MDXComponents { @@ -57,6 +58,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents { WhyIggy, QuickStartSnippet, DocsHero, + Bench090LatencyChart, ...components, }; }
