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).
| System | Protocol | Library (backend) | Direction |
|---|---|---|---|
| memcached | text protocol, TCP 11211 | python-memcached (dogpile.cache.memcached, oslo_cache.memcache_pool), pymemcache (dogpile.cache.pymemcache), pylibmc | synchronous request/response, from each service process |
| memcached with TLS or SASL | binary protocol | python-binary-memcached (oslo_cache.memcache_pool with tls_enabled or memcache_sasl_enabled, dogpile.cache.bmemcached) | same |
| Redis / Sentinel | RESP (redis-py 8 negotiates RESP3 with HELLO 3) | redis-py (dogpile.cache.redis, dogpile.cache.redis_sentinel) | same |
| etcd v3 | HTTP/JSON gateway | etcd3gw (oslo_cache.etcd3gw) | same |
Nothing is published or consumed asynchronously: every exchange is caused by a call of the service (05).
1. Keys
- The service chooses the key: a string for
region.get/set/delete, or the memoization key<module>:<function>|<arg1> <arg2>…(default values included,self/clsdropped; 01 § 3). - 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 byoslo_cache/core.py:498-499).- The
xmlcharrefreplacefallback never applies: UTF-8 encodes anystr(scenario 12 of 07). - There is no prefix and no namespace: two services that hash the same string share the entry.
- The
- 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:
| Backend | memcached flags | Encoding 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
| Operation | memcached | Redis |
|---|---|---|
| read | get <key> (get k1 k2… for get_multi) | GET / MGET |
| write | set <key> 1 <exptime> <n> (set_multi = one set per key) | SET <key> <value>, or SETEX <key> <ttl> <value> |
| delete | delete <key> (one per key for delete_multi) | DEL |
| locks | none 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'scache_time). It is not sent to the server. The reader comparesnow - ctwith its own expiration time and regenerates the value if it is older (scenarios 6 and 7). - Server lifetime =
backend_expiration_time, only if set:exptimeofset(memcached) orSETEX(Redis). By default it is 0: memcached keeps the item until eviction (scenario 5, 18 and 19).
Invalidation:
region.delete(key)sendsdelete: 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_maxsizepython-memcached clients per process, each with one TCP connection per server, closed aftermemcache_pool_unused_timeoutof inactivity (oslo_cache/_memcache_pool.py:226-249). python-memcached picks the server as((crc32(key) & 0xffffffff) >> 16) & 0x7fffmodulo the number of servers inmemcache_servers. If that server is dead, it retries up to 10 times with the hash ofstr(hash) + str(i)(python-memcachedcmemcache_hashandClient._get_server). The order of the list therefore matters for every client sharing the cache. - **
dogpile.cache.memcached**: one client per thread (dogpile'sClientPool(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 sendsflush_allto 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.