perf(memcached): one worker call per operation; delete_many counts removals - #273
Merged
Merged
Conversation
…movals delete_many inherited the base per-key loop: one thread hop per key, and it returned the number of keys it was given. It now sends one acknowledged DELETE per unique namespaced key inside a single worker call and counts the ones that existed. pymemcache's own delete_many returns True regardless and still issues one DELETE per key on HashClient, so it cannot report removals. increment, get_and_delete, delete_if_equals and expire_if_equals each ran every round trip in its own asyncio.to_thread call. Each now runs as one sync helper in a single call, with the same CAS retry, validation and error-mapping behaviour. expire_if_equals converts its ttl before the GETS, so a ttl past 2038 fails before any I/O like set does. Closes #174 Closes #176
This was referenced Sep 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #174, closes #176. With #175 and #177 already merged, this completes #119.
delete_many(#174)MemcachedBackendinherited the base per-key loop. That meant oneasyncio.to_threadhop per key, and it returned the number of keys it was given.pymemcache's own
delete_manydoesn't fix this. It always returnsTrue, and onHashClientit still sends oneDELETEper key (_run_cmd("delete", key, ...)in a loop), so it can't report what was removed.The override instead:
DELETE(noreply=False) per key, which is the same number of round trips as pymemcache's batched call;_make_key(over-long or illegal keys included), and sends a duplicate key only once;0without I/O for an empty list.This now matches the memory and Redis backends, which already count removals. The base-class docstring saying "Memcached keeps this per-key loop" is updated.
One thread hop per multi-step operation (#176)
Before this change, each round trip ran in its own
asyncio.to_threadcall: up to three forincrementand two for each of the other three. Each operation now runs in a single sync helper inside oneasyncio.to_threadcall:increment: INCR, ADD, INCR;get_and_delete: the GETS/CAS loop;delete_if_equalsandexpire_if_equals: GETS, compare, CAS.Unchanged behaviour:
CacheXError;validate_ttl/validate_deltastill run before any I/O;One small fix along the way:
expire_if_equalsnow converts its ttl before the GETS, so a ttl past 2038-01-19 raises before any I/O, asset/set_if_absent/incrementalready did. Before, the GETS ran first.Docs
docs/BACKENDS.md(EN and zh-TW): one worker trip per call, and whatdelete_many()returns now.CLAUDE.md: updated to match.Tests
New tests:
test_memcached_delete_many_counts_only_existing_keys(live): the count includes a hashed 300-byte key and ignores a duplicate and a missing key.test_memcached_delete_many_sends_each_namespaced_key_once(stub): checks the namespaced/hashed keys, the dedup, andnoreply=False.test_memcached_delete_many_with_no_keys_does_no_io.test_memcached_multi_step_operations_take_one_thread_hop, parametrized over the five operations. It countsasyncio.to_threadcalls on each operation's longest path.test_ttl_past_2038_is_rejected_before_iogains anexpire_if_equalscase.Mutation checks:
memcached.py, exactly 7 tests fail: the 5 single-hop cases, the stubdelete_manytest and the new 2038 case.delete_manytest.Verification:
uv run pytestwithout live servers: 808 passed, 191 skipped.CHANGELOG
Under Unreleased → Changed: