Repository navigation
Description
Recording + measurement only — no severity asserted, no fix chosen. Found while closing #15983 (pinning RedisKV's WATCH/MULTI limbs). It is a different defect class from that card (fidelity of the double, not coverage of our code), and different again from #15986 (which pins the ioredis 5-vs-6 version pair), so it is filed rather than folded into either.
The reading
On ioredis-mock@8.13.1, a connection that has itself EXECed a write to a key carries a stale dirty flag for that key: the next WATCH + EXEC on the same key from that same connection aborts (exec() returns null) even though no competing writer exists. A real Redis server would commit there.
Measured with a standalone script — no vitest, no Proxy, no ObjectStack code in the path — replaying the client-level sequence directly against a bare mock:
ARRAY set#1 (WATCH k, GET k, MULTI/SET k, EXEC)
NULL set#2 (same key, same connection, no competing writer)
ARRAY set#3 (the abort clears the flag)
ARRAY after an explicit UNWATCH between the two
ARRAY CONTROL: fresh key on a fresh connection
The last line is the control, and it fires: the probe reports ARRAY where no abort is expected, so the NULL readings are a discrimination rather than a constant.
Two further measurements bounding the shape:
- The flag is per-connection. A second connection WATCHing a key the first EXECed on is unaffected (
ARRAY). - WATCH itself is genuinely implemented. A competing write from another connection between
WATCHandEXECdoes abort, including when the rival rewrites byte-identical content — which is correct: real WATCH tracks writes, not value changes. So this is not awatch()-as-no-op double.
Where it already shows up
Instrumenting the shipped packages/services/service-cluster-redis/src/redis.contract.test.ts (recording Proxy around makeClient, suite otherwise unmodified, 28/28 green under instrumentation): multi().exec() is called 10 times and returns null for 2 of them — in set then get round-trips and increments version and in cas succeeds on match, fails on mismatch. Both are second writes to the same key on the same connection. Nothing in the suite creates a competing writer, so those two aborts are entirely this divergence.
Consequence today: RedisKV.set retries and converges, so both tests pass and the divergence is invisible. It cost #15983 a corrected premise — that card reasoned the retry limb was executed zero times; it is executed twice, for a reason that does not exist on a real server.
Scope of the claim
- ⛔ No claim that
RedisKVis wrong. Its retry loop is what absorbs this. - ⛔ No claim about the mechanism inside
ioredis-mock; only its observable behaviour is measured here. Not checked against the upstream tracker. - ⛔ No claim about any version other than the resolved
ioredis-mock@8.13.1in this workspace, nor about any command other thanWATCH/MULTI/EXEC/SET/DEL. - ⛔ Nothing about lock, counter or pub/sub behaviour.
Why it is worth writing down
It bounds what this double can certify. Transactional abort semantics are the surface a real server and an in-memory double are most likely to diverge on, and here they measurably do — in the direction of extra aborts, which a retry loop hides. Any future assertion of the form "this transaction committed without a retry" is unsound against this double, and any test that means to drive an abort must arrange a competing writer on a key the driver's own connection has not EXECed on (which is what #15983's pins now do).
Dedup
One targeted MCP search_issues query for this shape ⇒ 4 results, none describing it (#15983 and #15986 are the adjacent cards named above; #15467 is the closed parent; #7880 is objectql's transaction(), unrelated). The same call returning those known-hitting cards is the positive control that the channel answers, so the zero for this shape is a reading and not a silent empty. Search covered closed issues as well as open.
Activity
os-zhuang commentedon Sep 7, 2026
Triage: closing⚠️ This is the closure I am least comfortable with in today's round, so the reasoning is set out in full rather than asserted.
not planned— outside the three admission classes (#16351, as refined by PR #16557).Against each class:
ioredis-mock@8.13.1, not in this tree. Nothing inobjectstackbehaves wrongly:RedisKV.setretries and converges, and the card says so first — ⛔ "No claim thatRedisKVis wrong. Its retry loop is what absorbs this."⇒ The card's own framing is the disposition: ⛔ "Recording + measurement only — no severity asserted, no fix chosen." There is no work item here, and a card with no work item is a record.
⭐ The measurement deserves to be named because it is exemplary, and closing it is not a judgement on its quality. A standalone script — no vitest, no Proxy, no ObjectStack code in the path — replaying the client sequence against a bare mock, with the control on the same probe (
ARRAYon a fresh key/fresh connection, where no abort is expected) so theNULLreadings are a discrimination rather than a constant. Plus two bounding measurements that rule out the lazy explanations: the flag is per-connection (a second connection is unaffected), andWATCHis genuinely implemented (a competing write does abort, including a byte-identical rewrite — correct, since real WATCH tracks writes, not value changes). ⇒ ⛔ Not awatch()-as-no-op double, and the card proves it rather than assuming it.⭐ And the instrumented reading is the kind of thing that usually goes unmeasured:
multi().exec()returnsnullfor 2 of 10 calls in the shippedredis.contract.test.ts, both second writes to the same key on the same connection, with nothing in the suite creating a competing writer. ⇒ Those two aborts are entirely this divergence, and they are invisible because the retry absorbs them.承接者: ⛔ none needed — the actionable half is already landed.⚠️ Checked rather than assumed: the operative consequence is "any test that means to drive an abort must arrange a competing writer on a key the driver's own connection has not EXECed on", and the card states this is what #15983's pins now do. ⇒ The practice is encoded in the test file, where it survives; what remains here is the explanation of why.
What I am uncertain about, and it is the real cost of this close: ⭐ this card already prevented one wrong conclusion. #15983 reasoned the retry limb was executed zero times; it is executed twice, for a reason that does not exist on a real server. That is a corrected premise on a landed card — evidence that the explanation has working value, not just archival value. ⛔ Closing does not delete it (a closed issue is searchable and this one is thoroughly indexed), but it does mean nobody meets it unless they look.
⇒ ⭐ The durable home for this is a comment beside the double's setup in
packages/services/service-cluster-redis/src/redis.contract.test.ts, not an issue — that is where a reader forms the belief this card corrects. ⛔ I am not filing that as work; it is a free rider for whoever next opens that file, and this card is its text.Reopen conditions (any, no approval needed):
ioredis-mockis upgraded. ⇒ The divergence must be re-measured at the new version; ⛔ do not assume it persists or that it is fixed. The standalone script in this card is the instrument.Reversibility: fully reversible; nothing decays. ⭐ Note the scope discipline that makes this safe to close — the card fences its claim to the resolved version in this workspace and to
WATCH/MULTI/EXEC/SET/DELonly, explicitly declining to claim anything about the mechanism insideioredis-mockor about the upstream tracker. ⇒ A finding that states its own boundaries is one a later reader can trust without re-deriving it.Generated by Claude Code