|
1 | 1 | # Changelog |
2 | 2 |
|
| 3 | +## [0.23.0](https://github.com/cachekit-io/cachekit-py/compare/v0.22.0...v0.23.0) (2026-10-10) |
| 4 | + |
| 5 | + |
| 6 | +### ⚠ BREAKING CHANGES |
| 7 | + |
| 8 | +* **decorators:** share one call between concurrent misses on a key, run in its own task (LAB-7441) ([#547](https://github.com/cachekit-io/cachekit-py/issues/547)) |
| 9 | +* **redis:** code that reads redis-py's exception off the error stops seeing it. `BackendError.original_exception` and `__cause__` for a redis-py failure are now a `RedisClientError`, not redis-py's exception. Code that checked `isinstance(err.original_exception, redis.exceptions.AuthenticationError)` should check `issubclass(err.original_exception.exc_type, ...)`. A `RedisBackend` error carried no `original_exception` before; it now carries the same `RedisClientError`. `repr(RedisBackendConfig(...))` and `str(...)` no longer show `redis_url`; the attribute is unchanged. |
| 10 | +* **observability:** the tracing surface, which never traced, is removed: `MonitoringConfig.enable_tracing` (passing it raises `TypeError`), the `enable_tracing` key of `DecoratorConfig.to_dict()`, `create_cache_wrapper(enable_tracing=)`, `FeatureOrchestrator.create_span`, `FeatureOrchestrator.set_span_attributes`, `FeatureOrchestrator.record_exception`, `handle_cache_error(span=)`, and `StructuredLogger.set_trace_id` / `clear_trace_id`. A failed sync client creation or L2 GET no longer records a second `cache_operations_total` sample with `operation="exception"`; it records one failure sample, as the async paths do. |
| 11 | +* **config:** `master_key=` on `@cache`, `@cache.secure`, `DecoratorConfig.secure()`, `EncryptionConfig`, `CacheSerializationHandler` and `validate_encryption_config` raises `TypeError` for a `bytes`, `bytearray` or `memoryview` key: pass `key.hex()`. Only `EncryptionWrapper` takes raw key bytes. With encryption on, such a key already failed, with `AttributeError`. With encryption unset, the decorator raised `ConfigurationError` for it, and now raises `TypeError`. With encryption off (`encryption=False`, `EncryptionConfig(enabled=False)`) it was accepted, and failed only when an encrypted entry was read; it now raises where it is passed. On Python 3.14, `CacheSerializationHandler` read a bytes key holding ASCII hex digits as that hex; it now raises too. |
| 12 | +* **decorators:** `@cache(config=..., encryption=...)` raises `ConfigurationError` when the config is encrypted (`DecoratorConfig.secure(...)`, or a config built with `encryption=EncryptionConfig(enabled=True, ...)`), and `@cache(config=DecoratorConfig.io(...), backend=...)` raises `ConfigurationError`. A keyword that names no `DecoratorConfig` field raises `ConfigurationError`, not `TypeError`, from `@cache`, from every preset but `@cache.local`, from the `DecoratorConfig` preset classmethods and beside `config=`; so does `encryption=` on `@cache.secure` and `DecoratorConfig.secure()`. `EncryptionWrapper(master_key=...)` raises `EncryptionError`, and `previous_master_keys=` raises `KeyringConfigurationError`, for a raw key that is not exactly 32 bytes: pass `bytes.fromhex(hex_key)`, never `hex_key.encode()`. |
| 13 | +* **serializers:** `EncryptionWrapper` now raises `ConfigurationError` at construction when its serializer's class does not declare `cross_sdk_compatible = True`. `AutoSerializer` and unmarked custom serializers are refused. This is a breaking change for code that builds the wrapper directly around `AutoSerializer`. |
| 14 | +* **cachekitio:** a subdomain of the two API hosts now needs `CACHEKIT_ALLOW_CUSTOM_HOST=true`. A custom host with non-ASCII characters must be given in its ASCII (`xn--`) form. |
| 15 | +* **decorators:** `DecoratorConfig().backend` is `UNSET`, not `None`. `UNSET` is falsy, so `if config.backend:` reads as before, and `to_dict()` still reports an unset backend as `None`. Code that tests `config.backend is None` on an unset backend must test `is UNSET` instead. Type checkers now see `UNSET` in the field's type, so narrowing with `is not None` and then using the value as a backend is flagged; narrow with `if config.backend:` or `is not UNSET`. `DecoratorConfig(..., backend=None)` and every preset with `backend=None` now mean L1-only instead of "resolve the default backend". To keep the old behaviour, omit `backend` from the config. `DecoratorConfig.secure(..., backend=None)` and an interop config with `backend=None`, which decorated before, now raise `ConfigurationError` at decoration. |
| 16 | +* **encryption:** encrypted entries written by earlier releases through a prefixing backend (`MemcachedBackend` with a `key_prefix`, the tenant-scoped Redis backend from env auto-detection or `RedisBackendProvider`, or a custom backend with a non-empty `key_prefix`) no longer authenticate. By default each one is read once as a miss, counted as `auth_tamper`, recomputed and overwritten. With `fail_closed=True`, every read of one raises `DecryptionAuthenticationError` until it expires or is deleted. Delete those entries, or move the cache to a fresh key space (a new `namespace` or Memcached `key_prefix`), rather than wait out the TTL. During a rolling upgrade, old and new processes read each other's writes as failed authentication, so give the new release a fresh key space or stop the old processes first. There is no fallback to the earlier AAD. Unaffected: a `RedisBackend` you construct yourself, `FileBackend`, `CachekitIOBackend`, and interop mode. |
| 17 | + |
| 18 | +### Bug Fixes |
| 19 | + |
| 20 | +* **backends:** declare with_timeout's decorated shape and restore the pool timeout after overlapping windows (LAB-4489) ([#558](https://github.com/cachekit-io/cachekit-py/issues/558)) ([93d4d93](https://github.com/cachekit-io/cachekit-py/commit/93d4d93b2b0339a1cb94ea4a167b0d4a46780012)) |
| 21 | +* **backends:** drop with_timeout windows a forked child inherits (LAB-4489) ([#559](https://github.com/cachekit-io/cachekit-py/issues/559)) ([70ffd59](https://github.com/cachekit-io/cachekit-py/commit/70ffd594e65fdcb26a6d86cb3f244602fa45b51c)) |
| 22 | +* **cachekitio:** hold a bytes api_key only wrapped when a later check raises (LAB-8288) ([#529](https://github.com/cachekit-io/cachekit-py/issues/529)) ([707aad2](https://github.com/cachekit-io/cachekit-py/commit/707aad2696b206226df7cfca729b9863b576499c)) |
| 23 | +* **cachekitio:** keep the API key off a failed request's exception chain (LAB-8304) ([#539](https://github.com/cachekit-io/cachekit-py/issues/539)) ([6919e13](https://github.com/cachekit-io/cachekit-py/commit/6919e13d3509de98ea2bd1e9ac97a103153e01f3)) |
| 24 | +* **cachekitio:** keep the API key off an interrupt raised mid-request (LAB-8339) ([#541](https://github.com/cachekit-io/cachekit-py/issues/541)) ([966f25f](https://github.com/cachekit-io/cachekit-py/commit/966f25f4275a2ad7bc0752d043962de6b22e1d58)) |
| 25 | +* **cachekitio:** re-lease a client that close_http_clients() closed (LAB-8032) ([#533](https://github.com/cachekit-io/cachekit-py/issues/533)) ([f2bf102](https://github.com/cachekit-io/cachekit-py/commit/f2bf102d75b0da14e1bf94536de5c646abb19d9a)) |
| 26 | +* **cachekitio:** send X-CacheKit-TTL alone and ceil sub-second TTLs (LAB-8206) ([#520](https://github.com/cachekit-io/cachekit-py/issues/520)) ([ead1a09](https://github.com/cachekit-io/cachekit-py/commit/ead1a097662fa92f1562688e8de9d567db80cde6)) |
| 27 | +* **cachekitio:** validate the host the HTTP client connects to (LAB-8207) ([#519](https://github.com/cachekit-io/cachekit-py/issues/519)) ([f38fd55](https://github.com/cachekit-io/cachekit-py/commit/f38fd55129119a2982ac83b3511a5711fd6ffa2f)) |
| 28 | +* **circuit-breaker:** count a probe's outcome only toward the cycle that admitted it (LAB-5374) ([#565](https://github.com/cachekit-io/cachekit-py/issues/565)) ([a2c5f2c](https://github.com/cachekit-io/cachekit-py/commit/a2c5f2ce31405772c67be280e20391ab007589db)) |
| 29 | +* **config:** refuse a bytes master_key where a hex string is taken (LAB-8281) ([#526](https://github.com/cachekit-io/cachekit-py/issues/526)) ([22d330b](https://github.com/cachekit-io/cachekit-py/commit/22d330b45c43387eb25df20ef9f7ea4d40e840f5)) |
| 30 | +* **decorators:** count the async backed miss in cache_info() (LAB-8298) ([#538](https://github.com/cachekit-io/cachekit-py/issues/538)) ([904e4bb](https://github.com/cachekit-io/cachekit-py/commit/904e4bbe86af580fdde46aed3fcbb531b839c585)) |
| 31 | +* **decorators:** keep backend=None L1-only on reused decorators and in DecoratorConfig (LAB-8059) ([#515](https://github.com/cachekit-io/cachekit-py/issues/515)) ([da70f3a](https://github.com/cachekit-io/cachekit-py/commit/da70f3a0abad86ba5a2c4b4943453806fb68bf7f)) |
| 32 | +* **decorators:** refuse an out-of-model interop return on uncached calls (LAB-5375) ([#530](https://github.com/cachekit-io/cachekit-py/issues/530)) ([2e1fba5](https://github.com/cachekit-io/cachekit-py/commit/2e1fba57701de4847f29d6a3dcb1e1cc0a7b6e25)) |
| 33 | +* **decorators:** reject keywords a preset does not take, and raw keys that are not 32 bytes (LAB-8223) ([#524](https://github.com/cachekit-io/cachekit-py/issues/524)) ([9b3eb30](https://github.com/cachekit-io/cachekit-py/commit/9b3eb304a138d6a2b48feec426c243fc0463e520)) |
| 34 | +* **decorators:** restore the stats context on every sync exit, interrupts included (LAB-8741) ([#568](https://github.com/cachekit-io/cachekit-py/issues/568)) ([d2dc83a](https://github.com/cachekit-io/cachekit-py/commit/d2dc83a58afb7ce352afaf573d5b36aab08b4599)) |
| 35 | +* **decorators:** time each latency sample over the operation it names (LAB-7148) ([#554](https://github.com/cachekit-io/cachekit-py/issues/554)) ([e1a4f8f](https://github.com/cachekit-io/cachekit-py/commit/e1a4f8fe70adc5ecaaa4fac55f2a7142bd03a9fc)) |
| 36 | +* **encryption:** bind the backend key prefix into the AES-GCM AAD (LAB-8115) ([#518](https://github.com/cachekit-io/cachekit-py/issues/518)) ([30201ee](https://github.com/cachekit-io/cachekit-py/commit/30201ee9999b16df213f55b69d14615c3174c808)) |
| 37 | +* **encryption:** refuse bare Arrow after decrypt and a non-bool compressed AAD token (LAB-8182) ([#517](https://github.com/cachekit-io/cachekit-py/issues/517)) ([256f01d](https://github.com/cachekit-io/cachekit-py/commit/256f01df5e41f73a7a4584ac4904b8d95c5daee0)) |
| 38 | +* **encryption:** refuse master_key= repeated in previous keys at decoration (LAB-8701) ([#563](https://github.com/cachekit-io/cachekit-py/issues/563)) ([97bc0b6](https://github.com/cachekit-io/cachekit-py/commit/97bc0b6ca0af651a872bba2a09acd963e18c2441)) |
| 39 | +* **file:** expire a File-backend entry at its expiry second (LAB-8073) ([#514](https://github.com/cachekit-io/cachekit-py/issues/514)) ([9312ba7](https://github.com/cachekit-io/cachekit-py/commit/9312ba7a02ca6663cef00baec1e18116d3712c1c)) |
| 40 | +* **interop:** decode non-string map keys; run interop-mode 1.3.0 and decode-bounds 1.2.0 vectors (LAB-8307) ([#537](https://github.com/cachekit-io/cachekit-py/issues/537)) ([819460b](https://github.com/cachekit-io/cachekit-py/commit/819460b4ac6003a92964ca84be9ad8b912d82053)) |
| 41 | +* **observability:** remove the no-op tracing surface (LAB-6369) ([#546](https://github.com/cachekit-io/cachekit-py/issues/546)) ([ba6368b](https://github.com/cachekit-io/cachekit-py/commit/ba6368b210fa600fc2f414c6474c92eda74740a2)) |
| 42 | +* **redis:** clear an interrupt's frames on its way out of a Redis operation (LAB-8396) ([#564](https://github.com/cachekit-io/cachekit-py/issues/564)) ([09c2b8a](https://github.com/cachekit-io/cachekit-py/commit/09c2b8aaab33b485fadaa94c3739aedfc76c512b)) |
| 43 | +* **redis:** free a failed op's frames on LOADING/WRONGPASS, and log a release failure after the lock block raised (LAB-8383) ([#551](https://github.com/cachekit-io/cachekit-py/issues/551)) ([67006b5](https://github.com/cachekit-io/cachekit-py/commit/67006b5ee31c4f7f828cae5353374e3678df001f)) |
| 44 | +* **redis:** keep the Redis password off a failed operation's traceback (LAB-8338) ([#542](https://github.com/cachekit-io/cachekit-py/issues/542)) ([84ec76c](https://github.com/cachekit-io/cachekit-py/commit/84ec76c14ec624fa7f3b3a027a39326345ca2ea7)) |
| 45 | +* **serializers:** EncryptionWrapper refuses a serializer that is not cross-SDK (LAB-8195) ([#521](https://github.com/cachekit-io/cachekit-py/issues/521)) ([fc45785](https://github.com/cachekit-io/cachekit-py/commit/fc45785cbf01570c5b2c2a967516120c4bf77d10)) |
| 46 | +* **serializers:** raise SerializationError when numpy or pandas cannot load on decode (LAB-8131) ([#534](https://github.com/cachekit-io/cachekit-py/issues/534)) ([7aeb26c](https://github.com/cachekit-io/cachekit-py/commit/7aeb26c112608f02bdad14777fed9f3cf69c77d1)) |
| 47 | +* **serializers:** skip the envelope probe on decrypted StandardSerializer reads (LAB-8371) ([#544](https://github.com/cachekit-io/cachekit-py/issues/544)) ([1e779a0](https://github.com/cachekit-io/cachekit-py/commit/1e779a0ca45ff231788dab13197e8de20d076f89)) |
| 48 | +* **tests:** draw redis_proc ports below the Kubernetes NodePort range (LAB-8072) ([#516](https://github.com/cachekit-io/cachekit-py/issues/516)) ([78d1fa6](https://github.com/cachekit-io/cachekit-py/commit/78d1fa6b33db38b29b5261249f7fe142a2df4128)) |
| 49 | + |
| 50 | + |
| 51 | +### Performance Improvements |
| 52 | + |
| 53 | +* **cachekitio:** free a failed request's frames without the cyclic GC (LAB-8378) ([#552](https://github.com/cachekit-io/cachekit-py/issues/552)) ([c0b5cab](https://github.com/cachekit-io/cachekit-py/commit/c0b5cabfcf2fd10cff5b3c40893a90018d39d395)) |
| 54 | +* **decorators:** build the L2 cache handler once per backend; class-based backpressure permit (LAB-7112) ([#553](https://github.com/cachekit-io/cachekit-py/issues/553)) ([25bbabe](https://github.com/cachekit-io/cachekit-py/commit/25bbabeb63531e37fa6cc4c8ea7b86b98866b976)) |
| 55 | +* **decorators:** share one call between concurrent misses on a key, run in its own task (LAB-7441) ([#547](https://github.com/cachekit-io/cachekit-py/issues/547)) ([b8967d3](https://github.com/cachekit-io/cachekit-py/commit/b8967d33fd178af553c56c885e9979f85bdd56f1)) |
| 56 | +* **file:** keep entry count and bytes in counters instead of two directory scans per set (LAB-7107) ([#512](https://github.com/cachekit-io/cachekit-py/issues/512)) ([b1cfc89](https://github.com/cachekit-io/cachekit-py/commit/b1cfc896f602de44027b0da39ba5528466a51384)) |
| 57 | +* **file:** narrow the FileBackend lock to the rename commit and resolve cache_dir once (LAB-7076) ([#535](https://github.com/cachekit-io/cachekit-py/issues/535)) ([6ad08ae](https://github.com/cachekit-io/cachekit-py/commit/6ad08ae8f3c42a56f298e44e242e15bdb4a1619a)) |
| 58 | +* **ir:** ratchet four stale perf-ir budgets to main's cost (LAB-8629) ([#556](https://github.com/cachekit-io/cachekit-py/issues/556)) ([1c049a1](https://github.com/cachekit-io/cachekit-py/commit/1c049a19a12861fe0e5ab34cc9f711dc864019fa)) |
| 59 | +* **serializers:** load numpy, pandas and pyarrow on first use, not at import cachekit (LAB-7093) ([#509](https://github.com/cachekit-io/cachekit-py/issues/509)) ([9533b55](https://github.com/cachekit-io/cachekit-py/commit/9533b553e78f60f360c7a8603a21c59e3bfc8913)) |
| 60 | +* **serializers:** skip the standard object_hook's .get() calls on non-marker maps (LAB-7113) ([#508](https://github.com/cachekit-io/cachekit-py/issues/508)) ([3db2941](https://github.com/cachekit-io/cachekit-py/commit/3db2941b214667ec929a4058a3d418e30611e34a)) |
| 61 | +* **telemetry:** skip disabled cache-op logging and bind each Prometheus series once (LAB-7067) ([#555](https://github.com/cachekit-io/cachekit-py/issues/555)) ([fef0bb1](https://github.com/cachekit-io/cachekit-py/commit/fef0bb1cf818158af924a66bd48bbeafa49ecb3d)) |
| 62 | + |
3 | 63 | ## [0.22.0](https://github.com/cachekit-io/cachekit-py/compare/v0.21.0...v0.22.0) (2026-10-04) |
4 | 64 |
|
5 | 65 |
|
|
0 commit comments