OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Libraries

oslo.cache — Cache contract

Cache contract — oslo.cache — branch stable/2026.2

Branch stable/2026.2 @ fc96999a99e4 — repository https://opendev.org/openstack/oslo.cache — analysed on 2026-10-28 Measured with dogpile.cache 1.5.0, python-memcached 1.62, pymemcache 4.0.0, redis-py 8.1.0, Python 3.13

This file says what an OpenKeel service written in Go must do to share the same memcached (or Redis) as the Python OpenStack services: read their entries, write entries they can read, and invalidate them. It is backed by 19 scenarios recorded against fake servers (§ 6).

1. What sharing means, and when it is needed

Python services only read their own keys: the key contains the Python module and function name (§ 2). A Go service therefore shares an entry with a Python service in only three cases:

  1. The same logical key is computed on both sides: a Go Keystone that coexists with a Python Keystone during a migration, behind the same memcached.
  2. The cache is a published interface: keystonemiddleware's token cache, read by every service. It does not go through dogpile; its contract is in reverse-spec-keystonemiddleware 03.
  3. Invalidation: a Go component must make Python processes drop an entry.

Otherwise, a Go service can use the same memcached with its own key format, as long as its keys cannot collide with 40-character hexadecimal keys. Recommended: a prefix such as ok:.

2. Keys: requirements

#RequirementProof
K1Wire key = hex(sha1(utf8(logical key))), 40 lowercase characters, no prefixscenarios 1–3, 12
K2Memoization logical key = <Python module>:<function name>| + the positional arguments converted with str() and joined by a space. self/cls dropped; default values included; with a namespace: <module>:<function>|<namespace>|<args>scenarios 1, 17
K3Python str() must be reproduced exactly for each argument type: True, None, ['a', 'b'], {'k': 1}… A Go service that coexists with Python on the same function must format like Python, or not sharescenario 17
K4Keystone: logical key + : + str(region_id) before SHA-1, where region_id is the bytes value stored under <<<region>>>:<region name> (so the suffix looks like b'\x8f…')keystone:keystone/common/cache/core.py:76-89

3. Values: requirements

#RequirementProof
V1memcached: flags 1, value = pickle of dogpile.cache.api.CachedValue(payload, {"ct": <float>, "v": 2}). A reader must accept protocol 0 (python-memcached, with copy_reg._reconstructor / __builtin__.tuple and the memo) and protocols 2 to 5 (pymemcache)scenarios 1–4, 13
V2A Go writer can produce protocol 2 with the global dogpile.cache.api CachedValue and a 2-tuple of arguments: Python reads itscenario 13
V3Redis: <JSON {"ct": …, "v": 2}>|<pickle of the payload>, no flagsscenarios 18, 19
V4v ≠ 2 is a miss for Python readersdogpile CacheRegion._is_cache_miss
V5The payload is an arbitrary Python object. Shared entries must contain only types that a Go pickle library can map: dict, list, tuple, str, int, float, bool, None. Keystone caches its own objects too; those cannot be shared [inferred]—
V6Security: unpickling executes code chosen by whoever wrote the value. Anyone who can write to memcached can run code in every Python service sharing it. A Go reader must use a restricted unpickler (allowed globals: dogpile.cache.api.CachedValue, copy_reg._reconstructor, __builtin__.tuple, builtins.tuple), and memcached must stay on a private networkpickle format

4. Lifetimes and invalidation: requirements

#RequirementProof
T1Freshness: a reader serves the value only if now - ct ≤ its TTL ([group] cache_time, otherwise [cache] expiration_time, 600 s). ct is a float of Unix seconds from the writer's clock: clocks must be synchronisedscenarios 6, 7
T2Server TTL: exptime = backend_expiration_time if configured, otherwise 0. A Go writer should send the same value, never less than the readers' TTLscenario 5
T3Miss: read, then read again under a lock local to the process, compute, write. No distributed lock (distributed_lock defaults to false), so several processes may compute the same value at the same timescenario 1
T4[cache] enabled = false → no traffic at all (null backend). [group] caching = false → reads but no writesscenarios 8, 9
T5Invalidation visible to Python = delete <key>, or, for Keystone, a new region id under <<<region>>>:<name>. region.invalidate() is local to the processscenario 10
T6set_multi / delete_multi = one command per key (python-memcached); get_multi = one get k1 k2…scenario 11

5. Connections and failures: requirements

#RequirementProof
C1With several memcached servers, the Go client must choose the server like the Python client used opposite it: python-memcached = ((crc32(key) >> 16) & 0x7fff) % n, in the order of memcache_servers; pymemcache = rendezvous hashing. Python services using different clients already disagree03 § 4
C2Behaviour to keep when memcached is down: miss without error (python-memcached and memcache_pool)scenarios 14, 15
C3TLS or SASL → binary protocol (python-binary-memcached)oslo_cache/backends/memcache_pool.py:70-91
C4Configuration: accept the same [cache] options and refuse the same combinations (02 § 1), except the two defects listed there_rewrite/config-mapping_stable-2026.2.json

6. Scenarios (19/19)

Run with the oslo_cache.py adapter of the generate-reverse-spec skill:

conformance_capture.py scenarios <oslo.cache> --branch stable/2026.2 --adapter adapters/oslo_cache.py --workdir <tmp> --out <tmp>/cc

The adapter serves a fake memcached (text protocol: get/gets/set/add/replace/cas/append/prepend/delete/touch/incr/decr/flush_all/version) and a fake Redis (RESP2/RESP3: HELLO/GET/MGET/SET/SETEX/DEL/…), with the real item lifetimes. It drives oslo.cache through its public API. No real memcached or Redis was available; the fakes follow the protocols, so a run against real servers should give the same traffic [inferred].

#ScenarioResult
1memcache_pool: memoized call, miss then hitget, get, set … 1 0 211, get (hit); key = sha1 of …:get_project|p-pool default
2dogpile.cache.memcached: memoized callsame, protocol 0 pickle
3dogpile.cache.pymemcache: memoized callsame keys, protocol 5 pickle
4cross-library interoperability9/9 writer → reader pairs read
5backend_expiration_time = 900set … 1 900 …
6expiration_time = 1 s, 1.2 s pausevalue recomputed and rewritten; the server item had no TTL
7[svc] cache_time = 1 overrides 600recomputed
8[svc] caching = false2 × (get, get), no set, computed twice
9[cache] enabled = falseNullBackend, no traffic
10region.invalidate()no command; the other region still reads v1
11set_multi / get_multi / delete_multi2 set, 1 get with 3 keys, 2 delete
12non-ASCII keymangled key = sha1 of the UTF-8 bytes
13foreign writerprotocol 2 CachedValue written over a raw socket, read by Python
14memcached unreachable (memcache_pool)miss, value computed, no exception
15unreachable, other backendsmemcached: miss; pymemcache: ConnectionRefusedError; pymemcache + retry client: miss; Redis: ConnectionError
16pool of 1, already borrowedQueueEmpty after 1 s
17key generatorsmod:_sample|a 1, mod:_sample|ns|a 1, mod:method|a
18RedisGET, GET, SET <k> <json|pickle>, GET (hit), DEL
19Redis + backend_expiration_timeSETEX <k> 900 …

7. Files in _rewrite/

FileContent
scenarios_stable-2026.2.jsonthe 19 scenarios, outcome and observations
wire_stable-2026.2.jsonthe 68 commands received by the fake servers (50 memcached, 18 Redis), with decoded values: pickle protocol, globals, CachedValue
config-mapping_stable-2026.2.json17 [cache] configurations → dogpile arguments or refusal

8. Recommendation for OpenKeel

  • Do not share memoized entries between Go and Python: the keys depend on Python names and str() formatting (K2, K3), and values are pickles (V6). Give Go services their own key prefix.
  • Share only through published contracts: keystonemiddleware's token cache, or explicit deletes. For Keystone during a migration, either run a separate cache, or implement K4 and T5 to take part in region invalidation.
  • If a Go reader of pickles is still needed: restricted unpickler (V6), and V1–V4, T1–T2.

Source: reverse-spec-oslo.cache/07-cache-contract_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.