Skip to content

Add a flag to returns the CAS calue from ADD/SET/REPLACE/CAS and multi-set operations#261

Open
alexmv wants to merge 3 commits intojaysonsantos:masterfrom
alexmv:cas-returns
Open

Add a flag to returns the CAS calue from ADD/SET/REPLACE/CAS and multi-set operations#261
alexmv wants to merge 3 commits intojaysonsantos:masterfrom
alexmv:cas-returns

Conversation

@alexmv
Copy link
Copy Markdown
Contributor

@alexmv alexmv commented Apr 24, 2026

The binary protocol returns the CAS value from these operations already -- but it's currently being silently discarded. Being able to get the value back without an extra GETS reduces latency by dropping a round-trip, and also closes a class of race condition.

I've one this in a way which is fully backwards-compatible, since the API for this module is quite stable. The first commit in the series calls out that ReplicatingClient.cas() is pretty much guaranteed to cause drift in the values stored in the "replicas," so is almost certainly never what you want. For backwards-compatibility, I left this as a documentation-only change -- but it would be safer to make it raise NotImplementedError. Let me know what you'd prefer.

alexmv added 3 commits April 24, 2026 13:24
ReplicatingClient's CAS-touching methods have always had a silent
correctness problem when used against more than one replica: each
server maintains its own CAS counter, so a CAS value cannot match on
more than one replica. any(returns) then reports success as long as
one server accepted the write, but the other replicas silently
rejected it, leaving them divergent.

The get-side methods do not warn about this -- `gets()`,
`get(get_cas=True)`, and `get_multi(get_cas=True)` return a CAS from
whichever replica happened to respond first, even though that value
cannot be safely passed to cas() on a multi-replica client.

Add warnings on the class docstring and on each affected method, so
callers have some hope of noticing the hazard. For backwards
compatibility, the behavior itself is left unchanged.
add(), set(), replace(), and cas() all produce an item with a new CAS
value on success, and the memcached binary protocol already returns it
in the response header -- the client was simply discarding it. Callers
who want to chain a CAS-guarded update after a write had to follow up
with a separate gets() round-trip, which is both slower and racy
(another writer could slip in between):.

Add an optional `get_cas=False` kwarg matching the existing convention
on get()/get_multi(). When True, these methods now return a tuple of
`(success, cas)` instead of a plain bool; `cas` is the new CAS on
success, or None on failure.

For ReplicatingClient, the returned CAS comes from the first replica
that reported success, matching how get/gets already handle replicas.
CAS is inherently per-server, so only that server's CAS is meaningful
for subsequent CAS operations against the same server.
set_multi's current return shape is a list of failed keys, which can't
carry a per-key CAS value. It also uses the quiet setq/addq opcodes,
which intentionally suppress successful responses -- so even if the
shape allowed it, the wire protocol wouldn't return a CAS per key.

Add a separate `set_multi_cas` method that uses the non-quiet set/add
opcodes (one response per key) and returns `{str_key: int | None}` for
every input key -- int on success, None on failure. The existing
`{(key, cas): value}` input syntax from set_multi is preserved; the
result dict is keyed by the string key regardless of which form was
passed.

For ReplicatingClient, the returned CAS per key is the first non-None
CAS from any replica, matching the single-key helpers.
@alexmv
Copy link
Copy Markdown
Contributor Author

alexmv commented Apr 24, 2026

Pushed #262 to fix CI.

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