Skip to content

Releases: tamnd/yo

v0.3.33

Choose a tag to compare

@github-actions github-actions released this 10 Sep 12:35
v0.3.33
b00169b

Five pull requests between milestones, so this is a patch. Four of them finish atomic slot migration and add the election a cluster needs to survive a node going away, and one takes an allocation, an index insert and an index remove off one write in seven. Nothing here changes the on-disk format and no record kind was added.

Added

  • A slot migration finishes when the far side claims the slots. Everything up to this was already in, the task, the snapshot, the change stream, the write pause and the STREAM-EOF that says nothing more is coming, and what happened next was nothing: this node heard the claim, updated its map, and left the migration sitting in stream-eof until the write pause ran out and gave up on it. The bus now carries the slots this node lost out of the map lock and hands them to the migration, and a task moving exactly those slots and sitting in stream-eof is a migration finishing, so it completes, the pause lifts and the keys behind the slots are dropped. Exactly those slots is the reference's rule and it is stricter than it looks: a claim covering half of what a migration is moving is something else happening to the cluster while a migration ran, and it fails the migration rather than reporting it done. How the drop reaches a follower has three shapes, and they are three situations rather than a choice. A migration finishing writes one TRIMSLOTS naming the ranges and fires a del event per key, because an operator asked for this and is watching. A slot that changed hands by failover or by SETSLOT sends a deletion per key and fires nothing, because nobody asked for those keys to go. A TRIMSLOTS arriving from a master fires the events and propagates nothing, because the command that asked for it is already on the stream.
  • CLUSTER MIGRATION IMPORT, so a node can take slots as well as give them up. This was the last piece of the protocol missing, and it is what stopped the shipped resharding tool working against a cluster of these at all: redis-cli 8.10 picks the new way whenever every node reports 8.4.0 or above, and the first thing it does is tell the destination to import. A node told which slots it wants works out who owns them, refuses the five ways the reference refuses in the reference's order, books a task, answers with the task id straight away, and goes and gets the slots on a thread of its own. It opens the same two connections the reference opens, applies the snapshot while the changes pile up unread behind it, works through the pile, follows the stream, and claims the slots when the source says the stream has ended. The count it acknowledges is decoded command bytes with the control commands left out, which is the same rule on both sides, because the source stops taking writes the moment what it has been told matches what it has sent. Two real bugs came out of writing it. The source held the snapshot connection after the snapshot had been read, so a destination closing its end, which this and a real redis destination both do the moment the last of it lands, read as a channel dropping under a live migration and failed the whole move. And an import that failed partway left the keys it had applied sitting in slots this node does not own, so it now trims on the way out as well as on the way in, and only the slots that are still somebody else's.
  • A replica stands for election when its master fails, and a master here votes in one. This is what decides whether a cluster survives a node going away without somebody watching it. The election is the reference's, check for check: half a second plus a random half second plus a second for every replica of the same master holding more data, then bump the epoch and ask every node. A master answers if it serves a slot, if it has not already voted this epoch, if the node asking really is a replica of a master this node agrees is failed, if it has not voted about that master in the last two node timeouts, and if the slots being claimed have not moved under a newer epoch. There is no no vote on this protocol, so a master that disapproves and a master that has crashed look the same from where the replica stands, which is deliberate, because the alternative is a reply a replica could be made to wait for. The epoch a node last voted in now goes in the config file where the reference puts it, and it was the one field there being written as a constant nought. One real bug came out of running it on live nodes: failure detection measures the time since a ping went out, and a node with no link never gets one sent, so a node whose address stopped answering was dialled again every tenth of a second for ever and never marked failing. The first run sat at fail question mark for sixty five seconds and got no further. A real server has the same problem and solves it in the same place, by saying it sent a ping when the connect fails and by keeping an outstanding ping across a reconnect rather than restarting the clock, and both are in.

Changed

  • An overwrite stays where it is when the new value fits in the run. RawMap::set_with wrote a record over itself only when the new value was exactly the length of the old one, and allocated a new record and marked the old one dead for every other length. Exactly, because nothing in a record said how much room it occupied, so the two lengths in its header were the only thing a walk could step by, and a short value in a long run would have sent that walk into the middle of the next record. The room a record has and is not using is written down now, in the top eight bits of the second header word, in units of the arena's sixteen byte alignment, and the header is still eight bytes: a record cannot be longer than a segment, so a value length has never needed more than twenty one bits and those bits have always been zero. So a record occupies exactly what its header says and a walk steps by the run. An overwrite stays in place when the new record fits and would leave no more than a quarter of the run behind, which bounds what a key can hold unused rather than letting it settle at the largest value it has ever held, and compaction is where the room goes back, since it is the one moment a record is rewritten with nobody waiting on the reply. Measured over twenty thousand keys with values drawn from 1 to 1024, five passes, which is the shape the cache benchmark runs: 14.5 percent of overwrites stay in place for exactly the same number of arena bytes as before. Half the run would keep 30 percent and cost seventeen percent of the arena, and the whole run would keep 68.8 percent and cost two thirds of it. MEMORY USAGE counts the run now rather than the header and the name and the value added up, which is how a real server answers it, by asking the allocator how big the block really is.
  • CLUSTER SETSLOT is refused for a slot inside a live migration. There are two ways to move a slot and they write the same three fields from two directions, so using both on one slot ends with the migration claiming a slot the operator had already handed to somebody else. A real server refuses the old way and names the cancel that gets you out of it, and that sentence is worth copying exactly because a resharding tool is what reads it. Checked against a real 8.10.1 with a task running on both servers in the same state, five cases and no difference. The reference has a second gate there for a slot with a trim job outstanding, which cannot happen here because a trim runs on the spot rather than being queued.
  • The drain timeout is read rather than ignored. It was the last of the four migration settings that was in the config table and read by nothing, and it catches a move that is not broken but never ends: the far side gets through everything behind the snapshot and says so, then stays further behind the stream than a handoff may start at, because this node takes writes faster than that side applies them. Nothing is wrong with either end and left alone it runs until somebody notices. The deadline is the longer of the setting and twice however long the far side took over the snapshot and the pile, since a side that needed a minute for the first part is not one to give ten seconds for the rest, and the span starts where the backlog ended rather than where the move did, so a far side still working through the pile is never given up on for taking a while. With eight writers pushing four kilobyte values faster than the destination could apply them, the task sat in send-stream for twelve seconds with the setting at one millisecond and was not touched, and completed on its own the moment the writers stopped with all 23831 keys across.

Performance

  • The eight core box measured yo at v0.3.31 over the same twelve cell matrix the published epyc8coarse directory holds v0.3.28 in, five runs a cell, and this is where the threading milestone's first gate stands rather than a claim about this release. Sets at depth went up by two to three and a half times: 132 to 457 thousand a second at pipeline 50 with four threads, 140 to 418 at pipeline 25. Gets at one thread went down by a fifth to a quarter at pipeline 10 and deeper, which is open as #541. Against the best of the seven rivals in the same cell the best yo reads is 0.97x and the worst is 0.43x, against a gate of 2.00x, so the milestone has built what the gate needs and has not come near it. These are development measurements at five runs a cell where twenty one of the twenty four cells vary by more than a twentieth, so they are ratios and not rates.
  • Memory, on the same box, eight engines holding 2,499,968 keys with values drawn from 1 to 1024, at peak resident: yo pays 45.0 bytes an entry over the payload, against rugo at 24.7, dragonfly at 41.0, pogocache at 89.8, valkey at 103.4, redis at 116.2, memcache at 146.6 and garnet at 911.3. The payload is 518.6 bytes an entry, so total resident memory is 1.41 GB against rugo's 1.36, and the overhead is the colum...
Read more

v0.3.32

Choose a tag to compare

@github-actions github-actions released this 10 Sep 07:26
v0.3.32
2d7f85e

Six pull requests between milestones, so this is a patch. Four of them are the next steps of a slot migration, one takes a per batch cost off the command path, and one stops the server lying about how many threads it has. Nothing here changes the on-disk format and no record kind was added.

Added

  • A slot range is sent as a snapshot. The node giving slots up now does its half of an atomic migration. A node that has logged in as the cluster asks with CLUSTER SYNCSLOTS SYNC, is told to open a second connection, and when it opens that one with CLUSTER SYNCSLOTS RDBCHANNEL it is sent a snapshot of exactly those slots. The snapshot is a stream of ordinary commands rather than an RDB file, which is the only way a sixteen thousandth of the keyspace can be sent at all: the libraries, a SELECT for every database that holds anything, a count of the keys and the deadlines of each slot in front of the first key of it, the keys themselves, and an end marker. A string goes as the SET that would set it, which is what the reference does so the far side can take a large value apart as it arrives, and everything else goes as a RESTORE with an absolute deadline, because the far side runs these at some unknown moment after they were written and a duration would restart the clock. It is taken with every write held off, which is what this has instead of a fork, and that freeze has been pulled out of the replication code as at_an_instant so the full resync and the migration use one piece of machinery. CLUSTER MIGRATION STATUS and CANCEL now report and cancel the real task rather than answering empty and nought.

  • The four slot migration settings are in the config table. cluster-slot-migration-handoff-max-lag-bytes at 1048576, cluster-slot-migration-write-pause-timeout at 10000, cluster-slot-migration-sync-buffer-drain-timeout at 60000 and cluster-slot-migration-max-archived-tasks at 32, with the reference's defaults, units, ranges and refusals, and the last two hidden the way the reference hides them, which means a pattern does not find them and their own name does. The lag bound and the archived count were hardcoded and are read off the settings now. The first counts bytes and takes a unit, so 2mb reads back as 2097152 the way maxmemory does, and the other three count something else and refuse 10s with the reference's sentence about not being able to parse an integer.

  • The writes that land behind the snapshot are streamed after it. Every command that propagates goes past the migration on its way to the replicas, already rendered in the form a replica would see, and is kept or dropped on the slot its key is in. No keys, dropped. Keys outside the moving ranges, dropped. Keys in two different slots, the migration is cancelled, which is what a real server does with it too. The stream is switched on inside the same freeze the snapshot is read under, so the two meet exactly and nothing is sent twice or missed. CLUSTER SYNCSLOTS ACK now records how far the destination has got, refuses to go backwards, and moves the task to handoff-prep once the destination is within a megabyte of what has been sent.

  • The writes stop and the stream ends when a moving slot changes hands. Once the destination is within cluster-slot-migration-handoff-max-lag-bytes of everything that has been sent there is nothing left to catch up on, so the source stops taking writes, waits out the ones already running, sends CLUSTER SYNCSLOTS STREAM-EOF and waits for the far side to claim the slots. Reads keep working the whole time and a write is held rather than refused, so a client writing during a handoff sees a pause and not an error. The reference does this by waiting for a socket buffer to drain, which it can because it is single threaded and nothing else runs in between. Here the writes are on other threads, so the pause goes on first and then the same freeze the snapshot is taken under is used as a barrier: it touches every stripe, and a write feeds the migration stream while it still holds its stripe, so coming out the other side means every write in flight has already been sent. The pause carries its own deadline as well as being lifted by hand, so a bug in the lifting cannot leave a server refusing writes for good, and a destination that never takes the slots is given up on after cluster-slot-migration-write-pause-timeout with the slots and the keys where they were.

Changed

  • A memory reading is taken once a millisecond and weighs only the databases that moved. On a server with --maxmemory set, the maintenance turn after every batch folded a walk over every stripe of all sixteen databases, locking each one and asking it for its slab bytes plus seven collection totals. Fifteen of those sixteen are empty on every benchmark cell there is. A batch is a hundred nanoseconds, so that was ten thousand readings a millisecond of a number that moves by what sixty four commands allocated. The reading is now gated to one a millisecond per thread, and it re-reads a database only when something has marked it since, keeping each database's last figure beside the running total. What a reading that old costs is overshoot bounded by what a millisecond of writes can allocate, which is well inside the tolerance this number already had, because space comes back a segment at a time and a segment is two megabytes. The server sitting at its limit is not judged on this reading at all: make_room takes its own exact one the moment the cached figure says the server is over.

Fixed

  • io_threads_active and io-threads report the real thread count. Both were written down rather than read. INFO server had io_threads_active:0 as a literal inside the format string and io-threads sat in the fixed settings table with the value 1, so a server started with --threads 4 told every client it had one thread and was not using it, while the # Threads section of the same reply reported four. All three places now read the count off the server. Redis means the configured count by io_threads_active rather than the number of threads with work in hand, and matching Redis is the reason the field exists, so that is what this matches. CONFIG SET io-threads keeps the rule the fixed settings follow: a write of the count the server already has is taken and everything else is refused as immutable.
  • A failed migration named the wrong state and the RDB channel always said none. A failure sentence named the state the task had just moved to where the reference names the state it was in when it went wrong, because the reference formats the message before the caller moves the state, and rdb_channel_state said none where the reference says completed once the snapshot has gone out.
  • CONFIG GET matched a name with regard to case. CONFIG GET MAXMEMORY answered nothing at all, which is the sort of thing a client library written against a real server trips over on its first connection. A real server hashes the exact name through a case insensitive dictionary and runs a pattern through its matcher with the nocase flag set, and that is what this does now. The name a setting is answered under is the one the client spelled when they spelled it out and its own name when they gave a pattern, so CONFIG GET MAXMEMORY answers MAXMEMORY and CONFIG GET MAX* answers maxmemory, which reads like a quirk and is really the shape of the reference's code.
  • SPOP with one member left crossed to a replica as DEL. A real server always sends SREM with the member it took and lets the far side notice it is holding an empty set. Only the form with a count sends DEL, and only when the count takes the whole set. This was wrong for ordinary replication and not only for a migration, and the new migration differential is what turned it up.

Known gaps

  • What the memory reading change is worth has not been measured. Both benchmark boxes are running the yo only sweeps the threading milestone needs and will be for hours, so a before and after against the same harness has to wait. Six percent is the ceiling it can recover, from the same cell reading 133578 sets a second with the limit off against 125214 with it on, on the eight core box at pipeline 50.
  • An overwrite only writes in place when the new value is exactly as long as the old one, because nothing in a record records how much room it occupies. A fixed length cell runs at about 550 thousand sets a second on the eight core box and the same cell at --data-size-range 1-1024 runs at about 250 thousand. That is the other half of the write path gap and it is open as #531.
  • A slot migration still has no last step: the far side claiming the slots over the bus and this node dropping the keys it no longer owns. There is no CLUSTER MIGRATION IMPORT either, which is a node asking to take slots rather than being asked to give them up.
  • 0.3.28 has a tag, no crates.io release and no GitHub release, from a probe that timed out on 8 September. It stays a gap rather than being published out of order behind 0.3.30 and 0.3.31.

v0.3.31

Choose a tag to compare

@github-actions github-actions released this 10 Sep 04:38
v0.3.31
c682c4b

Ten pull requests and no milestone has closed, so this is a patch.

Two things dominate it. Cluster mode arrives in four pieces, going from a server that knew nothing about any other server to a node that joins a cluster of real Redis 8.10.1 nodes, is treated as an ordinary member of it, and can hand a slot to a neighbour and take one back. And the write path stops being the slowest thing in this build: the arena was grow only between compactions, so every overwrite that changed a value's length left a hole nothing could ever use and a collector copying live records around it, which on the cache benchmark's own workload cost nine gigabytes copied to write three. Freed space now goes back into circulation directly, which is 2.47x the SET rate on the box that measurement was taken on. The rest is replication finishing its second half, a deliberate failover, and two fixes to how connections and work land across the I/O threads. Nothing here changes the on-disk format and no record kind was added.

Added

  • Cluster mode: slots, the routing table and the redirections. Every key belongs to one of 16384 slots, worked out from CRC16 of the key modulo 16384 with the hash tag rule on top, and every node knows which node owns which slot. There is no coordinator and no lookup service, so the whole of routing is that function and that table. The hash tag edge cases are all here because they are the only way a client has of making two keys land together, which is the only way a command naming two keys can run at all: a brace with no closing brace hashes whole, {} is not a tag, and only the first closing brace after the first opening one counts, so {a}{b} hashes a. The slot table is written out rather than computed at startup, because every key on the command path goes through it and a table in the binary is already in cache when the first command arrives. Beside it are two more tables for the slots that are moving, and a gate that runs after the ACL check and answers MOVED or ASK.
  • The cluster bus. The binary protocol nodes talk to each other over, on the client port plus ten thousand, in the wire format 8.10.1 uses. A 2256 byte header carrying the signature, the length, the protocol version, the sender id, its slot bitmap, its epochs, its replication offset, its flags and its state, then either a gossip section of 104 byte entries or a typed body. PING, PONG and MEET carry gossip, PUBLISH and PUBLISHSHARD carry a channel and a message, and FAIL, UPDATE and the failover messages carry what their names say. The extension block on a ping carries the shard id and the internal secret, both padded to eight bytes the way getAlignedPingExtSize does it, and the secret converges on the lexicographically smallest one anybody has seen. A listener thread and a cron thread on a hundred millisecond tick run it, and a yo node now joins a cluster of real Redis nodes rather than being a node that thinks it is alone.
  • Moving a slot between two nodes. RESTORE-ASKING, which is the third of a slot migration's four steps and the one that was missing: MIGRATE sent keys across and got MOVED back for every one of them, because the receiving node does not own the slot yet and a plain RESTORE arriving there is redirected straight back. The sender switches to that spelling on one condition and nothing else, whether it is a cluster node, so a MIGRATE between two servers not in a cluster still sends the ordinary word and still works against a server too old to have heard of the other one. The routing gate reads the command's own asking flag rather than knowing the name.
  • The internal connection. AUTH "internal connection" <secret> is not a login as a user: the secret is the forty characters the bus gossips until the whole cluster agrees on one, so a client cannot get past it without knowing something only the nodes know, and a server that is not a cluster node has no secret at all and says so before it looks at what it was sent. The name is compared exactly rather than as a keyword, which is the reference, so AUTH "INTERNAL CONNECTION" is an ordinary failed login and reads like one. CLUSTER SYNCSLOTS is what that opens, and a connection that is not a node gets the refusal and then gets hung up on.
  • Replication, the replica half. The other side of the master half from 0.3.30. REPLICAOF and SLAVEOF do it at runtime and --replicaof HOST PORT does it at startup, with --masterauth and --masteruser for a master that wants a password and --replica-read-only deciding whether an ordinary client may write while the server follows somebody, which is yes by default because a write that lands on a replica is one the master never hears about and the next full resync throws away. The link is one thread and one socket rather than a place on the reactor, because the handshake is a handful of blocking round trips and the snapshot is one very large read, and neither is the shape an event loop measured in nanoseconds per command wants. What arrives is a stream of ordinary commands and what runs them is the ordinary dispatcher through a session the link owns, which is the point rather than a shortcut: a replica applying writes through a second path would be a second implementation of every command.
  • FAILOVER, handing the master's job over on purpose. Everything else about replication is a machine that keeps running when a server dies, and this is the case where nobody has died and an operator wants the master somewhere else. Three steps and the order is the whole trick. The master stops accepting writes, not by closing anything but with the same pause CLIENT PAUSE arms, so a write that arrives is held on its connection and the client sees a slow command rather than a failure, and reads carry on. Then it waits for a replica to acknowledge every byte it has ever written, which is a wait that finishes because nothing is being written any more. Then it becomes a replica of that one and tells it to become the master in the same breath, with a PSYNC carrying a fourth word, FAILOVER, that a replica promotes itself on before answering.
  • The yo-cli serve bench can measure writes. It could only send GETs, and #518 was about SETs, so the one number the threading milestone was stuck on could not be reproduced on whatever machine is in front of you, which is the whole point of that bench existing. YO_BENCH_OP picks what the measured window sends, YO_BENCH_OP_AGAINST sets the far side of a pair so the ratio between a write and a read comes out of one paired measurement rather than two absolute ones taken minutes apart, and YO_BENCH_SIZE now takes a range as well as a number, so YO_BENCH_SIZE=1-1024 is memtier's --data-size-range and means the same thing here as in the harness. That last one is a different setting from a fixed 1024 rather than a longer one, and the difference is the point.

Changed

  • The arena reuses freed runs. Arena::free used to be a counter: it raised the owning segment's dead byte total and nothing else, so a freed run was space no allocation could land on again and the only way space came back was compaction, which empties a segment by copying every live record out of it. At the quarter it starts from that is three bytes moved for every byte reclaimed. A freed run now goes on a list of runs of exactly its size and the next request for that size takes it back, with no copying, no index write and nothing left for a collector to do. The links live in the freed run itself, because a vector per size class would grow and growing is a call to the system allocator on a command path, which Y7 forbids. Runs under 32 bytes have nowhere to keep the links and stay off the lists. Doubly linked, so that a segment being reclaimed can take its runs off in constant time, which compaction now does as it walks. INFO memory gains mem_arena_listed, the number of runs waiting on a list, which beside mem_compact_bytes says which of the two collectors is doing the work.
  • Connections are shared out by how many each thread is already holding. Every worker has the listeners in its own poller and keeps a connection for as long as it is open, so how connections land is how work lands. A worker that has just accepted is awake and running while the others are coming back from a wait, so it wins the door again and again: at sixteen threads over a unix socket with 256 connections, one thread took 215 of 771 accepts and another took 22, and the command counts follow the accept counts exactly. A worker now steps out of the queue for the doors once it is holding more than the fewest any of them is holding, and steps back in when it is level, which turns a burst into round robin with no turn counter anywhere.
  • The load generator's per thread times are reported. Every run of the sweep records how long each of its threads ran for, and the ratio of the longest to the shortest is now in the output. That is what made the imbalance above visible rather than inferred: yo sat at 1.00 on one thread and was the loosest engine on the board above one, at 1.68 on eight and 1.80 on sixteen against 1.00 to 1.08 for everything except rugo, and individual runs were worse than the medians at 12.75 and 8.05.

Performance

  • 2.47x the SET rate on the cache benchmark's own workload, and ten percent less resident memory. Development measurement, not a gate number. Eight core AMD EPYC virtual machine, 24 GB, server pinned to cores 0 to 3 and memtier 2.5.1 to cores 4 to 7, two I/O threads, pipeline 50, 2.5 million keys, values drawn from 1 to 1024, --maxmemory 8gb. Before, 138257 sets a second at 1529 MB resident with 7200775196 bytes copied by compaction over the measured pass. After, 341955 sets a second at 1383 MB with compaction not running at all, mem_compact_walked and mem_compact_bytes both exactly zero. Paired three times alternating, 120527, 113961 and 132809...
Read more

v0.3.30

Choose a tag to compare

@github-actions github-actions released this 09 Sep 18:04
v0.3.30
ed59470

Eight pull requests and no milestone has closed, so this is a patch.

The headline is that this build is a master now. A real redis-server 8.10.1 told to replicaof yo attaches, takes a snapshot, follows the command stream and ends up holding the same keyspace, and it stays that way across a broken link because partial resync works too. Two more of the metadata commands land beside it, being the rest of the ACL and the whole of MEMORY, which is the last of M8's looking commands. The other four are keeping the tree buildable and measurable, which is one real throughput fix on many threaded servers, one more pass at the Miri run, one release job that gave up too easily and one Windows test that was asserting a sentence from another operating system. Nothing here changes the on-disk format and no record kind was added.

Added

  • Replication, the master half. REPLCONF, PSYNC and SYNC are new commands, and PSYNC is where a connection stops being a client: it gets +FULLRESYNC <replid> <offset>, then the snapshot as a bulk string with no newline after it, and from then on the socket carries commands and is never replied to again. Everything that connection was holding as a client goes at that moment, being an open transaction, the keys it was watching and any subscription, because all three are promises of a reply and a stream has no way left to keep one. Answering a REPLCONF ACK with an +OK puts a reply into the middle of a byte stream the replica is parsing as commands, which a real replica reports as Protocol error (Master using the inline protocol. Desync?) before it drops the link, so the silence there is the protocol rather than an optimisation. What crosses the link is the effect and not the command, which is the reference's own rewrite table followed case by case: a relative EX crosses as the absolute PXAT it resolved to, SPOP crosses as the SREM it turned out to be, INCRBYFLOAT crosses as a SET of the answer, a key a read noticed had expired crosses as a DEL ahead of whatever the read was doing, and a script crosses as each inner write on its own rather than as the EVAL. SELECT is injected lazily, exactly when the database a command is on is not the one the stream is already pointed at. WAIT counts replicas that have acknowledged rather than answering zero.
  • MEMORY USAGE, STATS, DOCTOR, PURGE, MALLOC-STATS and HELP, which is the last of the metadata commands on M8 and closes the last four COMMAND GETKEYS differences. USAGE adds up the record in the arena, the body hanging off it for a type that has one, and one key's share of the index, which is the same three things a real server adds up under different names, so the shape of the answer matches even though none of the three numbers does. It is exact rather than sampled, because every collection already keeps its own byte count for maxmemory, so SAMPLES is parsed and checked and then changes nothing, which is D-6. A value that has been written out to the file is counted as the record alone, because that is all of it that is in memory, and it is deliberately not read back in to answer, since asking what a key costs should not be the thing that makes it cost that. STATS answers the same 36 fields in the same order plus a row per database that is holding a key, and the fields that are zero are zero either because nothing is behind them yet or because of a real design property, there being no expires table when a deadline lives in the record and no half rehashed index. DOCTOR reports five of the reference's eight complaints, in the reference's own wording, and leaves out the three that need a resident set size or a replica rather than shipping a complaint that can never fire.
  • ACL LOG, ACL LOAD and ACL SAVE, which is the rest of the access control list. LOG is a ring buffer of refusals, newest first, at most acllog-max-len of them, written by every refusal the gate makes and by a failed AUTH with the name that was tried rather than the name the connection is on. Two refusals merge into one row when the reason, the context, the object and the user all match and they are within sixty seconds of each other, and the scan that looks for a match reads at most the first ten rows, which is the reference's rule and is why an eleventh distinct refusal can stop a match being found. The log lives on the server with its own lock rather than inside the user table, because the two are touched at opposite moments, so a server whose users can do what they ask never takes the log's lock at all. SAVE writes one user a line in name order to a temporary file beside the real one, fsyncs it, renames it and fsyncs the directory, which is what makes a half written ACL file impossible, and LOAD stages every rule off to the side so a file with a mistake anywhere in it changes nothing at all and every complaint it has comes back in one sentence. yodb takes the file as --aclfile, and a server that was not given one answers both with the reference's paragraph about ACL SETUSER and CONFIG REWRITE.

Changed

  • Each thread gets its own compaction cursor. Chasing the maintenance slice's nine per cent on issue #334, the paired attribution on the 32 thread box said DEBUG PAUSE-CRON 1 is worth 1.09x at 16 threads while neither of the two jobs inside the slice that have their own knob is worth anything measurable, which is what sent me to read the slice rather than the jobs. Three of its four steps cost one relaxed load on a default server. The fourth stored Server::next_db on every batch whether or not the walk found anything to move, because the store is how the next walk is told where to start, and a walk that finds nothing is nearly every walk on a server that is keeping up. So it was sixteen writers on one cache line at batch rate, with a cost that goes up with the thread count rather than staying still, which is the shape the attribution table has.
  • The last four yo-kv tests over three minutes interpreted now run in under twenty seconds each, which is the fourth pass at the bar issue #499 set and the same rule as the three before it. A count that is the claim of a test gets skipped under Miri with a reason naming the count, and a count that is only a way of reaching a state gets smaller with the ratios held. The one worth looking for first is the fourth, at 204s to 5s with nothing cut at all: it was slow because the helper building its test bytes ran a modulo per byte over about eight hundred thousand bytes, and it now builds one cycle of two hundred and fifty one and repeats it, so the bytes and every size the test walks are unchanged.

