Fix singleton prefix-listing upper bound¶
Motivation¶
api/services/state/singletons.py::list_singletons_by_prefix built its Azure
Table range query upper bound as sanitised_prefix + "~~~~~~~~" (eight ~,
0x7e). A fixed trailing sentinel is incorrect: it silently excludes any row
whose suffix sorts at or above the sentinel. Two concrete classes are dropped:
- a suffix containing eight or more
~after the prefix, and - any suffix whose first differing character is above
~— RowKeys may legitimately contain such characters because the key sanitiser only strips/ \ # ?and\u0000-\u001f/\u007f-\u009f, so e.g. accented letters (\u00e9) and other code points ≥\u00a0survive into the stored key.
The comment claimed a "chr(0x7e+1) trick", but the code did not implement it,
and even that intent would have been wrong for keys carrying characters ≥
\u0080.
The only caller today is the OpenAPI public-HTTPS reconciler, whose per-cluster
keys are openapi:runtime:public-base-url:cluster:<sha256[:16]> — pure hex, so
the defect never bit in production. It is still a latent correctness bug: any
future singleton with a different key shape could silently lose rows.
User-facing change¶
None observable today (the live keys are hex digests). This is an internal
correctness fix that makes the storage primitive honour its documented contract
("return every row whose key starts with prefix") for any suffix charset.
API / IaC diff summary¶
api/services/state/singletons.py:- New
_prefix_upper_bound(prefix)— returns the smallest string strictly greater than every string starting withprefixby incrementing the last code point of the prefix. Correct for any suffix because all matching keys share the prefix and therefore sort below the incremented-prefix bound, independent of what follows. list_singletons_by_prefixnow uses_prefix_upper_bound(sanitised_prefix)for theRowKey ltbound and guards an empty sanitised prefix.api/tests/test_state_singletons.py:_FakeTableClient.query_entitiesparses thePartitionKey eq / RowKey ge / RowKey ltfilter and applies an ordinal string comparison (matching Azure Tables) so the bound math is exercised, not a wildcard.test_list_by_prefix_returns_matching_rows— only the prefixed rows return.test_list_by_prefix_includes_suffix_above_tilde— regression guard: a nine-~suffix and a\u00e9-prefixed suffix are both returned (both were dropped by the old sentinel).test_list_by_prefix_empty_prefix_returns_empty.
Validation evidence¶
uv run pytest -q api/tests/test_state_singletons.py→ 8 passed (3 new).- Consumer sweep:
test_openapi_public_https_reconcile.py,test_openapi_runtime_endpoint_durable.py,test_openapi_runtime_token_cache.py→ 30 passed. - Full backend:
uv run pytest -q api/tests→ 3840 passed, 3 skipped. uv run ruff checkon both touched files → clean.