Skip to content

fix(backends): recognise counters the same way on every backend - #158

Merged
allen0099 merged 1 commit into
masterfrom
fix/strict-counter-detection
Sep 26, 2026
Merged

allen0099 merged 1 commit into
masterfrom
fix/strict-counter-detection

Conversation

@allen0099

Copy link
Copy Markdown
Owner

Closes #111.

Problem

Reproduced on all three backends before the change:

memory    increment(response '42') -> 43           increment(set counter_entry(5)) -> 6
redis     increment(response '42') -> CacheXError  increment(set counter_entry(5)) -> CacheXError
memcached increment(response '42') -> CacheXError  increment(set counter_entry(5)) -> CacheXError
decode_entry(b' 7 ') -> counter 7
decode_entry(b'1_0') -> counter 10

After the change, every backend gives CacheXError for the response and 6 for the counter, and both stored values decode to None.

Change

  • counter_value() (types.py) now requires fingerprint == COUNTER_FINGERPRINT as well as an integer body. The memory backend and the base fallback therefore refuse a cached response like the network backends do.
  • New parse_counter() (types.py) replaces int() in both counter_value and the codec. It accepts an optional -, ASCII digits and trailing spaces. It rejects leading whitespace, _, +, newlines and non-ASCII digits.
    • This differs from the issue's ^-?\d+$. Memcached pads a value that DECR shortens rather than resizing it: DECR on 10 leaves b"9 " (checked against a live server). A strict pattern would break every Memcached counter after a shrinking decrement.
  • encode_entry() stores exactly what counter_entry(n) builds as a bare integer, which is the form INCR/INCRBY work on. This is the issue's optional part. Without it, the set()-then-increment() inconsistency would remain. An entry with a different fingerprint, a media type, or a non-canonical body such as 042 is still stored as a JSON document, and it round-trips unchanged.

These edge cases are the only behaviour change; nothing in the API changes.

Tests

  • New tests/backends/test_counter_contract.py runs the same assertions on memory, the base fallback, Redis and Memcached:
    • a numeric response is refused and left intact;
    • set(counter_entry(5)) continues with increment();
    • a shrinking counter reads back as a counter.
      It also covers parse_counter and counter_value directly.
  • test_codec.py:
    • the case " 7\n", which used to be accepted, is now rejected;
    • new tests cover the bare-integer encoding and the entries that must stay JSON documents.

Mutation checks, each with the rest of the fix in place:

Mutation Failing tests
Drop the fingerprint check 3
Drop the bare-integer encoding 3 (including Redis and Memcached contract tests)
Go back to int() 12

The full suite passes against live Redis and Memcached (CACHEX_REQUIRE_LIVE_SERVERS=1): 826 passed, 100% coverage. mypy --strict is clean.

Like #156 and #157, this adds a ### Fixed section to ## [Unreleased], so the later merges need a small CHANGELOG rebase.

@allen0099 allen0099 added this to the 0.3.8 milestone Sep 26, 2026
@allen0099 allen0099 added bug Something isn't working backends Cache backends and their atomic primitives labels Sep 26, 2026
@allen0099
allen0099 force-pushed the fix/strict-counter-detection branch from 6757535 to 87e7d48 Compare September 26, 2026 11:31
counter_value() parsed any entry whose content was a number, so on the
memory backend and the base fallback increment() on a cached response with
body "42" returned 43 and overwrote it, while Redis and Memcached raised
CacheXError. It now also requires COUNTER_FINGERPRINT.

A counter written with set(key, counter_entry(n)) was stored as a JSON
document on Redis and Memcached, which INCR rejects. encode_entry now stores
exactly what counter_entry builds as a bare integer.

The codec read any value int() accepts as a counter, including " 7" and
"1_0". parse_counter accepts only an optional minus sign and ASCII digits,
plus the trailing spaces Memcached pads a value with when DECR shortens it.

Closes #111
@allen0099
allen0099 force-pushed the fix/strict-counter-detection branch from 87e7d48 to 2e9e72d Compare September 26, 2026 11:35
@allen0099
allen0099 merged commit 8e393eb into master Sep 26, 2026
11 checks passed
@allen0099
allen0099 deleted the fix/strict-counter-detection branch September 26, 2026 11:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backends Cache backends and their atomic primitives bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Counter detection differs between backends

1 participant