Fixed

  • Partial resync never happened, so the backlog was doing nothing at all. The offset a replica sends with PSYNC is the position of the first byte it wants counted from one, so a replica that has everything asks for one past the end of the stream, and everything on this side counts bytes written. Reading one as the other put every request past the end of the backlog, and the replica log said so plainly once you look for it, logging Trying a partial resynchronization (request <id>:128272) and getting Full resync from master: <id>:128271 back. repl_backlog_first_byte_offset in INFO was off by the same step and is now reported the way a replica counts, being zero when there is no backlog rather than the one that would be the first byte of the one that was never made.
  • CLIENT KILL TYPE replica returned zero and connected_clients counted the replica. The client type filter was written when there were no replication links, so the word matched nothing. A replica is now its own type, normal and pubsub exclude it the way getClientType does by asking its questions in order and stopping at the first yes, and connected_clients is sockets minus replicas.
  • A release that could not reach a registry gave up on the first try. cargo xtask reserve verify is the gate the release runs before it publishes anything and it exits 2 when it could not get an answer rather than pretending the name is fine, which is the right call and stays. On 8 September one read against search.maven.org timed out and the v0.3.28 release stopped there, so 0.3.28 has a tag and no crates.io release and no GitHub release, and nobody noticed for a day because the failure was in a job there is normally no reason to read. It now asks three times, two seconds then four between them, and only for a request that did not come back at all, because an HTTP status is a verdict including a 404 and a 403 and asking again would get the same one.
  • A test asserted that errno 2 reads No such file or directory, which it does out of a C library and does not out of Windows, where it reads The system cannot find the file specified., so test (windows-latest) went red on main. The function under test is fine and portable, since all it does is take off the (os error 2) that Rust adds to the system's own sentence, so the test now asserts the two things that are true everywhere and keeps the exact C sentence under cfg(unix), which is the platform the reference runs on and where matching it word for word is the point.

v0.3.29

Choose a tag to compare

@github-actions github-actions released this 09 Sep 08:17
v0.3.29
b478308

Eight pull requests and no milestone has closed, so this is a patch.

Two of them are command surface and they are one piece of work in two halves: the key specs Redis added in 7.0, and then the access control list that could not be built without them. That is the biggest part of M8 that was still missing, and a server that had one password and one user holding it now has as many users as an operator cares to write. The other six are about being able to build, measure and test this thing at all. Three are the throughput bench learning to refuse a machine it cannot trust, which matters because every box available this week has somebody else on it, two are the nightly Miri run, which had stopped finishing, and one is a link error on Windows. Nothing here changes the on-disk format and no record kind was added.

Added

  • Key specs on every command row, COMMAND GETKEYSANDFLAGS, and a key finder the ACL can use. The legacy first, last and step triple was never enough to say where a command's keys are. ZUNIONSTORE dst 2 a b has three keys and the triple names one, XREAD COUNT 2 STREAMS a b 0 0 has two and the triple names none, SORT k STORE d has two and the triple names one. That was survivable while the only caller was COMMAND GETKEYS, and it stops being survivable at the ACL, because a user given ~cache:* must not reach secret through ZUNIONSTORE, and a permission check built on a key finder that misses keys is not a permission check. The resolver is getKeysUsingKeySpecs line for line, including the three rules that read as mistakes and are not: a run reaching past the last argument is a syntax error rather than a shorter run, a keyword search stops one argument short of the end because a keyword in the last argument has no key behind it, and a spec that says it is incomplete throws away the whole answer rather than half of it. The specs are tried first and a per command finder is what is left when they fail, which is the order getKeysFromCommandWithSpecs goes in, and PFMERGE dst is the case that proves the order matters, since its source run starts at argument two of a two argument command.
  • Users, the ACL rule language and a gate in front of every command. A user is a name, a switch, a list of SHA-256 password hashes and one or more selectors, and a selector is a command bitmap with a list of key patterns and a list of channel patterns beside it. That is the reference's model down to the reserved bit that records whether a command added tomorrow would be allowed, which is what makes +@all -get and -@all followed by the other four hundred names two different users rather than one: they describe back differently and they behave differently the day a command is added. Every selector also keeps its rules as the text they were written in, because ACL LIST and ACL GETUSER have to hand back something SETUSER will take again, and a bitmap cannot say whether what was written was a category or the forty names inside it. The rule language is ACLSetUser and ACLSetSelector written out, refusals included, each wrapped in the sentence acl.c wraps them in. Ten subcommands ship, being SETUSER, GETUSER, DELUSER, LIST, USERS, WHOAMI, CAT, GENPASS, DRYRUN and HELP, and ACL HELP does not name the three that are still to come, because a client reading the help to find out what it can send should not be told about a subcommand this server would refuse as unknown. A SETUSER is staged on a copy and applied only if every rule worked, which is the reference and which matters because an operator tightening a user and mistyping one rule would otherwise be left with the new restrictions and none of the new grants. The gate sits where processCommand puts it, after the refusal for a command that cannot go inside a transaction and before the memory limit, and on a server whose users can all do everything it costs one relaxed load. requirepass is now one rule on the default user rather than a password of its own, and ACL GENPASS takes its bytes from /dev/urandom or BCryptGenRandom rather than from the engine's seeded generator, which closes D-127.

Changed

  • The yo-cli throughput bench now refuses a machine it cannot trust. A ten core laptop with a virtual machine, two builds and a Python job on it answered 3220 Kops at one thread where the same build on the same laptop had answered 6417 an hour earlier, and nothing in the output said so. A number like that does not look wrong, it looks like a regression, and acting on it is worse than having no bench at all. Every cell is measured three times, what gets reported is the median and the coefficient of variation across the three, and a run with cells over five percent says in as many words that nothing in it is worth quoting. The median rather than the mean, because a run that lost the machine to something else is an outlier rather than evidence, and the spread beside it because the whole point is that it is reported instead of being averaged away.
  • YO_BENCH_AGAINST turns that bench into an A/B against a second yodb binary, which is what makes it answer on a busy box instead of only refusing one. Three machines in a row had refused it. Each repeat measures the cell twice, once with each binary, back to back and in alternating order, and the ratio is what gets reported. The thing that makes it work is not the pairing, it is the median over enough pairs: a pair cancels slow drift and does not cancel a compiler taking half the cores for six seconds of one half of it. Measured with the binary against a copy of itself on a laptop at a load average of ten, where the true answer is 1.00x, three pairs said 1.16x and eleven pairs said 1.02x, while the absolute throughput moved by a factor of two between the two runs. A cell is judged on how many of its pairs agree rather than on the spread.
  • YO_BENCH_DEBUG_AGAINST sets a DEBUG knob on one side of a pair only. The attribution table that says what each background job costs was four separate runs of the same build with a different job turned off in each, which is why it was taken on a quiet machine and why it could not be taken again on a busy one. Worse, YO_BENCH_DEBUG reached both halves of a pair, so a paired attribution compared a build against itself with the same job turned off on both sides and read 1.00x by construction. Unset, the far side is set up the same way the near side is, which is what a comparison of two builds wants and is what happened before. Set, including set to nothing, the two sides differ by that and by nothing else, and both settings are printed in the header so a number pasted into an issue carries what it was measured with.

Fixed

  • The nightly deep run failed on three days running, for two reasons that were not bugs in the server. Net::held in yo-cli is cfg(all(test, unix)) and its only caller is cfg(all(test, not(miri))), so under Miri the caller went, the method stayed, and a shard built with -D warnings could not get as far as listing its tests. And one yo-resp test runs an EVAL, which needs a Lua state, which is C. Every other test in the tree that touches Lua already carries the same ignore attribute with the same words, and this one was written without it.
  • The yo-kv Miri shards ran past their forty minute timeout. Interpreted, a member of a sorted set costs somewhere near a second to build against about fifteen milliseconds for a member of a plain set, and that gap is why counting less was not enough on its own: a sorted set test needs a count past the listpack band or it is testing the packed case instead, and a hundred and twenty nine members at a second each is over two minutes before the test does anything. So where a test crosses a band rather than counts to a number, the band comes down under Miri and the counts either side of it come down with it, which leaves the same boundary in the same code with a twelfth of the members to carry over it. Three tests over a demoted body went from over five minutes each to under three seconds that way. A third pass timed the tests that reading the source had missed, because a bound written as 4_000usize has no word break in it and a bound that is a band is not a large number at all, and found ten more between a hundred and six hundred seconds which now run in six to twenty one seconds. Every one of the eight shards now finishes between eight and twenty one minutes. The rule behind it is in deep.yml: a count that is the claim of a test does not get smaller, it gets skipped with a reason naming what the count is for, and a count that is only a way of reaching a state does get smaller, with the assert that would go vacuous checked by forcing the Miri branches on and running the suite compiled.
  • The Windows build stopped linking the moment BCryptGenRandom got a caller. The library it lives in has to be named, and nothing else in the tree pulls it in. The standard library used to, back when its own generator was this same call, and it has since moved to ProcessPrng in another library, so a call that linked by accident stopped linking when the toolchain caught up. One attribute fixes it. It is worth writing down because of how it fails: the error is at link time, only on the MSVC target, which is not a target a person developing this is usually on, so the first thing that saw it was CI on the release branch.

v0.3.27

Choose a tag to compare

@github-actions github-actions released this 08 Sep 06:24
v0.3.27
e08d56d

Seven pull requests and no milestone has closed, so this is a patch.

Most of it is M8 compatibility work: MONITOR, the DEBUG container, the five persistence and role commands, and a password. The one to read if you run more than one thread is the accept fix, which is a one line change to the server loop that decides which thread a connection lands on. A file written by 0.3.26 opens unchanged under this version and a file written by this version opens under 0.3.26. No record kind was added.

Fixed

  • yodb serve --threads 16 frequently ran a whole benchmark on one thread. Every worker has the same listener in its own poller, and a worker that was woken looped on accept until the door said nothing was waiting. Clients do not arrive one at a time. A benchmark opens its two hundred and fifty six connections at once, every worker is idle at that moment, and whichever one won the wakeup drained the entire backlog into itself while the rest found nothing. A connection belongs to the thread that accepted it for as long as it is open, so the split was decided once, by a race, at the only moment when every thread was idle. A worker now takes one connection per ready event and goes back to the poller. The doors are level triggered, so a door with more waiting is ready again immediately and the next worker round the loop takes the next one. The cost is one extra wakeup per connection, paid once per connection rather than once per command. It was found in a cache-bench sweep on a 32 core box rather than by reading: GET throughput at eight and sixteen threads had a coefficient of variation between 0.40 and 0.77 where four of the six rival servers in the same cells on the same box sat between 0.00 and 0.02, and sixteen threads came out slower than eight. That makes those numbers measurements rather than good, and the re-measurement is still to come.
  • COMMAND INFO was sending arrays where the reference sends sets, for the flags, the acl categories and the three empty fields on the end. It only shows on RESP3 and it showed on every command in the table. It turned up because the compare written for the DEBUG work looks at the wire bytes rather than at what a client decoded them into, which is the whole argument for writing compares that way.
  • modularity panicked on a grouping whose labels ran above the node count. Any grouping of the nodes is meant to be accepted, since the labels mean nothing beyond which nodes share one, but the renumbering step sized its table by the node count and then indexed it by the label. Every caller inside the crate hands over labels that are already below the count, so nothing internal ever saw it, and a caller who numbered their groups by a hash or a database id hit it on the first call. Sizing by the largest label would let one of those callers ask for sixteen gigabytes, so the labels are ranked instead, and only when they need to be: the check is one pass, the fast path is what a level of Louvain runs twice a level, and the other path costs a sort of the distinct labels and a binary search a node.

Added

  • MONITOR. Every command the server runs, echoed to whoever asked for it, in Redis's format byte for byte: the time to the microsecond, the database and the address in brackets, then the command and every argument quoted the way sdscatrepr quotes them. A simple string on RESP2 and RESP3 alike rather than a push, which is why redis-cli monitor works against either. The database reported is the one the connection is on after the command ran. A command that never reached its body is not on the feed and a command that failed inside its body is. The six script commands are reported before they run rather than after, because a script's own calls come back through the same place and a script whose effects arrived in front of the EVAL that caused them would be unreadable. Administrative commands are kept off it per subcommand rather than per container, off 8.10.1's own table, so CLIENT ID is on the feed and CLIENT LIST is not. The line is rendered once however many monitors are watching and handed to each monitor's thread through the mailbox the pub/sub path already uses, so the feed never writes into a reply buffer another thread owns. A server nobody is watching pays one relaxed load of a zero per command. A monitor may not touch the keyspace, which reads like an accident of Redis's implementation and is load bearing, since a monitor is exempt from CLIENT PAUSE and would otherwise have a way around its own pause that nothing else has. Seventy cases against 8.10.1 on the same machine, zero differences.
  • The DEBUG container, eleven subcommands of it. DEBUG is the command a test suite talks to rather than a client, and Redis's own suite leans on it hard enough that a lot of it does not run at all against a server that has none. This is the half that is about this server: HELP, PROTOCOL, ERROR, LOG, SLEEP, POPULATE, SET-ACTIVE-EXPIRE, PAUSE-CRON, DICT-RESIZING, SET-SKIP-CHECKSUM-VALIDATION and QUICKLIST-PACKED-THRESHOLD. PROTOCOL is the one client libraries reach for, since it is the only way to make a server send a type on purpose, and all thirteen names answer the exact bytes 8.10.1 answers on both protocols, including the two nobody guesses: on RESP3 attrib is the attribute followed by a string and push is the string first and the push second, and on RESP2 push is the only one of the thirteen that comes back as an error. A knob that is remembered and read by nothing is worse than no knob, so three of the four gate something real: SET-ACTIVE-EXPIRE gates the background expiry sweep without making an expired key readable, PAUSE-CRON gates the whole maintenance slice the shard loop runs between batches, and DICT-RESIZING gates arena compaction, which is the nearest thing here to the dictionary resize a real server turns off, both being the background reclaim of room a table no longer needs. All four are read the reference's way, so anything unreadable means nought and a test that sends the word true to one of them has turned it off. 196 cases against 8.10.1 over both protocols, inside MULTI, through a monitor and with the notification channel open, 194 matching byte for byte, and the two that do not are one older gap seen once per protocol.
  • SAVE, BGSAVE, BGREWRITEAOF, LASTSAVE and ROLE. The snapshot writer already produced a real RDB image for BACKUP, so the work was putting it behind the commands a client reaches for and making the file land safely: the image is written to a temp file, synced, and renamed over dump.rdb, so a reader never sees half a file. A real redis-server starts on the file yo writes and answers all fourteen probes identically across every type, and redis-check-rdb reads it and says it looks OK. BGSAVE writes the file before it answers rather than forking, so no save is ever in progress, and BGREWRITEAOF counts itself and writes nothing because there is no append only file. INFO persistence reports the twelve fields a monitoring rule reads and leaves out everything about a fork or a load, on the principle that a missing field is a client falling back rather than a client believing a zero.
  • AUTH, requirepass and the NOAUTH gate. A server can be given a password with CONFIG SET requirepass or the new --requirepass flag on yodb serve, and a connection that has not authenticated is refused everything. There is one password and it belongs to a user called default, because requirepass on a real server is a thin layer over the ACL. Any other user name is refused with the same WRONGPASS a wrong password gets, since the difference between a name that does not exist and a password that was wrong is a list of user names. The rule worth reading twice is who starts out let in: the flag is decided when the connection is accepted rather than when it sends its first command, so setting a password does not lock out the clients already connected, including the one that just set it, and does lock out everything that connects after. That is the reference's behaviour and it is not what anybody would guess. The compare is written out byte by byte with the lengths folded in rather than returned on early, so how long a guess took does not say how much of it was right. A monitor is shown AUTH with every argument after the name replaced, the user name included, because a user name a client got wrong is very often a password typed one field up.

