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 ... MAXandZMOVEDUE, anddocs/COORDINATOR.mdshows a recorded failover with a client that keeps writing.
Ruvio 0.4.0130 changesRelease
- Docs:
docs/CLIENT_BEHAVIOR.mdhas 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 --replacestaged 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 asskipped_staging. A crash can still leave one staging key behind;docs/IMPORT.mdsays how to find it. Server unchanged. - Docs:
CLAUDE.mdand the.claude/skills and settings describe how to work on the repository with Claude Code;scripts/check_hygiene.shscans a change for text that must not reach the public repository. - Fixed: a replica's receive queue counted copied
PUBLISHmessages 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 whileCLIENT DURABILITYis in use when nothing arrived. Nothing changes on the primary or on disk. - Fixed: a connection with
CLIENT DURABILITYwhose 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
PUBLISHframe 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 highPUBLISHrate. No change to the protocol or on disk; throughput change: not measured. COORDINATOR ADDandCOORDINATOR REMOVEare refused whileprimary_lease_msis set: data nodes take grants only from the coordinators in theirprimary_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):
GET1268 to 1175,SET1454 to 1359,INCR1632 to 1540,DECRBY1829 to 1715,SETEX2960 to 2848,DEL934 to 857,PING732 to 634;LPUSHunchanged. Nothing changes in the protocol or on disk. - Performance:
PFADDon 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);EXPIREand 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 perPUBLISH. 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, andWAIT 1took 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 (onestatper 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
fsynccalls, on one of the coordinator's two async workers while the log's lock was held, so a disk with 0.5-1 sfsyncspikes 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/RPUSHwith one value andZADDwith one score/member pair no longer allocate a temporary list per command, a push with no blocked client no longer copies its values, andZADDon 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
RefCellborrow 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):GET1684 to 1519,SET1845 to 1678,INCR2081 to 1916,LPUSH4460 to 4342,PING1033 to 874.INFOcounters are unchanged. Nothing changes in the protocol or on disk. - Speed: under
allkeys-lruandallkeys-lfu, a fast-pathGETrecords 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,ZRANGEof 100 membersWITHSCOREScosts 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 GRANTbecause of its configuration (itsprimary_lease_coordinatorsdoes 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.INFOshowslease_grants_refused=nper group (all refusals, including the ordinary late-grant races, which are not warned about). Nothing changes on the nodes or on the wire. - Performance:
SETof a new key hashes the key once and probes the index once instead of about five times,INCRhashes 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):SETof new keys 3169 → 2411 (−24%),INCR2079 → 1946 (−6%),GET1634 → 1568 (−4%),SETof an existing key 1803 → 1765 (−2%),SETof new 5000-byte values 8837 → 8210 (−7%);LPUSH,LPOP,SADDandHSET0.4-3% lower. Nothing changes in the protocol, on disk or in memory accounting. - Performance: appending a WAL record costs less with
everysecoralwaysdurability. 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 witheverysec,SETwent from about 2,930 to 2,670 instructions per request andINCRfrom 3,130 to 2,870. The WAL format is unchanged (same bytes, same checksum);memorydurability 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,
everysecand 200,000SETat pipeline 16, the primary'ssendtocalls 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 andCLIENT DURABILITY/WAITare 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/SETat 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, 200kSETat pipeline 16) the replica'sfutexcalls went from about 508k to about 130k understrace, and it caught up in 22 s instead of 31 s while traced. Nothing changes in the protocol, the log orWAIT. - 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:
GETat pipeline 16 takes 4.4% fewer instructions per request,SET3.1%,GETat pipeline 64 2.8%,SETof 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
LEASEkey stayed forever afterRELEASEor expiry (typelease,PTTL-1), so every lock name ever used kept one key andSET NXcould never take it. The key now has the lease deadline as its TTL and goes away when the lease expires;RELEASEdeletes 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.RELEASEof a lease now raises keyspace eventdel. 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 ownerremoves 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 newFILLGETdropped the soft expiry of every stale-while-revalidate value, soFILLGETansweredHITfor 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 thanFILLGET/FILLSETare 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 listsRuvio.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 inintegrations/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, anddocs/CONNECTION_MODEL.mddescribes failover following andMOVED/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, scriptbenchmark/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/GETat 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.mdsays that a failover can lose writes acknowledged on a plain connection (asynchronous replication), that the primary lease only narrows the stale-read window, and thatCLIENT DURABILITYis the guarantee. No code change. ruvio-ctl import: the source and target passwords can come fromRUVIO_IMPORT_SOURCE_PASSWORDandRUVIO_IMPORT_TARGET_PASSWORDinstead 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.mdsays 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 (
0600files,0700directories) 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 auserline withoutnopassor a password) no longer accepts an emptyAUTH, and new connections only log in asdefaultwithoutAUTHwhiledefaultisnopass. Before,resetpassondefaultleft the listener open to unauthenticated clients. Setnopassor a password on the user to log in. Nothing changes per command. - Primary lease: with
primary-leaseon, 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 IMPORTof a range while its background drop of the keys of that range (after giving it away or cancelling an import) is still running, answeringretry 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,LPUSHCAPandRUVIO HOTKEYS/BIGKEYS;docs/FAQ.mdhas 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.pyis 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.mdpoints 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-uican connect over TLS (cargo featuretls, off by default;RUVIO_UI_TLSfor the UI), with the certificate and host name checked. Plain TCP stays the default.scripts/run_with_servers.shnow also starts two TLS servers (one demanding client certificates) for the TLS tests. The server is unchanged. - Docs:
docs/MEMORY_ACCOUNTING.mdnow 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.mdandclients/README.mdnow match the code: RESP3 throughHELLO 3, inline commands, scripts under replication, slot moves between machines through the coordinator, the client retry and discovery rules, and the building-blocks table listsLIMIT,IDEM,FILLGET,TRANSFER,CHANGES SUBSCRIBE,JSON.INDEXandCLIENT DURABILITY. No code change. - Documented when to turn the sync worker on for
alwaysdurability (docs/CONFIGURATION.md): the defaultautocovers one shard on Linux; with several shards setsync_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
EVALSHAand falls back toEVALonce onNOSCRIPT(eval_cached,evalCached,EvalCached,EvaluateCachedAsync), so a restart or a first call on another shard no longer surfacesNOSCRIPT. Nothing changes forevalandevalsha. - Documented installing the prebuilt
ruvio-binpackage from the Arch User Repository (AUR) in the README anddocs/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 aboutdown_after_ms) or by stopping the whole group; (3) WAL format minor 6 (newSlotsArrivedrecord, 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_primaryand its equivalents, seedocs/CLIENT_BEHAVIOR.mdand each client's changelog); turn it off to keep the old behaviour. Upgrade the coordinators first, then the data nodes. Steps indocs/COORDINATOR.md(*Upgrading*, *Data nodes*) anddocs/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; seedocs/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 inCLUSTER SLOTS,CLUSTER NODESandCLUSTER 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.mdexplains whatmaxmemorycounts 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 thanmaxmemory), 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 sendREPLICAOF 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. PlainREPLICAOFkeeps fencing an older primary. A node of an older build gets plainREPLICAOF.ruvio coordinatorrebalances slots:COORDINATOR REBALANCEon 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 DRYRUNanswers the plan (first-last from/shard -> to/shard) without moving anything.COORDINATOR STATUSaddsrebalance,rebalance_moves,rebalance_leftandrebalance_error;COORDINATOR CANCELcancels 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, andCOORDINATOR REBALANCEon 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 coordinatorspeaks TLS. New settings incoordinator.toml, all off by default:tls_certandtls_keyput 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_cais the CA that vouches for the other coordinators' and, withnode_tls, the data nodes' certificates;node_tls = truedials the data nodes with TLS (the coordinator shows its owntls_certto 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" indocs/COORDINATOR.md.- ACL: a user limited to key patterns (
~tenant:*) can no longer runKEYS,SCAN,RANDOMKEY,FLUSHDB,FLUSHALLorSWAPDB; they answerNOPERM, because they listed the key names of other patterns and could wipe the whole database. Upgrading: a user that needs one of these commands getsallkeys(ACL SETUSER name allkeys, or~*in the ACL file). Thedefaultuser and users withallkeysor~*are unchanged. Nothing changes per command for them. - All six clients retry
-TRYAGAINfor 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'sCHANGELOG.mdanddocs/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 idon 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 durabilityalwaysa replica syncs it record by record, and in a local debug test a writer sawTRYAGAINfor 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 coordinatorstarts 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 anotherCOORDINATOR MOVE.- Client tests: every client now has a live test of a failover carried out by real coordinators.
scripts/run_with_servers.shalso starts a group of two data nodes under threeruvio coordinatorprocesses (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, getsREADONLY, and its rounds skip that node even after it is promoted. The tests allow 15 s. No client or server code changed. ruvio coordinatorrefuses to start without asecret(orRUVIO_COORDINATOR_SECRET) whenlistenis not a loopback address. Before, an empty secret made it accept every call, including Raft votes, from anyone who reached the port. Upgrading: setsecretincoordinator.tomlon every coordinator (all coordinators share one), or addinsecure_no_secret = trueto keep the old behaviour on a network nobody else can reach. A loopbacklistenneeds 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 withdown_after_ms1000).docs/COORDINATOR.md*Primary lease* adds that a node answering a renewal later than 1 s misses that grant, with aprimary_lease_msof 3000-4000 for nodes that stall;CLIENT DURABILITYdocs 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 SUBSCRIBEon a demoted primary: a stream open when the shard gotREPLICAOF host portkept 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. Seedocs/CONFIGURATION.md(*Change stream*).ruvio coordinatorfinds replicas the file does not list: the leader adds every replica the agreed primary lists inINFO replicationand the group does not have yet (its replication address from the replica'sCONFIG GET replication-listen), records the group's members in Raft, and watches, lists (SENTINEL replicas) and can promote them. NewCOORDINATOR FORGET group host:porton 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_idinINFO 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; deleteraft-log.json,raft-vote.jsonandraft-snapshot.jsonand start all coordinators together.** See "Replicas the file does not list" indocs/COORDINATOR.md.ruvio coordinator: coordinators are added and removed while they run. NewCOORDINATOR ADD id host:portandCOORDINATOR REMOVE idon the leader, and the settingjoin(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,SENTINELquorumandnum-other-sentinels,SENTINEL sentinels) now count the coordinators in the Raft record rather thanpeersin the file, which is only the first membership; a coordinator whosepeersdiffer from the record logs a warning.INFOlists them ascoordinators:. See "Adding and removing coordinators" indocs/COORDINATOR.md.SCAN,HSCAN,SSCANandZSCANlower aCOUNTabove 10000 to 10000. One call runs on the shard's thread, so an unboundedCOUNTheld up every client of the shard; a scan that asked for more now needs more calls. Nothing changes for aCOUNTof 10000 or less. The bounds are listed in the newdocs/LIMITS.md.ruvio coordinatorINFOshows what the leader did and sees:failovers_totalandlast_failover_unix_ms, and per groupmembers_answering=n/m,state=ok|lease_running|waiting|no_candidatewithstate_ms, andlease_grant_mswith 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 ofdocs/OBSERVABILITY.md.ruvio coordinator: a member is heard from only when every shard answersROLEandINFO 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 oncedown_after_mspasses 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 forROLEonce 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 oneping_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 getsdown_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_ms1000: 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_replyandredis.status_reply, the same functions as theruvio.*ones, so scripts written for Redis that only use these four run unchanged. Theredistable is read-only likeruvio. 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 keptmin-replicas-to-write 1satisfied and took writes for the whole partition. Withprimary_lease_mson the coordinators (default 0, off;primary_lease_drift_percentdefault 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) andprimary-lease yeson 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 (newRUVIO LEASE BEGIN/GRANT,@admin @dangerous), a writer refuses writes withNOREPLICAS Primary lease expired: too few coordinators renewed it.unless grants from a majority run for more than 300 ms more (measured withCLOCK_BOOTTIMEon Linux, so a suspended host wakes with its lease expired), and a failover (andCOORDINATOR 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 replicationgainsprimary_lease,primary_lease_grantsandprimary_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.mdnow states the split-brain bound without the lease (onlymin-replicas-to-writeequal to the number of replicas bounds this shape, andCLIENT DURABILITY REPLICAS 1can lose acknowledged writes in it) and has a table of failure shapes and what each can lose.ruvio coordinatorrecords 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 acknowledgedCLIENT DURABILITYwrites. Coordinators'INFOaddsrecorded=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, seedocs/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 crossedmax_output_buffer_bytesbefore 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 oneping_interval_ms, capped at 1 s) before failovers,REPLICAOFcorrections, cluster map pushes, slot move steps andCOORDINATOR FAILOVER FORCE/MOVE/CANCEL; until it can, it logs one warning and does nothing to them. Seedocs/COORDINATOR.md.- Docs: how
IDEM,ONCE,LIMITandLEASEbehave across a failover (useCLIENT DURABILITY REPLICAS 1on the writing connection, keep clocks synchronized, a failover-safe lock recipe with fenced tokens); seedocs/IDEM.md. - Python client: the asyncio client now applies the 30 s command timeout the other clients have (
socket_timeout,Noneturns it off), with a blocking command's own wait added; seeclients/README.md. CHANGES SUBSCRIBEcursors 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 themovedvalue 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. NewCOMMITTEDoption: only records at or belowreplication_committed_sequenceare 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 usesCLIENT DURABILITY. A demoted primary now forgets its committed sequence. No file format changed: the generation and the fork come from the existingLINEAGE_####files. Seedocs/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 withinvalid expire time, as Redis does. Before, it was stored,TTLpanicked a debug build (a release build wrapped to a negative number) andPEXPIRETIMEanswered a negative number.EXPIREATandSET PXATup to the largest value are still accepted.LEASE,SEMAPHORE,RENEW,ONCEandGETEXhave no error reply; their very large deadlines are now reported without overflow byTTL,PTTLandPEXPIRETIME. Nothing changes on the path of other commands. - Primary lease: a data node takes
RUVIO LEASE GRANTonly from the coordinator ids listed in the new settingreplication.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 theruvio|leaseright (part of+@alland@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(orCONFIG 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. Seedocs/CONFIGURATION.mdanddocs/COORDINATOR.md. ruvio coordinatorkeepsCLIENT DURABILITYwrites across a failover (issue #24): it reads each member'sreplication_committed_sequencewith theINFO replicationit 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 settingunclean_failover(default false) promotes the best replica anyway, and newCOORDINATOR FAILOVER group FORCEon the leader does it once; both lose the records the replica lacks. The coordinators'INFOshows the known values ascommitted=on the group's line. Groups that never use durability fail over as before. Seedocs/COORDINATOR.md.CLIENT SETINFO LIB-NAME|LIB-VER valueanswersOK(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.XADDandXTRIMacceptLIMIT countafterMAXLEN ~orMINID ~instead of answering an error. The count is read and ignored: the stream is always trimmed to its threshold.LIMITwithout~is still a syntax error, as in Redis.XREADGROUPover several streams is not part of this change.- New
CLIENT DURABILITY REPLICAS n [TIMEOUT ms](issue #24): a connection's replies leave only oncenreplicas hold the records written before them on disk (default timeout 5000 ms;REPLICAS 0turns it off). The primary syncs its log at once instead of on the nexteverysectick, 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 withWAIT. Refused insideMULTIand 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 inINFO(replication_committed_sequence). Measured locally in debug: a durableSETwith 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. Seedocs/COORDINATOR.mdanddocs/COMPATIBILITY.md. INCRBYFLOATandHINCRBYFLOAT(issue #95): the result is rounded to 16 significant digits instead of 15 (0.1 + 0.2still prints0.3, as in Redis), so more of a large counter survives, e.g.12345678901234.5plus0.01now gives12345678901234.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.mdnow says so and points toINCRBY,TAKEandTRANSFERfor money. Clients' doc comments say 16 digits.- Behaviour change:
LEASEandSEMAPHOREtokens 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.LEASEtokens no longer count 1, 2, 3 per key and no longer restart at 1 afterDEL. The token isgeneration << 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 shardon the coordinators' leader moves the range to that shard ofgroupwhile both keep serving it;COORDINATOR STATUSandCOORDINATOR CANCELfollow 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 getTRYAGAINuntil the coordinators commit the move and send a newer map, after which the source answersMOVEDand drops its copy in bounded steps. Replicas of both groups follow from the log (newSlotsIncomingandSlotsGivenrecords, 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 settingmigration_max_bytes_per_sec(default 0, no cap) limits a source's copy rate. Nothing changes for a cluster that moves nothing. Seedocs/COORDINATOR.md. - Multi-node cluster (issue #22, first step):
cluster_topologynames a map that gives every hash slot to one group (a primary and its replicas) andcluster_groupthis node's group. A node serves its group's slots, split evenly over its shards, and answers-MOVED slot host:portfor every other slot (insideMULTIthe transaction is aborted);CLUSTER SLOTS,SHARDS,NODESandINFOdescribe 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. Withruvio coordinator, give each groupshardsandslotsincoordinator.toml: the coordinators send every node the map (newRUVIO TOPOLOGY SET/VERSION,@admin @dangerous), named by each group's agreed primary, after every failover; a node keeps the newest inTOPOLOGYin its data directory. A one-shard node with a map runs as a one-shard cluster. Nothing changes without a map. Seecluster_topologyindocs/CONFIGURATION.mdanddocs/COORDINATOR.md. - New
RUVIO MOVESLOTS first-last shardandRUVIO 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 inmemorymode, 13-18 ms ineverysecandalways, 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; noMOVED. One move at a time; refused while replication is on and insnapshotdurability. With a log, the move survives a crash at any step: the newSLOTSfile 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'sCHANGESstream gets amovedevent 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 havemove_slotsandmove_slots_status(MoveSlotsAsync/GetMoveSlotsStatusAsyncin C#). Seedocs/COMPATIBILITY.mdanddocs/RECOVERY.md. HMSET(issue #26): multi-fieldHSETthat repliesOK, 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 typedHMSETmethod.- 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 hotSET/DEL(andFLUSHDB/FLUSHALLon 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 answersMISCONF, 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: hotSETthroughput unchanged (everysec1.02x, pipelined 0.98x,always1.05x of before).- WAL (issue #32): a segment longer than
wal_segment_bytes(written before the setting was lowered, or aboveruvio-ctl's 256 MiB default) is read with a warning instead of being reported as corrupt, so startup,ruvio-ctl check/inspect/backup/restorework on such a data directory. - ACL (issue #38): a
+parent|sub/-parent|subrule must name a subcommand Ruvio checks (config|get,acl|setuser,client|kill, ...);ACL SETUSERrefuses 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 orsecurity.usersentry 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,FCALLandPUBLISHcan be queued inMULTI(they aborted the transaction before). Scripts run in their place atEXECand their writes join the transaction's WAL record; a queuedPUBLISHis sent afterEXECis recorded, soDISCARDor a refused write sends nothing.SELECTstays refused insideMULTI(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;NOAUTHcounts), used by aHEALTHCHECKinDockerfileandDockerfile.linux; the builder and runtime base images are pinned by digest, and.github/dependabot.ymlkeeps the digests current weekly. - Docs:
clients/java/README.mdsaid 287 commands (the matrix has 296); the compatibility check now also catches a capital "All N commands", andclients/README.mdexplains whyclients/testdata/commands.jsonhas fewer entries (it lists command names, the matrix lists subcommands separately) and thatruvio-uiis not a client (issue #47). ruvio import --replacenow 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). Seedocs/IMPORT.md.- The HTTP
/metricsendpoint is not served by the server (the setting was accepted but nothing listened). It is removed from the docs,deploy/kubernetes.yamlanddeploy/ruvio.toml; metrics are read withINFO prometheus. The server logs a warning at startup whenmetrics_addressis set. An HTTP endpoint may come later. SCAN cursor … SLOT nreturns only the keys of hash slotn, from any port of a multi-shard server (it runs on the shard that owns the slot).COUNTstill 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 ONEon 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. Sharedsecretfor coordinator calls andAUTH. Built with the defaultcoordinatorCargo 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;
localhostresolving 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, andadvertised_portor 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 inCLUSTER SLOTS,CLUSTER SHARDSandCLUSTER NODES, inROLE, and asslave0:ip=…,port=…,state=online,offset=…,lag=…lines inINFO replication. A replica answers the cluster commands with its primary first, so a client seeded with a replica sends writes to the primary. Newreplication.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 announcedhost:portinstead of the shard number. All six clients get a read policy (primary by default, prefer-replica, replica); seedocs/CLIENT_BEHAVIOR.md. A replica no longer acknowledges while it installs a full copy and catches up, soWAITandmin-replicas-to-writestop 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), optionalreplication.tls_client_cert/tls_client_key; the primary reusestls_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. Seedocs/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;LIMITwith a maximum abovei64::MAXreports a remaining count ofi64::MAXinstead 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.yamlnow uses the current image tag, no longer ships a placeholder password (create the secret withkubectl create secret, see the comment at the top) and runs as user 10001 with a read-only root file system, no capabilities,fsGroupfor the volume and a memory limit;deploy/ruvio.servicegetsStateDirectory=ruvio(the data directory is created and owned by the service) and sandboxing options;deploy/ruvio.tomlno longer setsrequirepass = "change-me"; the Dockerfiles stop copying a config file the image never read;docs/SETUP.mdpoints at the current release, andscripts/check_package_metadata.pynow fails when a download line or the Kubernetes image tag differs fromCargo.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.mdanddocs/THREAT_MODEL.mdsay so. TLS for the replication port itself is not done yet (issue #37). ruvio importhardening: 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 theEXISTScheck 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_LAYOUTis 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 backupflushes 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]andROLE: 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), andINFOshowsreplication_epoch. Newmin-replicas-to-writeandmin-replicas-max-lag(Redis semantics, off by default, changeable withCONFIG SET): a primary refuses writes withNOREPLICASwhile 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 ...]andCONFIG REWRITE.CONFIG SETchanges 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 andrequirepasssettings while the server runs; every shard applies the change beforeOK. 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 REWRITEwrites the live values into the config file the server started from.docs/CONFIGURATION.mdand the site's configuration page show for each setting whether it changes while running and under whichCONFIG SETname. All six clients haveconfig_setandconfig_rewrite(SetConfigAsync/RewriteConfigAsyncin C#). - Behaviour change: an ACL user granted
+config(or+@admin) could only runCONFIG GETbefore; it can now also runCONFIG SETandCONFIG 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 SLOTSfrom another known node or seed before giving up. No server change; the rules are indocs/CLIENT_BEHAVIOR.md. - New
RENEW key token ttl_ms: extends aLEASEorSEMAPHOREholder 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 haverenew; lease and permit handles gainrenewwhere they exist. - TTL on create:
INCR,DECR,INCRBY,DECRBY,INCRBYFLOAT,HINCRBY,ZADD,ZINCRBY,GEOADD,XADD,SETBIT,JSON.SETandBF.INSERTtakeEX secondsorPX 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,EXPIREATandPEXPIREATacceptNX,XX,GTorLT.SADD,LPUSHand the other variadic writes useEXPIRE key ttl NXin the same pipeline orMULTI.- New
ZPOPBYSCORE key max [MIN min] [COUNT n] [WITHSCORES]andZMOVEDUE source destination max [COUNT n]: take the due members of a sorted set (score up tomax) 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 answersOKor 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]andHTRANSFER 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 SLIDINGcounts with a weighted two-window estimate, andLIMIT key capacity refill_ms TOKENS [COST n]is a token bucket. Both keep a few bytes per key, write nothing when they refuse, and takeWITHRETRYto reply[remaining, retry_after_ms]. PlainLIMIT key max window_msis 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 ... MAXLENaccepts=as well as~before the count, likeXTRIMandXADD ... MINIDalready 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;singleis deprecated. A key sent to the wrong shard's port is forwarded to its owner instead of answered withMOVED(SSUBSCRIBEandSPUBLISHstill answerMOVED). - Behaviour change: in per-shard mode
FLUSHDBempties only the shard it is sent to, like Redis Cluster; a plain client on one port sees only that shard forKEYS,DBSIZE,SCANandFLUSHALL. PUBLISHreaches 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 inINFOaspubsub_cluster_dropped. The reply counts the local subscribers.PUBLISHinsideMULTIis refused on a multi-shard server.WAITalso 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 anSADDthat adds nothing writes no WAL record.SLOWLOGtimes commands with the CPU cycle counter. RUVIO_WAL_PREFILL=auto|on|offcontrols 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
ruvioandruvio-ctlbinaries for Linux x64 and arm64, macOS arm64 and Windows x64 on GitHub Releases, and thesalihcantekin/ruvioDocker 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 pages94eb5f9
- mainSiteFix empty docs pages and fail the build on an empty docs body2465531
- mainMerge dev into mainBlog, demo GIFs, link preview image22840b7
- mainMerge main into dev before the dev -> main merge718aca4
- mainSiteClient tabs and package links on blog examplesbb56c5d
- mainSiteNew link preview image in the dark theme0de1f40
- mainSiteBlog with five starter postsfeaf20b
- maindocsTerminal demo GIFs in README and coordinator docsebd859e
- mainfix(site): send GA4 page views, add cookie consent banner and robots.txt (main)74a7ad9
- mainfix(site): send GA4 page views, add cookie consent banner and robots.txtd9059d5
- mainGo clientRelease notes and install line for v0.3.0b16a72e
- releaseGo client 0.3.0
clients/go/v0.3.0 - mainBring the release version commit from main into dev5c58938
- mainSet release versions for the 0.4.0 releasec7835b6
- releaseServer 0.4.0
ruvio-0.4.0 - mainMerge dev into main for the release713eeac
- mainReleaseMerge dev into main8353386
- releaseServer 0.3.5-preview.33
ruvio-0.3.5-preview.33
- releaseServer 0.3.5-preview.33
- mainAdd optional Google Analytics 4 to ruviodb.com229aa91
- 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.4
ruvio-0.3.4 - releaseOperator UI 0.1.9
ruvio-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.0
clients/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.3d1b748
- dev → mainOperator UI theme and Console, client constants and docsf13e43a
- releaseGo client 0.1.1
clients/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.f1a8628
- 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.3
ruvio-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