Found while reviewing #3364.
_executeMulti/_executePipeline queue their commands without a slotNumber:
this._self.#queue.addCommand(args, { chainId, typeMapping })
extractCommandsForSlots(slots) (used for partial slot migrations) filters by
slotNumber, so these batch commands are never matched there — they just stay queued on
the original node, which is fine as long as that node is still alive.
But when a node loses all its slots, extractAllCommands() drains everything
regardless of slot, and cluster-slots.ts forwards the whole result to lastDestNode —
whichever destination happened to be processed last while walking the migration, not
necessarily the node that owns the batch's actual key slot. A MULTI/pipeline batch queued
at that point can end up sent to a node that doesn't own its slot, resulting in MOVED
errors or an aborted transaction instead of surviving the handoff.
Fixing this needs slot info preserved on batch-queued commands (or the full-node drain
routed per-command instead of to a single lastDestNode).
Found while reviewing #3364.
_executeMulti/_executePipelinequeue their commands without aslotNumber:extractCommandsForSlots(slots)(used for partial slot migrations) filters byslotNumber, so these batch commands are never matched there — they just stay queued onthe original node, which is fine as long as that node is still alive.
But when a node loses all its slots,
extractAllCommands()drains everythingregardless of slot, and
cluster-slots.tsforwards the whole result tolastDestNode—whichever destination happened to be processed last while walking the migration, not
necessarily the node that owns the batch's actual key slot. A MULTI/pipeline batch queued
at that point can end up sent to a node that doesn't own its slot, resulting in
MOVEDerrors or an aborted transaction instead of surviving the handoff.
Fixing this needs slot info preserved on batch-queued commands (or the full-node drain
routed per-command instead of to a single
lastDestNode).