What changed, day by day

Changelog

What each server release changed, then every change merged into main and dev, read from the repository's history each time the site is built. main is what the releases are cut from; dev is ahead of it.

Server release notes

The newest sections of CHANGELOG.md. "Unreleased" is on dev and goes out with the next version.

Unreleased2 changes
  • Site: a new link preview image (og:image) in the site's dark theme, with current facts; the old one still showed the light theme and an outdated command count.
  • Docs: the README opens with a short terminal recording of LEASE, LIMIT, INCRBY ... MAX and ZMOVEDUE, and docs/COORDINATOR.md shows a recorded failover with a client that keeps writing.
Ruvio 0.4.0130 changesRelease
  • Docs: docs/CLIENT_BEHAVIOR.md has a TLS section: what the six clients verify by default, which sockets use TLS (including follow-primary probes), how to disable verification, and the option names per client (issue #323).
  • Fixed: ruvio import --replace staged a copy under <key>:import-tmp, so a real key of that name in the target was deleted without a trace. The staging name now ends in a token made per run, the importer checks that the name is free in the target before writing, and source keys that carry the run's marker are skipped and shown as skipped_staging. A crash can still leave one staging key behind; docs/IMPORT.md says how to find it. Server unchanged.
  • Docs: CLAUDE.md and the .claude/ skills and settings describe how to work on the repository with Claude Code; scripts/check_hygiene.sh scans a change for text that must not reach the public repository.
  • Fixed: a replica's receive queue counted copied PUBLISH messages against nothing after the stall fix below, so a burst of large messages could hold up to 1,024 frames of any size; they now count against the queue's 16 MB budget like records. A replica no longer syncs its log on every drain while CLIENT DURABILITY is in use when nothing arrived. Nothing changes on the primary or on disk.
  • Fixed: a connection with CLIENT DURABILITY whose wait timed out had every byte still in its output buffer rewritten to -NOREPLICAS, including a reply the socket had taken in part and replies an earlier wait had already cleared; a client reading slowly then saw the refusal inside a bulk payload and lost sync with its replies. Only the replies of the timed-out batch are refused now.
  • Performance: a replica's receive thread no longer copies the channel and message and no longer checks the checksum of every copied PUBLISH frame just to tell it from a log record; it checks the length and the magic, and the shard still decodes and verifies the frame once. Fewer allocations per message on a replica of a primary with a high PUBLISH rate. No change to the protocol or on disk; throughput change: not measured.
  • COORDINATOR ADD and COORDINATOR REMOVE are refused while primary_lease_ms is set: data nodes take grants only from the coordinators in their primary_lease_coordinators, so a changed set stopped writes on every primary until each node restarted with the new list. docs/COORDINATOR.md (*Primary lease*) gives the order for changing the set with the lease off.
  • Performance: the fast parser for GET/SET/INCR/DEL/PING finds the command from its name once instead of comparing the name again after reading the arguments. Instructions per request (callgrind, one shard, pipeline 16): GET 1268 to 1175, SET 1454 to 1359, INCR 1632 to 1540, DECRBY 1829 to 1715, SETEX 2960 to 2848, DEL 934 to 857, PING 732 to 634; LPUSH unchanged. Nothing changes in the protocol or on disk.
  • Performance: PFADD on a sparse HyperLogLog finds the register once instead of twice (about 16% fewer instructions per call in a callgrind run with random elements, same stored bytes); EXPIRE and the other deadline writes set the TTL hint in the lookup that finds the key; bulk replies are framed without a staging copy and integers are written two digits at a time; a primary with no replica attached no longer builds a replication frame per PUBLISH. No behaviour change.
  • Fixed: replication stopped after one PUBLISH. A replica took the copied message's frame for a log record that never got applied, so its receive queue filled after about 1,000 more writes and the replica fell behind until it reconnected. Copied messages no longer wait in that queue. Nothing changes in the protocol or on disk.
  • Replication: a primary's replica sender now wakes as soon as a record is synced, and reads replica acks without waiting (a 1 ms read timeout rounded up to the kernel's timer tick, about 4 ms, kept it from noticing new records); before, it found new records on its next 10 ms poll. With always, a write reached the replica after 1.2 ms (p50) instead of 4.9 ms, and WAIT 1 took 21 ms instead of 36 ms (local Linux container, one shard). While it keeps up with the log, the sender keeps the current WAL segment open and reads on from where it stopped (one stat per chunk instead of listing the WAL directory and opening, checking and seeking the segment again; on Windows it reopens as before), and checks each record's header and checksum without decoding its payload. An idle link makes about 190 small system calls per second instead of 120. Nothing changes in the log or the protocol.
  • Fixed: the coordinator's Raft log and vote files were written, with their two fsync calls, on one of the coordinator's two async workers while the log's lock was held, so a disk with 0.5-1 s fsync spikes stopped lease renewals, pings and Raft RPCs for that long. The write now runs on the blocking pool and is awaited before the append is reported, in the order the log changed, and readers of the log no longer wait for the disk. Log and vote file formats and the order of guarantees are unchanged. Checked with the coordinator unit and live tests (22 pass); a run with a slow disk and the fault scripts: not measured. The snapshot file write is not changed.
  • Protocol: a RESP count or bulk length of one to three digits is now read in one step, without the per-digit overflow checks; longer lengths take the old path. Measured with callgrind (one shard, 16-deep pipelines): about 3-6% fewer instructions per request for GET, SET, INCR, LPUSH and a 10-key MSET.
  • Performance: LPUSH/RPUSH with one value and ZADD with one score/member pair no longer allocate a temporary list per command, a push with no blocked client no longer copies its values, and ZADD on a small sorted set no longer builds a set of its members each time. Fewer instructions per command (callgrind, see PR); replies, the log and the protocol are unchanged.
  • Performance: fewer instructions per command on the connection path: the command counters no longer take a RefCell borrow or clone the shard state, and the TTL reaper, database switch, keyspace notice flush and ACL check return inline when there is nothing to do. Measured with callgrind (one shard, pipeline 16, instructions per request): GET 1684 to 1519, SET 1845 to 1678, INCR 2081 to 1916, LPUSH 4460 to 4342, PING 1033 to 874. INFO counters are unchanged. Nothing changes in the protocol or on disk.
  • Speed: under allkeys-lru and allkeys-lfu, a fast-path GET records the read through the key record its lookup already found instead of looking the key up a second time. Eviction order and the recency and frequency data are unchanged; other policies are untouched. Nothing changes in the protocol or on disk.
  • Sorted sets: scores in replies (ZRANGE ... WITHSCORES, pops, ZSCORE, ZINCRBY) are written with less work: integer scores skip the general float formatter and no 352-byte buffer is zeroed per score. Under callgrind, ZRANGE of 100 members WITHSCORES costs 33% fewer instructions with integer scores and 8% fewer with fractional ones. The reply text is byte-for-byte unchanged.
  • Coordinator: a data node that refuses RUVIO LEASE GRANT because of its configuration (its primary_lease_coordinators does not list the coordinator, or its count differs after a coordinator set change; also a replica recorded as primary, or out-of-range values) is now logged as a warning naming the node, the error and the likely cause, at most once a minute per node; it was a debug line only, so the node stopped taking writes when its lease ended with nothing visible. INFO shows lease_grants_refused=n per group (all refusals, including the ordinary late-grant races, which are not warned about). Nothing changes on the nodes or on the wire.
  • Performance: SET of a new key hashes the key once and probes the index once instead of about five times, INCR hashes its key once instead of twice, and a key lookup reads the key record's header once. Instructions per request at pipeline 16 (callgrind, one shard, x86 Linux): SET of new keys 3169 → 2411 (−24%), INCR 2079 → 1946 (−6%), GET 1634 → 1568 (−4%), SET of an existing key 1803 → 1765 (−2%), SET of new 5000-byte values 8837 → 8210 (−7%); LPUSH, LPOP, SADD and HSET 0.4-3% lower. Nothing changes in the protocol, on disk or in memory accounting.
  • Performance: appending a WAL record costs less with everysec or always durability. Short records use the CPU's CRC-32C instruction directly (x86-64 with SSE4.2; other CPUs keep the previous code), a staged record is encoded straight into the batch buffer instead of being copied there, and the check for a failed background sync is one atomic load instead of a mutex lock. Under callgrind with everysec, SET went from about 2,930 to 2,670 instructions per request and INCR from 3,130 to 2,870. The WAL format is unchanged (same bytes, same checksum); memory durability is unaffected.
  • Shards and cluster mode: the hash slot of a key (CRC16) is now computed with lookup tables, four bytes at a time, instead of bit by bit. For a 12-byte key it went from about 19 ns to about 7 ns in a micro-benchmark; slots are bit-identical (checked against the bitwise version and the shared client slot vectors). One shard without cluster mode does not compute slots and is unchanged. Nothing changes in the protocol or on disk.
  • Replication: a primary now sends the log records it ships to a replica in writes of up to 256 KiB instead of one write per record. With one shard, one replica, everysec and 200,000 SET at pipeline 16, the primary's sendto calls went from 82,824 to 12,553 (strace, same machine, fresh builds), and in that run the replica was caught up 3 s after the load instead of holding 50,251 of 86,370 keys. Throughput and CPU gain: not measured. The bytes on the wire, record order, acknowledgements and CLIENT DURABILITY/WAIT are unchanged.
  • Connections: a read wait no longer arms and drops a tokio timer each round, and a reply the socket takes at once no longer builds a write timer. One timer per connection serves the idle and read timeouts and moves only when it fires early or must fire sooner, so both still count from the start of the current wait, as before. Measured with callgrind on one shard: GET/SET at pipeline 1 cost about 11% fewer instructions per request (GET 7307 to 6475), pipeline 16 about 2.5% fewer. Nothing changes in the protocol, the timeouts or on disk.
  • Replication: the replica's receiving thread reads up to 64 KiB at a time, takes frames from that buffer without shifting it, wakes the shard once per read instead of once per frame, and no longer takes the log's lock for every frame (queue accounting and the acknowledgement cursor are read without it). With 1 primary and 1 replica (everysec, 200k SET at pipeline 16) the replica's futex calls went from about 508k to about 130k under strace, and it caught up in 22 s instead of 31 s while traced. Nothing changes in the protocol, the log or WAIT.
  • Shards: a connection whose commands are forwarded to other shards (one port in front of several shards) no longer copies its whole pending output for every batch of forwarded replies, and keeps its forwarding buffers between batches instead of allocating them again. Measured under callgrind with 4 shards behind one port: GET at pipeline 16 takes 4.4% fewer instructions per request, SET 3.1%, GET at pipeline 64 2.8%, SET of 4 KB values 5.3%; no row measured was slower. The reply bytes and their order are unchanged; nothing changes in the protocol or on disk.
  • Performance: less fixed work per command before it runs. The command name lookup compares the name as two machine words instead of byte by byte, and the fast parser for GET/SET/INCR/DEL/PING sends other commands to the general parser after looking at the first two letters of the name, without reading it. Measured with callgrind on a release build, one shard, pipeline 16, after the faster bulk length parsing: GET 45 fewer instructions per request (4%), SET 36 and INCR 26 fewer (about 1%), LPUSH, RPUSH, HSET, ZADD and LPOP 128-145 fewer (2-4%), SADD 107 fewer (2%), EXISTS 168, HGET 113 and ZRANGEBYSCORE 143 fewer (5-9%). Nothing changes in the protocol or on disk.
  • Fixed: a LEASE key stayed forever after RELEASE or expiry (type lease, PTTL -1), so every lock name ever used kept one key and SET NX could never take it. The key now has the lease deadline as its TTL and goes away when the lease expires; RELEASE deletes it and is logged as a plain delete (no WAL format change). Fencing tokens still only grow: a restart from a checkpoint takes new tokens from past the checkpoint's time and from the tokens in it, since a released lease no longer leaves its token behind. RELEASE of a lease now raises keyspace event del. Older logs and snapshots load: a lease in them gets its deadline from the lease, and one that has run out is dropped.
  • IDEM key ABORT fingerprint owner removes a pending idempotency record of the same owner and fingerprint, so a retry acquires it at once instead of waiting for the retention to end. Returns 1 when it removed the record, 0 otherwise; a completed record is never removed. Logged and replicated as an ordinary delete (no WAL format change). See docs/IDEM.md.
  • Fixed: FILLSET ... SWR: once a shard had 65,536 fill markers, making room for a new FILLGET dropped the soft expiry of every stale-while-revalidate value, so FILLGET answered HIT for a stale value until it expired and no caller got a refresh token. Making room now keeps the mark of a value that is still stored and drops only lapsed fill tokens and the marks of values past their deadline. Each mark keeps the value's deadline (8 bytes more per mark, at most 65,536 marks per shard). Memory only: nothing changes in the log, snapshots or the protocol, and commands other than FILLGET/FILLSET are untouched (issue #248).
  • Docs and site: Observability lists the metrics of every .NET integration package; the C# client README covers command tracing and metrics and AbortIdempotencyAsync; the README package table lists Ruvio.Extensions.Idempotency; the integrations page describes the new idempotency and Data Protection options.
  • Docs and site: the .NET rate limiting example on the integrations and use-case pages shows per-policy partitions, chained limits, a concurrency limit, a default policy and OnRejected, with comments. The package changes are in integrations/dotnet/Ruvio.AspNetCore.RateLimiting/CHANGELOG.md. No server change.
  • Docs and site: the benchmarks page now shows the Mac and Azure x86 records (the 2 vCPU cloud host record is no longer shown) and starts with the table of Lua scripts replaced by one command; the home page highlights those results; the long-run tests page is linked from the benchmarks page and the docs; a new concept page, docs/CLUSTER.md, explains shards, replication, failover and multi-node clusters, and docs/CONNECTION_MODEL.md describes failover following and MOVED/ASK. No code change.
  • Docs and site: a measurement record of shard scaling with a cluster-aware client and of the one-port forwarding path before and after its fix (docs/evidence/2026-10-26-azure-shard-scaling.md, benchmark/records/x86-azure-2026-10-26-shard-scaling.json, script benchmark/azure/shard-bench.sh), shown on the benchmarks and long-run tests pages. No code change.
  • Shards: a shard that forwards commands to other shards (a client that is not cluster-aware, connected to one port of a multi-shard server) now wakes each other shard once per event-loop turn instead of once per send. On a 4-core Linux VM with 3 shards and all clients on one port, SET/GET at pipeline 16 went from 0.62-0.80M to 0.93-1.10M ops/s; one shard and cluster-aware clients are unchanged. Nothing changes in the protocol or on disk.
  • Security: the comparison of the requirepass, replication, metrics and coordinator secrets no longer depends on the length of either secret: both sides are hashed to a fixed size before the compare. Nothing changes for correct secrets or per command.
  • CLIENT DURABILITY: after a reply timed out with -NOREPLICAS, the connection's next replies, reads included, now wait again for the replicas (and time out until they hold the write). Before, they left at once and could show a write that a failover then lost, so a reader could see a value go backwards. While replicas are down, a connection with durability on cannot read once any write has been logged; read on a connection without it. Connections that did not ask for durability, and replies after a successful wait, are unchanged. Nothing changes in the log or the protocol.
  • Docs: docs/FAQ.md says that a failover can lose writes acknowledged on a plain connection (asynchronous replication), that the primary lease only narrows the stale-read window, and that CLIENT DURABILITY is the guarantee. No code change.
  • ruvio-ctl import: the source and target passwords can come from RUVIO_IMPORT_SOURCE_PASSWORD and RUVIO_IMPORT_TARGET_PASSWORD instead of the URL (redis://user:@host), and a password written in a URL now prints a warning that it shows in the process list. The import connections are still plain TCP; docs/IMPORT.md says so. Nothing changes for the server.
  • Persistence: WAL segments, snapshot files, the shard layout file and a data directory the server creates are now owner-only (0600 files, 0700 directories) instead of following the process umask. Files and directories that already exist keep their mode. Nothing changes per command.
  • ACL: an enabled user with no password hashes (for example after ACL SETUSER default resetpass, or a user line without nopass or a password) no longer accepts an empty AUTH, and new connections only log in as default without AUTH while default is nopass. Before, resetpass on default left the listener open to unauthenticated clients. Set nopass or a password on the user to log in. Nothing changes per command.
  • Primary lease: with primary-lease on, a script's time limit is cut to the time the lease still holds past the 300 ms write-gate margin, so a long script no longer keeps writing after the coordinators' lease ended. With the lease off nothing changes.
  • Slot moves between groups: a replica that applied its primary's completed gain of a range and restarted before the coordinators sent the new cluster map no longer refuses to start ("cluster_topology gives slot … to another group, but this node holds its data"); it keeps the range and waits for the map. And a data node now refuses RUVIO MIGRATE IMPORT of a range while its background drop of the keys of that range (after giving it away or cancelling an import) is still running, answering retry shortly; before, that drop could remove keys of the new import. A range taken away by a map still needs the coordinators. No file or protocol format changed; nothing changes per command. Live restart test and a test of the refused import: not measured.
  • Clients: the Go client can connect over TLS (Options.TLS), checking the certificate and host name; plain TCP stays the default.
  • Clients: the C# client can connect over TLS (RuvioClientOptions.Tls), checking the certificate and host name; plain TCP stays the default.
  • Docs: the README building-blocks table adds RENEW, SET ... IFEQ|IFGT|IFLT, ZPOPBYSCORE/ZMOVEDUE, LPUSHCAP and RUVIO HOTKEYS/BIGKEYS; docs/FAQ.md has a new section on locks, rate limits, IDEM, CHANGES SUBSCRIBE, delayed jobs and when to stay on Redis; the roadmap notes the shipped part of the JSON field index; examples/09_building_blocks_tour.py is a few-second tour of the main building blocks against a running server. No code change.
  • Docs and site: the Get started page has an "Encrypt with TLS" step with a connect snippet for each of the six clients, and docs/CONFIGURATION.md points to the client TLS options. No code change.
  • Clients: the Python clients (sync and asyncio) can connect over TLS (tls=True, ruvios://), checking the certificate and host name; plain TCP stays the default.
  • Clients: the Java client can connect over TLS (RuvioOptions.Builder.tls()), checking the certificate and host name; plain TCP stays the default.
  • Clients: the Rust client and ruvio-ui can connect over TLS (cargo feature tls, off by default; RUVIO_UI_TLS for the UI), with the certificate and host name checked. Plain TCP stays the default. scripts/run_with_servers.sh now also starts two TLS servers (one demanding client certificates) for the TLS tests. The server is unchanged.
  • Docs: docs/MEMORY_ACCOUNTING.md now says where process memory can reach twice the data (a checkpoint under heavy writes, a full sync, FLUSHALL) and how to plan for it. No code change.
  • Docs: README, docs/COMPATIBILITY.md, ARCHITECTURE.md, PORTABILITY.md, THREAT_MODEL.md, RESILIENCE.md, RELEASING.md and clients/README.md now match the code: RESP3 through HELLO 3, inline commands, scripts under replication, slot moves between machines through the coordinator, the client retry and discovery rules, and the building-blocks table lists LIMIT, IDEM, FILLGET, TRANSFER, CHANGES SUBSCRIBE, JSON.INDEX and CLIENT DURABILITY. No code change.
  • Documented when to turn the sync worker on for always durability (docs/CONFIGURATION.md): the default auto covers one shard on Linux; with several shards set sync_worker = "on" and compare write latency on your disk.
  • Clients: the Go, Java, Python and Node.js reply parsers now refuse a bulk string over 64 MiB and an aggregate over 1,048,576 elements, as the Rust and C# clients already did (docs/CLIENT_BEHAVIOR.md). A reply that large is read in parts (SCAN, LRANGE).
  • Clients: every client has a helper that runs a script with EVALSHA and falls back to EVAL once on NOSCRIPT (eval_cached, evalCached, EvalCached, EvaluateCachedAsync), so a restart or a first call on another shard no longer surfaces NOSCRIPT. Nothing changes for eval and evalsha.
  • Documented installing the prebuilt ruvio-bin package from the Arch User Repository (AUR) in the README and docs/SETUP.md.
  • Upgrading to this release: (1) upgrade all ruvio coordinators together: their Raft log format changed and a rolling upgrade stalls; (2) the replication handshake changed (versions 15-17): a primary and its replicas must run the same release, so a group is upgraded either by switching over to upgraded replicas (reads go on; writes pause for the replicas' restarts plus about down_after_ms) or by stopping the whole group; (3) WAL format minor 6 (new SlotsArrived record, logged by a target primary when it takes a range moved between groups): every log segment this release opens carries minor 6 in its header, and 0.3.4 refuses a segment above minor 3, so a node that ran this release, with or without a slot move, cannot be started with an earlier release; go back with the backup taken before the upgrade, or a fresh full copy from a primary of the older release. A replica of a development build from before this record that follows a newer primary refuses each such record and asks for a full copy, once per finished move; run primary and replicas on the same build; (4) the six client libraries now follow a failover by default (follow_primary and its equivalents, see docs/CLIENT_BEHAVIOR.md and each client's changelog); turn it off to keep the old behaviour. Upgrade the coordinators first, then the data nodes. Steps in docs/COORDINATOR.md (*Upgrading*, *Data nodes*) and docs/RECOVERY.md (*Upgrade*).
  • Slot moves between groups: the end of an arriving range is now in the target primary's log (new WAL record SlotsArrived, format minor 6, logged on the receiving shard when the map gives the primary the range, before the primary records it), and the target's replicas take the range from it. Before, a replica took it only when the coordinators' map reached it, so a replica that was down while two moves into its group ran refused the second range and every newer map once back, and needed a full copy. A late cancel of a range no longer ends a newer range on another shard. A replica drops the copies of a cancelled range, and of a range its primary gave away, before it applies the next record (before, the drop ran in steps and could also drop the copies of a move of the same range that started right after), and drops a cancelled range's copies even when it never saw the range begin. Still not covered: a multi-shard replica down while moves into two of its shards run (each shard replicates on its own, so it may get the second range's start before the first range's end) and a replica that takes a full copy during a move; see docs/COORDINATOR.md (*Limits*). WAL format minor 6: a release before this one cannot read a log that recorded a move between groups after the upgrade. Primary and replicas run the same release, as before. Nothing changes per command.
  • Cluster answers: a replica whose primary has not accepted it yet (just started or repointed with REPLICAOF, and its primary not reached yet) no longer lists itself as the master of its slots in CLUSTER SLOTS, CLUSTER NODES and CLUSTER SHARDS. With a cluster map it lists its group's primary from the map, then itself; without a map the answer is unchanged.
  • docs/MEMORY_ACCOUNTING.md explains what maxmemory counts and what it leaves out (connection and reply buffers, Pub/Sub queues, the Lua heap and unsynced WAL bytes are not charged, so the process can use more than maxmemory), and now matches the code for eviction time and debt, active expiry and the WAL dirty bytes.
  • ruvio coordinator: a data node configured as the group's primary that was restarted a second time after a failover no longer fences the real primary. Its last role was replica, so it forked a newer epoch on start; when the coordinators pointed it back it claimed that epoch, the real primary refused every write (READONLY ... a replica has followed a newer epoch) and the restarted node, which never followed, named itself the owner of the group's slots. The coordinators now send REPLICAOF host port YIELD (new Ruvio extension): the member claims no epoch while it follows the recorded primary and takes a full copy; the epoch on disk is not lowered. Plain REPLICAOF keeps fencing an older primary. A node of an older build gets plain REPLICAOF.
  • ruvio coordinator rebalances slots: COORDINATOR REBALANCE on the leader evens out the slots across the groups, in proportion to their shard counts, as a sequence of slot moves of at most 256 slots each, run one after another on the existing move path, and answers how many moves it plans; COORDINATOR REBALANCE DRYRUN answers the plan (first-last from/shard -> to/shard) without moving anything. COORDINATOR STATUS adds rebalance, rebalance_moves, rebalance_left and rebalance_error; COORDINATOR CANCEL cancels the running move and ends the rebalance. Balance is by slot count, not keys or memory. The plan is not kept in Raft: after a leader change the new leader finishes the move in flight, and COORDINATOR REBALANCE on it goes on. Nothing changes on the data nodes' hot path or in any file or protocol format.
  • Slot moves between groups: a replica of the target group could stop taking cluster maps for good when a second move to its group began before the coordinators' map for the first one reached it (it refused to record the second range while the first waited for a map, then refused every newer map because the second range's data was "not here"). The leader now sends the current map to the target group's replicas before each move (at most 1 s each), so moves can run back to back, as a rebalance does. A replica that is down during two such moves can still be left behind; docs/COORDINATOR.md (*Limits*) says how to recover it. Data nodes are unchanged.
  • ruvio coordinator speaks TLS. New settings in coordinator.toml, all off by default: tls_cert and tls_key put the coordinator port (the Raft calls between coordinators and the Sentinel API) on TLS and make a coordinator call the others with TLS, so every coordinator needs them; tls_ca is the CA that vouches for the other coordinators' and, with node_tls, the data nodes' certificates; node_tls = true dials the data nodes with TLS (the coordinator shows its own tls_cert to a node that asks for a client certificate). Certificate files reload on the next connection after they change. The secret is still checked. Coordinators can now manage data nodes that take only TLS. With TLS off nothing changes; data nodes, clients and the hot path are unchanged. See "Security" in docs/COORDINATOR.md.
  • ACL: a user limited to key patterns (~tenant:*) can no longer run KEYS, SCAN, RANDOMKEY, FLUSHDB, FLUSHALL or SWAPDB; they answer NOPERM, because they listed the key names of other patterns and could wipe the whole database. Upgrading: a user that needs one of these commands gets allkeys (ACL SETUSER name allkeys, or ~* in the ACL file). The default user and users with allkeys or ~* are unchanged. Nothing changes per command for them.
  • All six clients retry -TRYAGAIN for 635 ms (7 retries, waits doubling from 5 ms) instead of 35 ms, long enough for the freeze of a slot move between groups; see each client's CHANGELOG.md and docs/CLIENT_BEHAVIOR.md.
  • Slot moves between groups freeze the range for much less time: the source now waits for the target's replicas to hold the copy before it freezes the range (new RUVIO MIGRATE REPLICAS id on the target, at most 10 s; a target without it is not waited for), and the coordinators ask how the move is going every 10 ms instead of 50 ms, over one connection. Before, the leader waited after the freeze for the target's replicas to take the whole copy; with durability always a replica syncs it record by record, and in a local debug test a writer saw TRYAGAIN for 1.3-1.7 s, now 170-240 ms. Not measured on release builds. No file or protocol format changed; mixed versions keep working.
  • ruvio coordinator starts a slot move again (up to three starts, as after a failover) when the source gave it up because the target did not prepare the frozen range within one second. Before, such a move ended as failed and needed another COORDINATOR MOVE.
  • Client tests: every client now has a live test of a failover carried out by real coordinators. scripts/run_with_servers.sh also starts a group of two data nodes under three ruvio coordinator processes (RUVIO_COORDINATOR_ADDRS, RUVIO_COORDINATOR_MASTER, RUVIO_COORDINATOR_GROUP_ADDRS, RUVIO_COORDINATOR_GROUP_PIDS); each client's test kills the primary with SIGKILL and writes, at once, through a client with the coordinator option and one with the data nodes as seeds, until both reach the new primary. Measured on a Mac (debug server): through the coordinators about 0.9-1.6 s after the kill; with seeds about 5 s (Java) to 7.2 s (the others), because the seeds client lands on the replica, gets READONLY, and its rounds skip that node even after it is promoted. The tests allow 15 s. No client or server code changed.
  • ruvio coordinator refuses to start without a secret (or RUVIO_COORDINATOR_SECRET) when listen is not a loopback address. Before, an empty secret made it accept every call, including Raft votes, from anyone who reached the port. Upgrading: set secret in coordinator.toml on every coordinator (all coordinators share one), or add insecure_no_secret = true to keep the old behaviour on a network nobody else can reach. A loopback listen needs neither. Data nodes and clients are unchanged.
  • Docs: data-node upgrade runbook for groups under ruvio coordinator, checked with three data nodes and three coordinators as local processes (no write taken before the pause was lost; the promoted replica took writes 1.2 s after the primary stopped with down_after_ms 1000). docs/COORDINATOR.md *Primary lease* adds that a node answering a renewal later than 1 s misses that grant, with a primary_lease_ms of 3000-4000 for nodes that stall; CLIENT DURABILITY docs add that after a timeout the connection's next batch waits only if the shard logged a new record, so a read right after can see a write no replica holds yet.
  • CHANGES SUBSCRIBE on a demoted primary: a stream open when the shard got REPLICAOF host port kept running, and once the shard's log was cut back to the fork and continued with the new primary's records, it streamed those records under the old generation; a consumer that later moved to the new primary was told to rewind only after it had applied them. The stream now ends with ["resync", "the shard follows another primary; resume on it"] before any of the new primary's records, and the consumer subscribes on the new primary with its last cursor. Checked in the stream's own loop (role and epoch, already on the shard); nothing changes per command or for a server without streams. See docs/CONFIGURATION.md (*Change stream*).
  • ruvio coordinator finds replicas the file does not list: the leader adds every replica the agreed primary lists in INFO replication and the group does not have yet (its replication address from the replica's CONFIG GET replication-listen), records the group's members in Raft, and watches, lists (SENTINEL replicas) and can promote them. New COORDINATOR FORGET group host:port on the leader drops a member and keeps it from being found again (not the primary, not one the file lists). Data nodes report a process identity, run_id in INFO server (as Redis does), so the coordinators do not add one node found under two addresses twice. Members are now known by their client address instead of their position in the file: the Raft record names the primary by address, and the coordinator-to-coordinator calls ask about a member by address. **The coordinators' Raft state changed again: a data directory written by an earlier development build of the coordinator cannot be read; delete raft-log.json, raft-vote.json and raft-snapshot.json and start all coordinators together.** See "Replicas the file does not list" in docs/COORDINATOR.md.
  • ruvio coordinator: coordinators are added and removed while they run. New COORDINATOR ADD id host:port and COORDINATOR REMOVE id on the leader, and the setting join (default false) for a new coordinator that waits to be added instead of proposing its own membership. Majorities (failovers, the primary lease's coordinator count, SENTINEL quorum and num-other-sentinels, SENTINEL sentinels) now count the coordinators in the Raft record rather than peers in the file, which is only the first membership; a coordinator whose peers differ from the record logs a warning. INFO lists them as coordinators:. See "Adding and removing coordinators" in docs/COORDINATOR.md.
  • SCAN, HSCAN, SSCAN and ZSCAN lower a COUNT above 10000 to 10000. One call runs on the shard's thread, so an unbounded COUNT held up every client of the shard; a scan that asked for more now needs more calls. Nothing changes for a COUNT of 10000 or less. The bounds are listed in the new docs/LIMITS.md.
  • ruvio coordinator INFO shows what the leader did and sees: failovers_total and last_failover_unix_ms, and per group members_answering=n/m, state=ok|lease_running|waiting|no_candidate with state_ms, and lease_grant_ms with the primary lease. A group waiting without a writer is warned about when it starts waiting and then at most every 10 s, instead of on every tick. See the coordinator section of docs/OBSERVABILITY.md.
  • ruvio coordinator: a member is heard from only when every shard answers ROLE and INFO replication. Before, only shard 0 had to answer, so a multi-shard primary with another shard down (crashed, its port cut) was never failed over, and a replica with a shard down could be promoted. Now such a primary is failed over once down_after_ms passes without a complete answer, and such a replica is passed over. With the primary lease, a coordinator does not renew the lease of a primary with a shard it cannot reach, so the failover is not held up by a lease the old primary keeps renewing through its other shards. Coordinators ask each shard for ROLE once more per ping; nothing changes on the data nodes.
  • ruvio coordinator: the leader confirms with a majority of coordinators before every command it sends a data node, not only at the start of each tick (a confirmation is still trusted for one ping_interval_ms, at most 1 s), so a leader that loses its majority in the middle of a tick no longer points members at a primary a newer leader replaced. Each group is now tended on its own task, and a member gets down_after_ms (at least 1 s) to answer a command instead of 10 s: a member that accepts connections but stops answering no longer holds up a failover in another group (measured on a Mac, debug build, down_after_ms 1000: the other group's replica took over 1.3 s after its primary was killed, against 9.6 s before). Nothing changes on the data nodes.
  • Multi-shard shutdown with a failed shard log: the failed shard hands the final checkpoint round a copy of its last published file, and could hand it a second one after that round was already published, starting a new round no shard completed. Its directory (a full copy of the shard file) was left in checkpoints/ until two later rounds published. No round starts after the final round is published now. No file format changed.
  • Lua scripts can call redis.call, redis.pcall, redis.error_reply and redis.status_reply, the same functions as the ruvio.* ones, so scripts written for Redis that only use these four run unchanged. The redis table is read-only like ruvio. Nothing else of Redis's Lua API is added, and nothing changes per command or for scripts that do not use the name.
  • ruvio coordinator: optional primary lease (issue #108). An old primary cut off from the coordinators together with one of its replicas kept min-replicas-to-write 1 satisfied and took writes for the whole partition. With primary_lease_ms on the coordinators (default 0, off; primary_lease_drift_percent default 10, below 100: how much faster or slower than real time any clock may run, a coordinator counting a lease ended after it times (100 + d) / (100 - d) plus 200 ms) and primary-lease yes on the data nodes (replication.primary_lease, RUVIO_PRIMARY_LEASE, CONFIG SET; default off), each coordinator renews the agreed primary's lease every ping interval, each group on its own task (new RUVIO LEASE BEGIN/GRANT, @admin @dangerous), a writer refuses writes with NOREPLICAS Primary lease expired: too few coordinators renewed it. unless grants from a majority run for more than 300 ms more (measured with CLOCK_BOOTTIME on Linux, so a suspended host wakes with its lease expired), and a failover (and COORDINATOR FAILOVER FORCE) waits until a majority of coordinators report the lease ended by their own settings; each of them then grants that member again only after the leader confirms, after any failover it is deciding, that its record still names it (a confirmation waits only for a decision about its own group). A node counts the majority against the largest coordinator count (at most 255) a running grant of the current epoch names, and forgets grants that ended, so coordinators that start over at a lower epoch are taken at most one lease later. The window shrinks from the whole partition to at most the lease; writes taken in it are still lost. The check runs on the existing 100 ms tick: nothing is added per command, and with the setting off the tick reads one flag. INFO replication gains primary_lease, primary_lease_grants and primary_lease_remaining_ms. Turning it on needs every coordinator on this version (new coordinator-to-coordinator calls report and confirm the lease; older coordinators neither grant nor answer them, so failovers wait for a majority that does; with the lease off a newer leader falls back to the older call): upgrade the nodes, then all coordinators together, set it on the coordinators, and turn it on at the nodes last; turn it off on the nodes first. Clients need no change. Docs: docs/COORDINATOR.md now states the split-brain bound without the lease (only min-replicas-to-write equal to the number of replicas bounds this shape, and CLIENT DURABILITY REPLICAS 1 can lose acknowledged writes in it) and has a table of failure shapes and what each can lose.
  • ruvio coordinator records the committed sequences a failover must keep in Raft (issue #107): the leader writes the shards that rose at most once a second, in one entry for all groups, and decides at each promotion which values still hold (recorded with the promotion). A new leader acts only after it applied the log of its term. So a new leader, or coordinators that all restarted while the primary was down, no longer promote a replica missing acknowledged CLIENT DURABILITY writes. Coordinators' INFO adds recorded= per group. The coordinators snapshot their log every 100 entries instead of 5000. The coordinators' Raft log format changed: upgrade all coordinators together (a rolling upgrade stalls; going back means deleting the coordinators' Raft files, see docs/COORDINATOR.md). Data nodes, clients and the hot path are unchanged.
  • Output limit: a pipeline whose client was reading its replies could still be cut with -ERR output buffer limit exceeded, its last replies dropped, when the replies queued since the last send crossed max_output_buffer_bytes before the next send (more likely with large replies, or a limit under 128 KiB, where nothing was sent before the limit). A pipeline now sends what the socket takes by the time half the limit is unsent, so only bytes the client has not taken count, as documented; a single reply larger than the limit is still refused.
  • ruvio coordinator: a leader cut off from the other coordinators stops acting on the data nodes (issue #106). The Raft library does not step down a leader that lost its majority, so the leader now confirms with a majority (one heartbeat round per tick, at most one ping_interval_ms, capped at 1 s) before failovers, REPLICAOF corrections, cluster map pushes, slot move steps and COORDINATOR FAILOVER FORCE/MOVE/CANCEL; until it can, it logs one warning and does nothing to them. See docs/COORDINATOR.md.
  • Docs: how IDEM, ONCE, LIMIT and LEASE behave across a failover (use CLIENT DURABILITY REPLICAS 1 on the writing connection, keep clocks synchronized, a failover-safe lock recipe with fenced tokens); see docs/IDEM.md.
  • Python client: the asyncio client now applies the 30 s command timeout the other clients have (socket_timeout, None turns it off), with a blocking command's own wait added; see clients/README.md.
  • CHANGES SUBSCRIBE cursors survive a failover (issue #24): a cursor is now <generation>:<shard>:<seq>, the generation being the shard's replication epoch, and events, heartbeats, the subscribe reply and the moved value of a range moved between a server's own shards use that form. The old <shard>:<seq> form is still accepted, read as the current generation. On a promoted replica, a cursor of the old primary's generation at or before the fork resumes as before; one past it gets one ["rewound", cursor] push naming the fork, then the stream continues from there, and the client undoes or reloads what it applied after it. A cursor of a newer generation, or of one older than the shard forked from, is refused. New COMMITTED option: only records at or below replication_committed_sequence are streamed, so no event is taken back by a failover; it is refused on a shard that does not replicate, and delivers nothing new while no client uses CLIENT DURABILITY. A demoted primary now forgets its committed sequence. No file format changed: the generation and the fork come from the existing LINEAGE_#### files. See docs/CONFIGURATION.md (*Change stream*).
  • Very large expirations (issues #94, #101): a relative expiration whose deadline does not fit a signed 64-bit millisecond clock (SET k v PX 9223372036854775807, EX 9223372036854775, PEXPIRE, PSETEX, EXPIRE, TTL on create, FILLSET, IDEM, RESTORE) is refused with invalid expire time, as Redis does. Before, it was stored, TTL panicked a debug build (a release build wrapped to a negative number) and PEXPIRETIME answered a negative number. EXPIREAT and SET PXAT up to the largest value are still accepted. LEASE, SEMAPHORE, RENEW, ONCE and GETEX have no error reply; their very large deadlines are now reported without overflow by TTL, PTTL and PEXPIRETIME. Nothing changes on the path of other commands.
  • Primary lease: a data node takes RUVIO LEASE GRANT only from the coordinator ids listed in the new setting replication.primary_lease_coordinators (RUVIO_PRIMARY_LEASE_COORDINATORS=1,2,3), and only when the grant's coordinator count equals the list's length. Before, the grant named its own coordinator id and count, so a client with the ruvio|lease right (part of +@all and @admin) could fake a majority or raise the bar so none formed. Upgrading: list your coordinators' ids on every data node and restart it before turning the lease on; primary_lease = true (or CONFIG SET primary-lease yes) is refused while the list is empty, and grants from unlisted ids are answered with an error. Nothing changes without the lease. See docs/CONFIGURATION.md and docs/COORDINATOR.md.
  • ruvio coordinator keeps CLIENT DURABILITY writes across a failover (issue #24): it reads each member's replication_committed_sequence with the INFO replication it already sends, and promotes only a replica that holds, in every shard, the highest committed sequence seen for the group. When none does, the group waits without a writer and the leader logs a warning on every tick. New coordinator setting unclean_failover (default false) promotes the best replica anyway, and new COORDINATOR FAILOVER group FORCE on the leader does it once; both lose the records the replica lacks. The coordinators' INFO shows the known values as committed= on the group's line. Groups that never use durability fail over as before. See docs/COORDINATOR.md.
  • CLIENT SETINFO LIB-NAME|LIB-VER value answers OK (the library name and version are not kept), so client libraries that announce themselves at connect no longer get an error. Any other attribute is refused as in Redis. Nothing else changes.
  • XADD and XTRIM accept LIMIT count after MAXLEN ~ or MINID ~ instead of answering an error. The count is read and ignored: the stream is always trimmed to its threshold. LIMIT without ~ is still a syntax error, as in Redis. XREADGROUP over several streams is not part of this change.
  • New CLIENT DURABILITY REPLICAS n [TIMEOUT ms] (issue #24): a connection's replies leave only once n replicas hold the records written before them on disk (default timeout 5000 ms; REPLICAS 0 turns it off). The primary syncs its log at once instead of on the next everysec tick, and its replicas sync and acknowledge every batch while someone waits. On timeout every reply of that batch becomes -NOREPLICAS Not enough good replicas to write.; the writes stay applied, as with WAIT. Refused inside MULTI and on a server without replication. A connection that does not ask pays nothing (one check per reply flush). Replicas show the primary's committed sequence in INFO (replication_committed_sequence). Measured locally in debug: a durable SET with one replica on the same machine took about 26-28 ms; not measured on real machines. All six clients have the command and a connect option that sends it on every connection. The replication handshake changed (versions 15-17): primary and replicas must be upgraded together. See docs/COORDINATOR.md and docs/COMPATIBILITY.md.
  • INCRBYFLOAT and HINCRBYFLOAT (issue #95): the result is rounded to 16 significant digits instead of 15 (0.1 + 0.2 still prints 0.3, as in Redis), so more of a large counter survives, e.g. 12345678901234.5 plus 0.01 now gives 12345678901234.51. The rounded text is still what is stored, so digits beyond the 16th are lost and very small increments can still vanish; docs/COMPATIBILITY.md now says so and points to INCRBY, TAKE and TRANSFER for money. Clients' doc comments say 16 digits.
  • Behaviour change: LEASE and SEMAPHORE tokens are fenced (issue #96): a token is larger than every token an earlier holder of the key got, also after the key expired or was deleted, after a restart, a failover or a slot move to another group. LEASE tokens no longer count 1, 2, 3 per key and no longer restart at 1 after DEL. The token is generation << 48 | counter: the generation is the replication epoch, or the cluster map version a coordinator raises on every failover and committed move; the counter has a clock floor (milliseconds × 64). A semaphore that exists at the upgrade keeps counting from its saved token; new semaphores start lower than the old clock-based tokens did. The reply is still one integer; treat it as an opaque 64-bit value. No log format change; nothing on the path of other commands.
  • Slot ranges move between groups of a multi-node cluster (issue #23): COORDINATOR MOVE first last group shard on the coordinators' leader moves the range to that shard of group while both keep serving it; COORDINATOR STATUS and COORDINATOR CANCEL follow and stop it. The source node copies the range to the target over its client port (RUVIO MIGRATE, @admin @dangerous), then freezes it for the last changes: its commands get TRYAGAIN until the coordinators commit the move and send a newer map, after which the source answers MOVED and drops its copy in bounded steps. Replicas of both groups follow from the log (new SlotsIncoming and SlotsGiven records, WAL format minor 5), a prepared move survives restarts of both nodes, and a failover before the commit cancels and restarts it. A node now accepts a map that changes its own slots, but refuses one that gives it a slot whose data is not there. New setting migration_max_bytes_per_sec (default 0, no cap) limits a source's copy rate. Nothing changes for a cluster that moves nothing. See docs/COORDINATOR.md.
  • Multi-node cluster (issue #22, first step): cluster_topology names a map that gives every hash slot to one group (a primary and its replicas) and cluster_group this node's group. A node serves its group's slots, split evenly over its shards, and answers -MOVED slot host:port for every other slot (inside MULTI the transaction is aborted); CLUSTER SLOTS, SHARDS, NODES and INFO describe the whole map. The map is read at startup and fixed; data is not moved between groups yet. A node that holds keys of slots the map serves elsewhere refuses to start. With ruvio coordinator, give each group shards and slots in coordinator.toml: the coordinators send every node the map (new RUVIO TOPOLOGY SET/VERSION, @admin @dangerous), named by each group's agreed primary, after every failover; a node keeps the newest in TOPOLOGY in its data directory. A one-shard node with a map runs as a one-shard cluster. Nothing changes without a map. See cluster_topology in docs/CONFIGURATION.md and docs/COORDINATOR.md.
  • New RUVIO MOVESLOTS first-last shard and RUVIO MOVESLOTS STATUS (@admin @dangerous): move a slot range to another shard of the same server while both keep serving it, to take load off a busy shard. The source copies the range in bounded steps, sends the keys written meanwhile again, and hands over in one step that pauses the source shard briefly (measured on a Mac with 200,000 keys in one slot: about 0.06 ms at the median and at most 1.5 ms in memory mode, 13-18 ms in everysec and always, almost all of it the target's log sync and writing the slot table); then it drops its copy. Clients still routing to the old shard are forwarded; no MOVED. One move at a time; refused while replication is on and in snapshot durability. With a log, the move survives a crash at any step: the new SLOTS file in the data directory records the moves, and recovery keeps, per slot, only what each shard held while it owned it. Copies are logged as a new WAL record (SlotCopy), and the old owner's CHANGES stream gets a moved event naming the cursor where the new owner continues. The WAL format minor is now 4: a server that moved slots writes logs an older release cannot read. All six clients have move_slots and move_slots_status (MoveSlotsAsync/GetMoveSlotsStatusAsync in C#). See docs/COMPATIBILITY.md and docs/RECOVERY.md.
  • HMSET (issue #26): multi-field HSET that replies OK, as Redis does; PHP cache handlers such as CodeIgniter 4's send it through phpredis. New [docs/PHP.md](docs/PHP.md): phpredis and Predis, checked against Ruvio, with CodeIgniter 4 and Laravel cache and session notes. The six clients have a typed HMSET method.
  • Recovery memory (issue #71): after a crash the WAL records replayed since the last checkpoint were kept for the life of the shard, so a restart used 2-3x the memory of the same data and a memory-limited pod could crash-loop. Each record is now dropped once applied and the freed pages are returned. Measured with 1M keys of 64 B, everysec, SIGKILL and restart: RSS 286 MB before, 102 MB after (95 MB before the kill).
  • hot_keys (issue #52): a hot SET/DEL (and FLUSHDB/FLUSHALL on hot keys) is now logged before the shared hot store changes, so a write the WAL refuses (a failed log, an oversized record) is no longer readable by other clients until a restart. The record is written to disk just after the store's lock is released, as a grouped shard write is: a disk error there answers MISCONF, and the log keeps the record and writes it when it resumes. The exception note in CONFIGURATION.md and the README is gone. Measured with 2 shards: hot SET throughput unchanged (everysec 1.02x, pipelined 0.98x, always 1.05x of before).
  • WAL (issue #32): a segment longer than wal_segment_bytes (written before the setting was lowered, or above ruvio-ctl's 256 MiB default) is read with a warning instead of being reported as corrupt, so startup, ruvio-ctl check/inspect/backup/restore work on such a data directory.
  • ACL (issue #38): a +parent|sub / -parent|sub rule must name a subcommand Ruvio checks (config|get, acl|setuser, client|kill, ...); ACL SETUSER refuses any other. Such a rule could never match a command, and each distinct name was kept in memory for the life of the process. Upgrading: an ACL file or security.users entry written by an earlier release may hold such a rule (-client|setname, +cluster|info); it is dropped at load with a warning naming the user and the rule, and the server starts as before.
  • EVAL, EVALSHA, FCALL and PUBLISH can be queued in MULTI (they aborted the transaction before). Scripts run in their place at EXEC and their writes join the transaction's WAL record; a queued PUBLISH is sent after EXEC is recorded, so DISCARD or a refused write sends nothing. SELECT stays refused inside MULTI (issue #41).
  • Client tracking table (issue #43): the readers of a tracked key are kept inline when there is one (no allocation), in a small vector up to 32, and in a set above that, so a connection leaving no longer scans every other reader of a hot key (O(N²) before) and the common one-reader key uses less memory. Measured on 1M tracked keys: 238 → 190 bytes per key with one reader (-20%), 310 → 288 with two, about the same at four, 796 → 757 at twelve. Tracking off still costs nothing.
  • Docker image (issue #46): new ruvio-ctl ping [--address A] [--connect-only] (exit 0 when the server answers; NOAUTH counts), used by a HEALTHCHECK in Dockerfile and Dockerfile.linux; the builder and runtime base images are pinned by digest, and .github/dependabot.yml keeps the digests current weekly.
  • Docs: clients/java/README.md said 287 commands (the matrix has 296); the compatibility check now also catches a capital "All N commands", and clients/README.md explains why clients/testdata/commands.json has fewer entries (it lists command names, the matrix lists subcommands separately) and that ruvio-ui is not a client (issue #47).
  • ruvio import --replace now writes each key under a temporary key and renames it over the target's, so a crash no longer leaves a half-written key or one without its TTL (issue #29). See docs/IMPORT.md.
  • The HTTP /metrics endpoint is not served by the server (the setting was accepted but nothing listened). It is removed from the docs, deploy/kubernetes.yaml and deploy/ruvio.toml; metrics are read with INFO prometheus. The server logs a warning at startup when metrics_address is set. An HTTP endpoint may come later.
  • SCAN cursor … SLOT n returns only the keys of hash slot n, from any port of a multi-shard server (it runs on the shard that owns the slot). COUNT still bounds each call. No memory is added per key.
  • New ruvio coordinator: automatic failover. Three or five coordinators (a separate mode of the same binary; no data, no data commands) agree through Raft on each replication group's primary, fail it over when a majority cannot reach it (REPLICAOF NO ONE on the replica holding the most history, the others pointed at it), and make an old primary a replica when it returns. They answer the Redis Sentinel API subset clients use (SENTINEL get-master-addr-by-name, masters, replicas, +switch-master), so clients with Sentinel support follow a failover. Shared secret for coordinator calls and AUTH. Built with the default coordinator Cargo feature (openraft 0.9.25). See docs/COORDINATOR.md and deploy/compose.failover.yml.
  • A replica that follows its primary by host name now tries every address the name resolves to; localhost resolving to ::1 first no longer keeps it from a primary listening on 127.0.0.1.
  • Reads from replicas. A replica now tells its primary where clients reach it (advertised_host, and advertised_port or the bind port plus the shard index; a replica that announces a loopback host from another machine is listed at the address it connected from). Once it has finished copying, the primary lists it after its own shard in CLUSTER SLOTS, CLUSTER SHARDS and CLUSTER NODES, in ROLE, and as slave0:ip=…,port=…,state=online,offset=…,lag=… lines in INFO replication. A replica answers the cluster commands with its primary first, so a client seeded with a replica sends writes to the primary. New replication.announced (RUVIO_REPLICA_ANNOUNCED, CONFIG GET replica-announced, default on) keeps a replica out of these answers. Node ids are now the SHA-1 of the announced host:port instead of the shard number. All six clients get a read policy (primary by default, prefer-replica, replica); see docs/CLIENT_BEHAVIOR.md. A replica no longer acknowledges while it installs a full copy and catches up, so WAIT and min-replicas-to-write stop counting it until its copy is complete (before, it acknowledged the log it was replacing). The replication handshake changed again: primary and replicas must be upgraded together.
  • New opt-in TLS for the replication link (issue #37): replication.tls = true, replication.tls_ca (replica), optional replication.tls_client_cert/tls_client_key; the primary reuses tls_cert/tls_key. Off by default, with no change to the stream and nothing loaded when off. The startup warning no longer appears once it is on. See docs/CONFIGURATION.md.
  • Fixes: XADD * on a stream whose last sequence is the largest possible now continues at the next millisecond instead of failing with an invalid ID; LIMIT with a maximum above i64::MAX reports a remaining count of i64::MAX instead of a negative number; a replica whose primary address plus shard number passes port 65535 now reports a configuration error instead of dialling a wrong low port.
  • Deployment files: deploy/kubernetes.yaml now uses the current image tag, no longer ships a placeholder password (create the secret with kubectl create secret, see the comment at the top) and runs as user 10001 with a read-only root file system, no capabilities, fsGroup for the volume and a memory limit; deploy/ruvio.service gets StateDirectory=ruvio (the data directory is created and owned by the service) and sandboxing options; deploy/ruvio.toml no longer sets requirepass = "change-me"; the Dockerfiles stop copying a config file the image never read; docs/SETUP.md points at the current release, and scripts/check_package_metadata.py now fails when a download line or the Kubernetes image tag differs from Cargo.toml.
  • The server now logs a warning at startup when TLS is on and replication listens on, or dials, another machine: TLS covers client connections only, and the replication stream (including its password) is not encrypted. docs/CONFIGURATION.md and docs/THREAT_MODEL.md say so. TLS for the replication port itself is not done yet (issue #37).
  • ruvio import hardening: the reply reader refuses bulk strings above 512 MiB, arrays above 64M items and nesting deeper than 32 instead of allocating or recursing without bound; an error reply to the EXISTS check on the target now stops the import instead of being read as "key absent" (which could overwrite the target's key); with --replace, a key that vanished from the source between the probe and the copy is counted as vanished and the target's copy is no longer deleted; the key's TTL is read again after the copy, so it no longer includes the copy time.
  • Durability fixes: SHARD_LAYOUT is written through a temporary file and renamed, so a crash while it is created can no longer leave a partial file that blocks every later start; ruvio-ctl backup flushes the copied files, the signature and the directories to disk before it reports success; a certificate reload that fails (a half-rotated pair, a half-written file) now keeps serving the previous certificate, logs a warning and is not retried until a file changes, instead of failing every new TLS connection.
  • New REPLICAOF host port, REPLICAOF NO ONE [FORCE] and ROLE: a replica is promoted, and a primary or replica is pointed at another primary, without a restart, on every shard at once. A promoted shard keeps its replicas' logs usable (see the lineage entry). A primary that a replica of its successor connects to stops taking writes (READONLY, INFO replication_fenced), and INFO shows replication_epoch. New min-replicas-to-write and min-replicas-max-lag (Redis semantics, off by default, changeable with CONFIG SET): a primary refuses writes with NOREPLICAS while too few replicas acknowledge; replicas now repeat their acknowledgement every second while idle.
  • Promoting a replica no longer makes every other replica take a full copy. The promoted shard records the history it followed and where it forked (LINEAGE_#### per shard); a replica that stopped at or before that point continues from its own log. An epoch that grows with every promotion makes a replica refuse a replaced primary and a replaced primary refuse such a replica. The replication handshake changed: primary and replicas must be upgraded together, as before.
  • New CONFIG SET name value [name value ...] and CONFIG REWRITE. CONFIG SET changes the memory (maxmemory, maxmemory-policy, compact thresholds, notify-keyspace-events), slowlog and key-statistics, connection (timeouts, connection limits, max-commands-per-second, max-output-buffer-bytes), script and requirepass settings while the server runs; every shard applies the change before OK. A restart-only, unknown or invalid name fails the whole call and changes nothing. Connection settings apply to new connections. Nothing is logged or replicated. CONFIG REWRITE writes the live values into the config file the server started from. docs/CONFIGURATION.md and the site's configuration page show for each setting whether it changes while running and under which CONFIG SET name. All six clients have config_set and config_rewrite (SetConfigAsync/RewriteConfigAsync in C#).
  • Behaviour change: an ACL user granted +config (or +@admin) could only run CONFIG GET before; it can now also run CONFIG SET and CONFIG REWRITE, as in Redis. Give read-only users +config|get, or add -config|set -config|rewrite.
  • All six clients accept a list of seed addresses next to the single host and port. Connecting tries them in order; after a dropped connection a standalone client reconnects starting with the next seed, and a cluster client reloads CLUSTER SLOTS from another known node or seed before giving up. No server change; the rules are in docs/CLIENT_BEHAVIOR.md.
  • New RENEW key token ttl_ms: extends a LEASE or SEMAPHORE holder by its token (1 when the token is a live holder, 0 otherwise) without releasing it, so a long job keeps its slot and its fencing token. All six clients have renew; lease and permit handles gain renew where they exist.
  • TTL on create: INCR, DECR, INCRBY, DECRBY, INCRBYFLOAT, HINCRBY, ZADD, ZINCRBY, GEOADD, XADD, SETBIT, JSON.SET and BF.INSERT take EX seconds or PX milliseconds. The deadline is set only when the call creates the key; an existing key keeps its deadline. The write and the deadline are one WAL record, so a crash never leaves the key without its TTL.
  • EXPIRE, PEXPIRE, EXPIREAT and PEXPIREAT accept NX, XX, GT or LT. SADD, LPUSH and the other variadic writes use EXPIRE key ttl NX in the same pipeline or MULTI.
  • New ZPOPBYSCORE key max [MIN min] [COUNT n] [WITHSCORES] and ZMOVEDUE source destination max [COUNT n]: take the due members of a sorted set (score up to max) and return them, or move them to the tail of a list, in one command. At most 10000 members per call.
  • New: SET key value IFGT|IFLT [EX|PX|EXAT|PXAT|KEEPTTL] writes an integer only when the key is missing or the number is greater (IFGT) or smaller (IFLT) than the stored one, so a watermark, version or best price needs no script. It answers OK or a null bulk; a non-integer argument or stored string is an error and writes nothing. Typed options in all six clients.
  • New TRANSFER source destination amount [MAX cap] and HTRANSFER key from to amount [MAX cap]: move an integer amount between two keys (same slot) or two fields of a hash only when the source holds enough, in one WAL record. Reply [source, destination] after, or a null bulk and no write.
  • LIMIT key max window_ms SLIDING counts with a weighted two-window estimate, and LIMIT key capacity refill_ms TOKENS [COST n] is a token bucket. Both keep a few bytes per key, write nothing when they refuse, and take WITHRETRY to reply [remaining, retry_after_ms]. Plain LIMIT key max window_ms is unchanged, and the new modes use the existing limit records, so the WAL and snapshot formats do not change.
Ruvio 0.3.41 changeRelease
  • XADD ... MAXLEN accepts = as well as ~ before the count, like XTRIM and XADD ... MINID already did.
Ruvio 0.3.310 changesRelease
  • Behaviour change: port_mode = "per-shard" is now the default. A multi-shard server also listens on port+1 .. port+N-1, one port per shard; single is deprecated. A key sent to the wrong shard's port is forwarded to its owner instead of answered with MOVED (SSUBSCRIBE and SPUBLISH still answer MOVED).
  • Behaviour change: in per-shard mode FLUSHDB empties only the shard it is sent to, like Redis Cluster; a plain client on one port sees only that shard for KEYS, DBSIZE, SCAN and FLUSHALL.
  • PUBLISH reaches subscribers on every shard without waiting for them. Each shard's fan-out is bounded (16384 messages or 64 MiB); messages over the bound are dropped and counted in INFO as pubsub_cluster_dropped. The reply counts the local subscribers. PUBLISH inside MULTI is refused on a multi-shard server.
  • WAIT also waits for the replicas of the shards a connection's writes were forwarded to.
  • More writes share one WAL group write (INCRBYFLOAT, SETRANGE, BITFIELD, COPY, LSET, LMOVE), and an SADD that adds nothing writes no WAL record. SLOWLOG times commands with the CPU cycle counter.
  • RUVIO_WAL_PREFILL=auto|on|off controls zero-filling of new WAL segments.
  • Fixed: ACL rules naming a subcommand allocated a new copy of the name each time.
  • Fixed: on Windows, backup, restore and replication failed to flush files opened read-only.
  • Fixed: a replication port past 65535 (listen port plus shard number) wrapped around to an unrelated port instead of being refused.
  • Fixed: on macOS a blocked client that hung up behind unread input was not noticed until its timeout.
Ruvio 0.3.21 changeRelease
  • First release from the public repository: prebuilt ruvio and ruvio-ctl binaries for Linux x64 and arm64, macOS arm64 and Windows x64 on GitHub Releases, and the salihcantekin/ruvio Docker image for amd64 and arm64.

Release notes by package

Every change on main and dev

Page 1 of 2, 2026-10-16 – 2026-10-29. Built from main at 94eb5f9 and dev at b39e6fb.

    • mainMerge dev into mainFix empty docs pages#370 from dev, 3 commits94eb5f9
    • mainSiteFix empty docs pages and fail the build on an empty docs body#369 from fix/docs-empty-pages, 1 commit2465531
    • mainMerge dev into mainBlog, demo GIFs, link preview image#368 from dev, 17 commits22840b7
    • mainMerge main into dev before the dev -> main merge#367 from merge/main-into-dev-1029, 5 commits718aca4
    • mainSiteClient tabs and package links on blog examples#366 from feature/blog-client-tabs, 1 commitbb56c5d
    • mainSiteNew link preview image in the dark theme#364 from site/og-image, 1 commit0de1f40
    • mainSiteBlog with five starter posts#363 from feature/site-blog, 3 commitsfeaf20b
    • maindocsTerminal demo GIFs in README and coordinator docs#362 from docs/demo-gifs, 1 commitebd859e
    • mainfix(site): send GA4 page views, add cookie consent banner and robots.txt (main)#361 from fix/ga-main, 1 commit74a7ad9
    • mainfix(site): send GA4 page views, add cookie consent banner and robots.txt#360 from fix/ga-gtag-scope, 3 commitsd9059d5
    • mainGo clientRelease notes and install line for v0.3.0#359 from claude/go-changelog, 1 commitb16a72e
    • releaseGo client 0.3.0clients/go/v0.3.0
    • mainBring the release version commit from main into dev#358 from claude/sync-main-into-dev, 2 commits5c58938
    • mainSet release versions for the 0.4.0 release#357 from claude/release-versions, 1 commitc7835b6
    • releaseServer 0.4.0ruvio-0.4.0
    • mainMerge dev into main for the release#356 from dev, 72 commits713eeac
    • mainReleaseMerge dev into main#311 from dev, 722 commits8353386
    • mainAdd optional Google Analytics 4 to ruviodb.com#156 from feature/google-analytics, 1 commit229aa91
    • mainFix OSS Scanner image build context#84 from fix/oss-scanner-dockerignore, 1 commitf9ce450
    • mainAdd OSS Scanner image and threat model guide#83 from feat/oss-scanner-main, 1 commitc410615
    • mainSet release versions to server 0.3.4, rust client 0.4.0, C# 0.3.4, UI 0.1.9, npm 0.2.0, PyPI 0.1.2, and Maven 0.1.2.7710800
    • releaseServer 0.3.4ruvio-0.3.4
    • releaseOperator UI 0.1.9ruvio-ui-0.1.9
    • mainSet release versions to server 0.3.4, rust client 0.4.0, C# 0.3.4, UI 0.1.9, npm 0.2.0, PyPI 0.1.2, and Maven 0.1.2.db11eae
    • mainGo client v0.2.0 release notes and install line1e87d07
    • releaseGo client 0.2.0clients/go/v0.2.0
    • mainSet release versions to server 0.3.3, rust client 0.4.0, C# 0.3.4, UI 0.1.9, npm 0.2.0, PyPI 0.1.2, and Maven 0.1.2.6 version updates3d1b748
    • dev → mainOperator UI theme and Console, client constants and docs3 changes from devf13e43a
    • releaseGo client 0.1.1clients/go/v0.1.1
    • mainSet release versions to server 0.3.3, rust client 0.3.2, C# 0.3.3, UI 0.1.8, npm 0.1.2, PyPI 0.1.1, and Maven 0.1.1.6 version updatesf1a8628
    • mainReleaseBuild the server image for amd64 and arm64 on native runnerse1b58dd
    • mainSet release versions to server 0.3.3, rust client 0.3.1, C# 0.3.2, UI 0.1.7, npm 0.1.1, PyPI 0.1.0, and Maven 0.1.0.f5e5bd6
    • releaseServer 0.3.3ruvio-0.3.3
    • mainChangelogsServer notes since 0.3.2; integration helpers go out with 0.1.2d60e2fc
    • mainTestsShorter SAVE stress run and one eviction test per configuration4cf22bf
    • mainTestsSet the short read timeout on the stream the surface test reads from159dd76
    • mainTestsThe extreme-numbers surface test reports its slowest steps when it runs long585e788
    • mainTestsEvented request helpers write while they read, so large pipelines cannot stall93f5b11
    • mainTestsCut the time of four slow engine and budget tests without weakening them3a43bc3
    • mainTestsSAVE throttle checks use the fastest of several SAVEs; surface batches read while they write7c39660
    • mainDocsThe model checker's --duration is nominal work and the seed fixes the scheduleb01443a
    • mainTestsHold the lazy-free queue bounds where a value must be deferred7fed000
    • mainTestsProbe ports and spawn processes under one lock7db84a7
    • mainModel checkerA schedule the seed reproduces, reserved ports, no orphansf9eb160
    • mainTimeCalibrate the stamp rate from tightly bracketed clock pairs9bb452a
    • mainTestsWait for this test's own lazy free and draw enough set samples202637f
    • mainTestsLive servers on reserved port blocks held across restartscef6e83
    • mainRepoCheck text files out with LF on every OS9ffbb3d
    • mainPersistenceFlush files through a handle with write access43b6abd
    • mainTestsWait for the data directory lock a forked child still holds878d831
    • mainTestsHold an injected WAL sync failure until the test releases it7703943
    • mainTestsTime the complexity guards by thread cycles on Windows0826bad
    • mainTestsKeep the everysec zero-alloc rounds clear of a background sync835a533
    • mainTestsBounded waits and reserved ports in the evented and replication tests285a98a
    • mainReplicationRefuse a shard port past 65535 instead of wrapping9b31f4c
    • mainCIHang guards on every client step and a PING readiness check for the differentiala2df354
    • main.NET integration tests: no sub-second wall-clock windows6bd4fd1
    • mainC# client tests: isolate allocation budgets, hold closed ports, wider waitsc292701
    • mainPython client testsHang guards, a DELAY margin and polls instead of sleepsb0ed8e2
    • mainNode client testsWait for events and conditions instead of sleeping707a244
    • mainJava client testsExplicit stream IDs, a DELAY margin and hang guards1f843c3
    • mainGo client testsRead streams from an explicit ID, no tight time bounds447f211
    • mainRust client testsOwn databases, joined fakes and gates instead of sleeps3f7699d
    • mainScriptsWait for PING->PONG from our own servers and stop them within a boundb594dfb
    • mainSite and READMEsUse string, JSON and endpoint helpers in integration examplesaa89053
    • mainCacheWrite and read strings and JSON without an intermediate byte[]73b7e31
    • mainAdd string, JSON and model helpers for IDistributedCache; endpoint extensions for rate limiting and idempotencyd4c41f4
    • mainCIPr-gate runs the Rust tests under nextest with per-test time limits552b61c
    • mainCIMiri on the unsafe code only; nextest with per-test time limits6ec4432
    • mainTestsA port allocator for servers started on a named port0d4ef75
    • mainACLAllocate a subcommand name once instead of leaking one per rule70eacc5
    • mainModel checkerFLUSHDB empties only its own shard in per-shard modea29863c
    • mainTestsWAIT test ports below the ephemeral range; config-file tests skip Miria9ac163
    • mainFix the tests that failed by timing or port reuse on CI, and see a hang-up behind unread input on macOSe7f9058
    • mainCI fixesCluster smoke expects forwarding, tours on separate servers, WAIT test retries busy ports09eb321
    • mainCI fixesRustls 0.23.46 for the new advisories; keep a WAL-fault test out of Miri6384716
    • mainSet the .NET integration package versions for the release5386fe4
    • mainWAIT counts only the shards a connection forwarded writes toe5c5e0b
    • mainReview fixes for the dev preview release1e84832
    • mainApply the C# layout rules: blank line before guard ifsf46781a
    • mainC# layout: a blank line before every if, guards includedd9e9201
    • mainCI on dev and preview releases from devf322ff9
    • mainStop tracking the C# layout tool's build output058a49d
    • mainrustfmt2f32939
    • mainWAIT in per-shard mode also waits for the shards a connection forwarded to2f06449
    • mainPUBLISH fan-outPer-shard budget in messages and bytes, one shared copy, a drop countere5a9753
    • mainApply the C# layout rules to the client, integrations and test harnesses37c598a
    • mainC# layout tool: blank lines and bodies on their own lined536201
    • mainReview fixes for the client routing work66e5317
    • mainSite and docsClient routing notes and cost tools; Rust sharded_subscriberba91610
    • mainC# client: a caller with no cancellation reads its own reply when nobody reads93302d6
    • mainC# cost tool: RUVIO_COST_ONLY runs a single row for quick experiments5f1335e
    • mainC# client: opt-in dedicated reader thread per command socket808be3b
    • mainEvidenceGo rows after the reader hand-off fix; per-socket floor of each clientcca1a53
    • mainClient cost tools run in about a minute; Go reads spread pipeline replies itselfe874b48
    • mainGo clientRoute spread cluster pipelines without a goroutine per nodeec964bc
    • mainCheaper cluster routing in five clients, with a shared routing shapefcd7c3c
    • mainReview fixesPUBLISH fan-out after the gate and bounded; owner lookup without allocation7650ef9
    • mainRemove the GitHub Pages site; ruviodb.com is the site8c91ae8

Older history is on GitHub: main and dev.