Changed

  • The thirty tests that open a Lua state are skipped under Miri, which is what every one of the six yo-resp shards in the nightly deep run was failing on. A Lua state lives inside LuaJIT, which is C compiled by a build script, and Miri interprets Rust, so it stops at the first call across. There is nothing to shrink and nothing an interpreter could check even in principle, and the reason string on these says so rather than borrowing the wording from the counts that were made smaller for the same run, so a reader does not come away thinking this one could be shrunk too. The list came out of a run rather than out of reading names, which matters because one of them reads as though it stops short of Lua and runs a script at the end. Native runs are unchanged, and everything on this side of the boundary is still interpreted, including the five script libraries that are Rust here rather than the C a real server links.

v0.3.26

Choose a tag to compare

@github-actions github-actions released this 07 Sep 12:58
v0.3.26
c58bf17

Seven pull requests and no milestone has closed, so this is a patch.

Most of it is M8 compatibility work on the CLIENT container, which was missing entirely and is now thirteen subcommands, plus the two INFO counters every dashboard reads and the active half of hash field expiry. The rest is test work that puts the last two crates back under Miri. A file written by 0.3.25 opens unchanged under this version and a file written by this version opens under 0.3.25. No record kind was added.

Added

  • The CLIENT container, thirteen subcommands of it. ID, GETNAME, SETNAME, SETINFO, INFO, REPLY, NO-EVICT, NO-TOUCH and HELP are about the connection that asked and needed nothing new to answer. LIST, KILL, PAUSE and UNPAUSE are about every connection on every thread, so they come with a registry of live connections that the accept path pushes to and the close path lifts out of, in the order they opened in, which is the order CLIENT LIST reports. CLIENT INFO is forty fields read off 8.10.1 one at a time, in its order and with its spelling, because the tooling that parses this line splits on spaces and looks for names it knows. Nine of them describe memory a real server holds in a shape yo does not and are answered with yo's own honest numbers, which is D-123 along with TRACKING, the one part of the container still missing.
  • CLIENT PAUSE and CLIENT UNPAUSE, which stop the server rather than a connection. The state is one word on the server, a deadline and a mode bit, so the command path pays a relaxed load and a test against zero when nothing is armed. Arming widens and never narrows: the later deadline wins and the stricter mode wins, so a short pause on top of a long one does not cut it short. A held command has not run at all, and the connection keeps it and everything pipelined behind it in order until the pause ends, the same way a blocking command keeps them. Nothing is exempt, including CLIENT UNPAUSE itself under an ALL pause, which reads like a bug and is what 8.10.1 does, measured on the wire rather than assumed.
  • keyspace_hits and keyspace_misses in INFO stats. These were the hole in a section that already carried every other counter a dashboard reads, and the hit rate everyone quotes is the first over the sum. A real server counts them in lookupKey, next to the keymiss notification and skipped for the same reasons, so the keys it misses are the keys it says keymiss for. The counting happens where the lookups already are rather than in the walk the notification uses, because that walk costs a stripe lock and a map probe per key and the notification is off nearly everywhere while these two are always on. A command that reaches its lookup more times than a real server does only counts the first, so ZRANGE asking for the window and then walking it is one read and not two.
  • expired_subkeys and expired_subkeys_active in INFO stats, alongside the active reap below.

Fixed

  • A hash field with a deadline waited for somebody to read the hash. A field given a deadline with HEXPIRE went when a command next touched the key and not before, so a hash nobody read held every field it had been told to drop, and a client subscribed to hexpired heard nothing at all. The reap now names every field it took and the dispatch layer puts the per field news back into the one event a real server publishes, on all four subkey channels, with del after it when the hash is left empty. The active side is its own cycle with its own list, because a field deadline lives inside the hash body where the marked index the key sweep draws from cannot see it. A name goes on that list once, when its hash first takes a field deadline, and comes off when the key is gone or is no longer a hash. A hash whose earliest deadline has not passed costs a load and a comparison, and a server that has never sent HEXPIRE has an empty list and pays nothing.

Changed

  • yo-graph and yo-doc are back in the deep Miri run, which was the last of the five crates that did not fit. yo-graph went from at least 5731 seconds with 38 tests still running when the cap cut them off, to 162 tests in 666 seconds. yo-doc went from 27 tests to all 153 of them in eighteen minutes, single threaded in both cases. Sixteen tests are skipped between the two and every one says why where it is skipped: twelve of them are a measurement rather than a shape, and four sit exactly on a compile time limit that has no runtime knob. Every size that came down was checked by forcing the Miri branches on and running the suite compiled, which is what catches a cut that leaves an assert looking at nothing, and that check turned up a real bug in modularity on a label above the node count, which is issue 462 and is not fixed here.

v0.3.25

Choose a tag to compare

@github-actions github-actions released this 07 Sep 09:24
v0.3.25
02c3a94

Four pull requests and no milestone has closed, so this is a patch.

All of it is M8 compatibility work on keyspace notifications, which finishes the classes: the two that say a value was thrown away, the six commands that had been writing in silence, and the class that says a read found nothing. A file written by 0.3.24 opens unchanged under this version and a file written by this version opens under 0.3.24. No record kind was added.

