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

Reverse-spec · Libraries

oslo.cache — Interactions

Interactions with other components — oslo.cache — branch stable/2026.2

Branch stable/2026.2 @ fc96999a99e4 — repository https://opendev.org/openstack/oslo.cache — analysed on 2026-10-28 dogpile.cache 1.5.0, python-memcached 1.62, pymemcache 4.0.0, redis-py 8.1.0 (OpenStack 2026.2 upper-constraints)

What is written below was recorded by 19 scenarios against fake memcached and Redis servers (_rewrite/wire_stable-2026.2.json, 68 commands; see 07).

SystemProtocolLibrary (backend)Direction
memcachedtext protocol, TCP 11211python-memcached (dogpile.cache.memcached, oslo_cache.memcache_pool), pymemcache (dogpile.cache.pymemcache), pylibmcsynchronous request/response, from each service process
memcached with TLS or SASLbinary protocolpython-binary-memcached (oslo_cache.memcache_pool with tls_enabled or memcache_sasl_enabled, dogpile.cache.bmemcached)same
Redis / SentinelRESP (redis-py 8 negotiates RESP3 with HELLO 3)redis-py (dogpile.cache.redis, dogpile.cache.redis_sentinel)same
etcd v3HTTP/JSON gatewayetcd3gw (oslo_cache.etcd3gw)same

Nothing is published or consumed asynchronously: every exchange is caused by a call of the service (05).

1. Keys

  1. The service chooses the key: a string for region.get/set/delete, or the memoization key <module>:<function>|<arg1> <arg2>… (default values included, self/cls dropped; 01 § 3).
  2. oslo.cache hashes it with SHA-1: the key on the wire is the 40 lowercase hexadecimal characters of sha1(utf-8(key)) (oslo_cache/core.py:392-405, installed by oslo_cache/core.py:498-499).
    • The xmlcharrefreplace fallback never applies: UTF-8 encodes any str (scenario 12 of 07).
    • There is no prefix and no namespace: two services that hash the same string share the entry.
  3. The same key is used for memcached, Redis and etcd.

Keystone adds its own layer (keystone:keystone/common/cache/core.py:76-89): before the SHA-1, each key gets the suffix :<region id>. The region id is 10 random bytes, kept in the cache itself under the key <<<region>>>:<region name> (keystone:keystone/common/cache/core.py:29-51). Invalidating a region means writing a new id: all old keys become unreachable, for every process. This is the only distributed invalidation in OpenStack's use of oslo.cache. oslo.cache itself has none (§ 3).

2. Values

dogpile stores a **CachedValue**, a named tuple (payload, metadata) with metadata = {"ct": <creation time, float Unix seconds>, "v": 2}. v is dogpile's value format version: a different v is treated as a miss.

How it is serialised depends on the backend:

Backendmemcached flagsEncoding of the value
python-memcached (dogpile.cache.memcached, oslo_cache.memcache_pool)1 (pickle)pickle protocol 0 of the whole CachedValue, written with Python 2 names: copy_reg._reconstructor, dogpile.cache.api.CachedValue, __builtin__.tuple
pymemcache (dogpile.cache.pymemcache)1 (pickle)pickle protocol 5 (pickle.HIGHEST_PROTOCOL) of the whole CachedValue; global dogpile.cache.api.CachedValue
Redis (dogpile.cache.redis*)—**<JSON metadata>|<pickle of the payload>**: {"ct": 1793197005.10, "v": 2} then | then the payload pickled with protocol 4 (pickle.DEFAULT_PROTOCOL on Python 3.13)
etcd3gw—JSON of [payload, metadata] (oslo_cache/backends/etcd3gw.py:51-74)

All memcached clients read each other's values (scenario 4): Python's unpickler accepts every protocol.

Sample frame (python-memcached, region set("k", "v"); the line breaks inside the value are part of protocol 0):

set 13fbd79c3d390e5d6585a21e11ff5ec1970cff0c 1 0 140\r\n
ccopy_reg\n_reconstructor\np0\n(cdogpile.cache.api\nCachedValue\np1\nc__builtin__\ntuple\np2\n(Vv\np3\n(dp4\nVct\np5\nF1793196865.25\nsg3\nI2\nstp6\ntp7\nRp8\n.\r\n
STORED\r\n

sg3 reuses the memoised string v (the payload) as the metadata key v: a reader must implement the pickle memo.

3. Commands and lifetimes

OperationmemcachedRedis
readget <key> (get k1 k2… for get_multi)GET / MGET
writeset <key> 1 <exptime> <n> (set_multi = one set per key)SET <key> <value>, or SETEX <key> <ttl> <value>
deletedelete <key> (one per key for delete_multi)DEL
locksnone by default (distributed_lock = false: dogpile's lock is local to the process)none by default

Two lifetimes coexist:

  • Logical lifetime = expiration_time (or the group's cache_time). It is not sent to the server. The reader compares now - ct with its own expiration time and regenerates the value if it is older (scenarios 6 and 7).
  • Server lifetime = backend_expiration_time, only if set: exptime of set (memcached) or SETEX (Redis). By default it is 0: memcached keeps the item until eviction (scenario 5, 18 and 19).

Invalidation:

  • region.delete(key) sends delete: visible to everyone.
  • region.invalidate() (hard or soft) sends nothing: it records a timestamp in the process, and only that region ignores older values. Another process, or another region of the same process, still reads the old value (scenario 10).
  • Keystone's distributed invalidation (§ 1) is built on top of this.

Miss sequence: on a miss, dogpile reads the key twice (get, then get again after taking its local lock), then set (scenario 1). With [group] caching = false the two gets are still sent, but no set (scenario 8).

4. Connections

  • memcache_pool: up to memcache_pool_maxsize python-memcached clients per process, each with one TCP connection per server, closed after memcache_pool_unused_timeout of inactivity (oslo_cache/_memcache_pool.py:226-249). python-memcached picks the server as ((crc32(key) & 0xffffffff) >> 16) & 0x7fff modulo the number of servers in memcache_servers. If that server is dead, it retries up to 10 times with the hash of str(hash) + str(i) (python-memcached cmemcache_hash and Client._get_server). The order of the list therefore matters for every client sharing the cache.
  • **dogpile.cache.memcached**: one client per thread (dogpile's ClientPool(threading.local)).
  • pymemcache: dogpile always uses pymemcache's HashClient, which picks the server by rendezvous hashing (RendezvousHash). It does not distribute keys like python-memcached, so mixing pymemcache and python-memcached against several servers sends the same key to different servers: each one misses what the other wrote. With a single server, they interoperate (scenario 4).
  • Dead server: python-memcached marks it dead for memcache_dead_retry (300 s); the pool shares the mark among its clients (oslo_cache/_memcache_pool.py:285-321). pymemcache and redis-py raise instead (scenario 15).
  • memcache_pool_flush_on_reconnect: python-memcached sends flush_all to a server when it reconnects after marking it dead (flush_on_next_connect), so that keys rewritten elsewhere in the meantime are not read stale. This empties the whole server, for every service sharing it.

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