Found while fixing #570 (PR #591). #570 was scoped to BLMOVE/BRPOPLPUSH; the same routing rule breaks every two-key write whose destination is not the routing key, and #591 deliberately fixed only the LMOVE family.
Mechanism
A keyed command is routed by extract_primary_key, which returns args[0]. The owning shard of that ONE key then executes the whole command against its own slice. For a command that also WRITES a second key, the write lands in the routing key's shard under the second key's name — invisible to every normally-routed read of that name, which goes to its owner.
Measured
Merge-base binary fc60836c, --shards 4, 12 key placements each, FLUSHALL between trials. "LOST" = the command reported success and the destination is not visible afterwards.
| command |
dst visible |
refused |
LOST |
last reply |
RPOPLPUSH a b |
1 |
0 |
11 |
m (element gone from both keys) |
SMOVE a b m |
1 |
0 |
11 |
1 (member gone from both sets) |
RENAME a b |
1 |
0 |
11 |
+OK (key gone from both names) |
SINTERSTORE b a |
1 |
0 |
11 |
0 (source intact, dest written to wrong shard) |
ZRANGESTORE b a 0 -1 |
1 |
0 |
11 |
0 (source intact, dest written to wrong shard) |
RPOPLPUSH is fixed by #591. The remaining four are not.
Two distinct severities:
- Acked data loss —
SMOVE, RENAME. The value leaves the source and is not readable at the destination. RENAME is the sharpest: +OK, and the key exists under neither name.
- Wrong result, no loss —
SINTERSTORE, ZRANGESTORE. The source survives; the destination is written to the wrong shard's table, so a later read sees nothing and the count returned is computed from a source the executing shard could not see either.
Not exhaustive. The same shape is expected for at least RENAMENX, SUNIONSTORE, SDIFFSTORE, ZUNIONSTORE, ZINTERSTORE, ZDIFFSTORE, GEORADIUS ... STORE/STOREDIST, GEOSEARCHSTORE, SORT ... STORE, BITFIELD-adjacent stores and LCS ... IDX-adjacent forms — every one of these should be measured, not assumed.
COPY is NOT affected: it is in is_multi_key_command and coordinated by coordinate_copy, which hops to the destination's owner (degrading to an error for non-string values). BITOP, MSET, MSETNX, MGET, DEL/UNLINK/EXISTS are likewise coordinated.
Suggested direction
A per-command guard for each of these repeats #591's work N times. The systemic version is one rule at the routing choke point:
if a command is a WRITE, is not in is_multi_key_command (the set that IS coordinated), and its declared keys span more than one shard, refuse CROSSSLOT.
analyze_txn_locality already implements exactly this test for MULTI/EXEC bodies, including the positional STORE/STOREDIST destinations that command_keys does not cover, and #583 landed a shared key-position walker with acl::keyspec that understands every one of these layouts. The pieces exist; what is missing is applying them to single commands.
Caveats for whoever picks this up:
- the guard must be behind the
is_multi_key_command allow-list, or it will break MSET/DEL/MGET, which legitimately span shards;
- movablekeys commands (
SORT ... STORE, GEORADIUS ... STORE, EVAL) are not covered by the fixed first_key/last_key/step specs — store_clause_dest in shared.rs is the existing precedent;
- it must be applied on BOTH dispatch paths (
handler_monoio and handler_sharded), or it is invisible on the shipped runtime;
- tests should assert conservation (exactly one copy of the value exists, and the reply agrees with where it is) rather than the error string, so the suite survives a future cross-shard hop — see
tests/list_move_cross_shard.rs.
Repro script and the raw table above are reproducible with a 4-shard server and the five commands listed; each needs --disk-free-min-pct 0 on a near-full data volume.
Found while fixing #570 (PR #591). #570 was scoped to
BLMOVE/BRPOPLPUSH; the same routing rule breaks every two-key write whose destination is not the routing key, and #591 deliberately fixed only theLMOVEfamily.Mechanism
A keyed command is routed by
extract_primary_key, which returnsargs[0]. The owning shard of that ONE key then executes the whole command against its own slice. For a command that also WRITES a second key, the write lands in the routing key's shard under the second key's name — invisible to every normally-routed read of that name, which goes to its owner.Measured
Merge-base binary
fc60836c,--shards 4, 12 key placements each,FLUSHALLbetween trials. "LOST" = the command reported success and the destination is not visible afterwards.RPOPLPUSH a bm(element gone from both keys)SMOVE a b m1(member gone from both sets)RENAME a b+OK(key gone from both names)SINTERSTORE b a0(source intact, dest written to wrong shard)ZRANGESTORE b a 0 -10(source intact, dest written to wrong shard)RPOPLPUSHis fixed by #591. The remaining four are not.Two distinct severities:
SMOVE,RENAME. The value leaves the source and is not readable at the destination.RENAMEis the sharpest:+OK, and the key exists under neither name.SINTERSTORE,ZRANGESTORE. The source survives; the destination is written to the wrong shard's table, so a later read sees nothing and the count returned is computed from a source the executing shard could not see either.Not exhaustive. The same shape is expected for at least
RENAMENX,SUNIONSTORE,SDIFFSTORE,ZUNIONSTORE,ZINTERSTORE,ZDIFFSTORE,GEORADIUS ... STORE/STOREDIST,GEOSEARCHSTORE,SORT ... STORE,BITFIELD-adjacent stores andLCS ... IDX-adjacent forms — every one of these should be measured, not assumed.COPYis NOT affected: it is inis_multi_key_commandand coordinated bycoordinate_copy, which hops to the destination's owner (degrading to an error for non-string values).BITOP,MSET,MSETNX,MGET,DEL/UNLINK/EXISTSare likewise coordinated.Suggested direction
A per-command guard for each of these repeats #591's work N times. The systemic version is one rule at the routing choke point:
analyze_txn_localityalready implements exactly this test for MULTI/EXEC bodies, including the positionalSTORE/STOREDISTdestinations thatcommand_keysdoes not cover, and #583 landed a shared key-position walker withacl::keyspecthat understands every one of these layouts. The pieces exist; what is missing is applying them to single commands.Caveats for whoever picks this up:
is_multi_key_commandallow-list, or it will breakMSET/DEL/MGET, which legitimately span shards;SORT ... STORE,GEORADIUS ... STORE,EVAL) are not covered by the fixedfirst_key/last_key/stepspecs —store_clause_destinshared.rsis the existing precedent;handler_monoioandhandler_sharded), or it is invisible on the shipped runtime;tests/list_move_cross_shard.rs.Repro script and the raw table above are reproducible with a 4-shard server and the five commands listed; each needs
--disk-free-min-pct 0on a near-full data volume.