Added

  • The overwritten and type_changed classes. A write that throws away the whole of what was under a name now says so, and says the kind changed when it did. The line is between replacing a value and reaching into one rather than between kinds of write, so SET, MSET, GETSET and every store form are on one side of it and APPEND, SETRANGE, INCR, RPUSH and SADD are on the other, the same way round as Redis 8.10.1. RENAME and COPY say the pair last where everything else says it first, on both sides and for the same reason, which is that they take the destination away and put another key in its place.
  • The keymiss class, which is a read saying it found nothing. A real server fires this from inside lookupKey, which covers every command at once because every read there goes through one function. There is no such funnel here, so the question is asked in front of the command out of a table of which of a command's arguments are keys it is going to read: the read only commands whose values live in the keyspace, the forms whose keys hide behind a count, the store families' sources rather than their destinations, the two stream reads whose keys sit after STREAMS with XREAD looking at each of them twice, and the dozen writes that read something before they change it. The module commands are in it too, since the module API's own key opener goes through the same lookup, with the handful a real server keeps quiet measured rather than guessed: TS.INFO, CF.COMPACT and the whole of the search group bar the two suggestion dictionary reads. Three hundred and twenty cases agree with the reference across the flag settings.
  • Six commands that had been writing in silence now say what they did. SETBIT and BITFIELD say setbit, PFADD and PFMERGE say pfadd, GEOADD says zadd on the sorted set class, and both writable GEORADIUS forms and GEOSEARCHSTORE say their own names. The rule they share is a dirty guard rather than the fact that a write ran, so SETBIT k 1 0 on a bit that was already zero says nothing while SETBIT k 1000 0 on a short value says it, and neither half of that is readable from the reply.

Fixed

  • MOVE and COPY ... DB were putting the news of a new key on the wrong database. Both were publishing on the database the connection was on rather than the one the key landed on, which nothing caught because the birth notice comes from a layer with no database number to hand. A client subscribed to __keyevent@1__:new now hears about a key moved into database one.
  • Three commands were treating a key arriving and a value changing as the same thing. RENAME, COPY and RESTORE wrote over the destination's body where they should take the whole record away first, and SORT ... STORE took the record away where it should keep it, so all four said the wrong thing about whether the destination was new.

v0.3.24

Choose a tag to compare

@github-actions github-actions released this 07 Sep 06:49
v0.3.24
7c19aa8

Seven pull requests and no milestone has closed, so this is a patch.

All of it is M8 compatibility work, and almost all of that is keyspace notifications: the six collection classes, the four subkey channels Redis 8 added, and the three events a client hears about that no command's reply covers. A file written by 0.3.23 opens unchanged under this version and a file written by this version opens under 0.3.23. No record kind was added.

Added

  • The list, set, sorted set, stream and hash notification classes. A client subscribed to __keyspace@0__:mykey or __keyevent@0__:rpush now hears about every write those five groups make, which takes the setting from the three classes 0.3.23 shipped to all of them. Four hundred and eleven cases were run against Redis 8.10.1 over a raw socket under twenty settings of the flag, comparing every channel and payload in the order they arrived and the replies alongside them, and they agree on all of them. Most of the work in a class is deciding when to stay quiet, because the reply and the notification answer different questions: LTRIM says so even when nothing moved, SMOVE onto the set the member came from says nothing, ZADD on a member already at that score is not a write, and HGETEX ... PERSIST replies the value whether or not there was a deadline to take off.
  • The four subkey channels Redis 8 added, which is __subkeyspace@0__, __subkeyevent@0__, __subkeyspaceitem@0__ and __subkeyspaceevent@0__. They carry the hash fields a command touched, with the length prefixed comma joined field list a real server sends and the same two rules about a key or an event name that would not read back apart, so a client that cares about one field of a large hash can subscribe to that field rather than to the key.
  • Keys that reached their deadline and keys an eviction took now say so. These are the first events here that no command asked for, so they leave yo-kv on a hook rather than from a call site, and neither of them drags a del along behind it, which is what a real server does and is easy to get wrong. The sweep reports the same way the lazy path does, so a key nobody read back still says it went.
  • A key coming into being now says so too, on the new class that Redis keeps out of A on purpose. It goes out on the same hook and from the one function underneath every group that puts a record in the map, which is what puts it in front of the write that caused it without anything having to be told what that order is. Eighty two cases covering every way a key can be created were run against 8.10.1, including RENAME, COPY, MOVE, RESTORE, SORT ... STORE, MSET and the store forms.

Changed

  • EXEC keeps its decode buffer on the session rather than building one per queued command, so a transaction of a hundred commands allocates once instead of a hundred times.
  • The allocation gate prints the frames when the site is not in this repository, which turns a failure that named a file nobody here wrote into one that shows the path from our code into it.

Fixed

  • The active expire cycle had never run in the deployed server. The sweep hung off a maintenance slice that the serve loop does not use, so a server that wrote under a deadline and never read the keys back held every one of them until somebody looked. It now runs from the housekeeping call the loop does make, gated to once a millisecond and to a slice of keys, and two hundred keys written with PX 60 go from DBSIZE 200 to 0 inside two seconds on an idle server. Anybody who set a deadline and watched memory not come back was hitting this.

v0.3.23

Choose a tag to compare

@github-actions github-actions released this 07 Sep 04:18
v0.3.23
7c698ec

Four pull requests and no milestone has closed, so this is a patch.

All of it is M8 compatibility work on the connection side: transactions, pub/sub and the keyspace notifications that ride on pub/sub. A file written by 0.3.22 opens unchanged under this version and a file written by this version opens under 0.3.22. No record kind was added.

Added

  • Transactions, which is MULTI, EXEC, DISCARD, WATCH and UNWATCH. All five are in and verified against Redis 8.10.1 over sixty six scripted cases plus a second round of twenty, covering what gets queued, what refuses to be queued, what an error inside a queue does to the rest of it and what a watch on a key that moved does to the EXEC that follows. A queued command is never half run: each one holds every stripe it needs for as long as it needs, so a client cannot see one command of the queue partly applied.
  • Pub/sub, which is SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, SSUBSCRIBE, SUNSUBSCRIBE, PUBLISH, SPUBLISH and PUBSUB. All nine are in and verified over fifteen RESP2 cases and five RESP3 cases at no differences. The three namespaces are kept apart the way a real server keeps them apart, so a channel, a pattern and a shard channel can share a name and hear none of each other. Getting a message from the thread that published it to the thread that owns the subscriber goes through a mailbox per thread, and a publish allocates the body once behind an Arc however many subscribers it reaches, so a subscriber costs nothing until there is one.
  • Keyspace notifications, which is notify-keyspace-events and the two channels it publishes on. A client subscribed to __keyspace@0__:mykey or __keyevent@0__:del hears about every write the keyspace, string and bitmap groups make. Eighty two cases were run against 8.10.1 under five settings of the flag, comparing every channel and payload in the order they arrived, and they agree on all of them. A server with the setting off pays a load and a branch per call site and nothing else, and so does a server with the setting on and nobody listening.

Changed

  • The command table's multiplier was searched again. Nine new command names took the table to 420 and the worst probe to two slots, so a twentieth search ran over four billion candidates and put it back to one slot for every name at twenty four extra probes.
  • yo-kv is back in the Miri run, which it had dropped out of.