Skip to content

Redis 8.12 atomic sorted-set moves: ZMOVE / ZMOVEMEMBER on IDatabase - #3351

Open
mgravell wants to merge 1 commit into
8.12from
marc/zmove
Open

mgravell wants to merge 1 commit into
8.12from
marc/zmove

Conversation

@mgravell

Copy link
Copy Markdown
Member

Sub-PR of #3343. IDatabase support for the sorted-set moves that Redis 8.12-rc3 adds (redis/redis#16135): ZMOVE and ZMOVEMEMBER. Written against the client HLD in redis-developer/client-designs (redis-oss/client-hld/zmove/README.md). BZMOVE is not exposed, per the blocking-command policy.

Surface

Member Command Notes
SortedSetMove(src, dst, Order order = Ascending, long? count = null) ZMOVE src dst MIN|MAX [COUNT n] from the low or high end
SortedSetMoveByScore(src, dst, double start, double stop, Exclude exclude = None, long? count = null) ZMOVE src dst BYSCORE min max [COUNT n] bounds as SortedSetRemoveRangeByScore takes them
SortedSetMoveMembers(src, dst, RedisValue[] members, When when = Always) ZMOVEMEMBER src dst [NX|XX] MEMBERS n m... When is the per-member condition on the destination

All return SortedSetEntry[]? (and Task<...> twins), and all are [Experimental(Experiments.Server_8_12)].

Choices worth a look:

  • null means nothing moved. The server's null reply is never folded into an empty array; same contract as the LMOVEM overload of ListMove.
  • COUNT passes through. It is sent if and only if the caller gives one, including 1 and values the server rejects. SortedSetPop elides COUNT 1; that is not copied, because for BYSCORE no count means the whole range.
  • Three names rather than overloads. SortedSetMove(a, b, 0, 5) would be ambiguous between Order/long? and double/double (literal 0 converts to an enum).
  • start/stop are required on SortedSetMoveByScore: infinite defaults would make a bare call move the whole set.
  • Members are not tidied. Not de-duplicated or re-ordered, a member named NX or MEMBERS is data, and an empty list goes out as MEMBERS 0 for the server to reject.

Reply parsing

The reply is nested [member, score] pairs in RESP2 as well as RESP3 (only the score type differs), unlike ZPOPMIN. SortedSetWithScores reads RESP2 as a flat interleaved list, so there is a new strict processor, ResultProcessor.NullableSortedSetEntryArray; a flat list or a non-pair is an unexpected reply rather than a guess.

Routing, retry, coverage

  • Both commands are primary-only and CommandRetryWriteAccumulating, alongside ZMPOP.
  • BZMOVE gets a policy row in docs/CommandCoverage.md; no enum member, as with BZMPOP.
  • Transaction analyzer: nothing mapped, with the reason left next to the SMOVE row. SortedSetRemove + SortedSetAdd writes the caller's score and adds unconditionally; ZMOVEMEMBER carries the source's score and moves nothing when the source lacks the member.

Shared 8.12 plumbing

  • New RedisFeatures.v8_12_0_rc3 (8.11.242) and RedisFeatures.SortedSetMove: the existing v8_12_0 is rc2 (8.11.241), which does not have these commands.
  • Test image moves to client-libs-test:8.12-rc3 (Dockerfile and compose). rc3 requires cluster-bus-port-protected-mode no for a plaintext cluster bus; the cluster configs on this branch already set it.
  • docs/exp/SER011.md gains the feature bullet.

Tests

  • ResultProcessorUnitTests/SortedSetMove: RESP2 and RESP3 pairs, single pair stays nested, infinite scores, order, both null forms, empty array, six malformed shapes.
  • RoundTripUnitTests/SortedSetMoveRoundTrip: exact bytes for every token shape, plus argument validation.
  • SortedSetMoveTests (live, [RunPerProtocol], require: v8_12_0_rc3): selection, count, null cases, same key, conditions, keyword-like members, server errors, transaction, key prefix, and two cluster tests (same-slot routing; cross-slot refused with nothing moved).
  • KeyPrefixed* forwarding tests for all six members.

Run locally: 76/76 on net10.0 and net8.0 against throwaway 8.12-rc3 standalone and single-node cluster servers. Full suite on net10.0 against an older (8.9.241) topology: 6838 pass, 254 skip, 1 fail (FailoverTests.SubscriptionsSurvivePrimarySwitchAsync, which passed on one of two isolated reruns; not checked on the base branch). net481 not run.

dotnet build Build.csproj -c Release /p:CI=true is clean apart from the NU1903 ILCompiler advisory in toys/AotRig.

Known, not addressed here

On a cluster, cross-slot keys are refused by the library for the bare SortedSetMove(src, dst) and by the server (CROSSSLOT) for the other forms. The cause is in Message.cs: the two-key messages carrying two or more values do not combine the second key into the hash slot. It predates this branch and same-slot calls route correctly; the cluster test accepts either layer. To be handled separately.

redis/redis#16135 (8.12-rc3) adds ZMOVE, BZMOVE and ZMOVEMEMBER:

- SortedSetMove (ZMOVE MIN|MAX [COUNT]) - from the low or high end
- SortedSetMoveByScore (ZMOVE BYSCORE min max [COUNT]) - a score range
- SortedSetMoveMembers (ZMOVEMEMBER [NX|XX] MEMBERS n ...) - named members,
  with When as the per-member condition on the destination

Each returns SortedSetEntry[]?, null when nothing moved (the server's null
reply is never folded into an empty array). COUNT is sent if and only if the
caller gives one: for BYSCORE, no COUNT means the whole range, so the
SortedSetPop habit of eliding COUNT 1 is not copied.

The reply is nested [member, score] pairs in RESP2 as well as RESP3, unlike
ZPOPMIN, so SortedSetWithScores does not fit; NullableSortedSetEntryArray
is a strict nested-pair processor.

BZMOVE is not exposed (blocking; policy row in CommandCoverage.md).

Gated [Experimental] behind SER011 and the new RedisFeatures.v8_12_0_rc3
(8.11.242: rc2 does not have the commands); the test docker image moves to
client-libs-test:8.12-rc3.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant