diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 788b0fa7..97bce112 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.22.0" + ".": "0.23.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 08fe8b86..a3d80d79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,72 @@ # Changelog +## [0.23.0](https://github.com/cachekit-io/cachekit-py/compare/v0.22.0...v0.23.0) (2026-10-12) + + +### ⚠ BREAKING CHANGES + +* **logging:** CachekitConfig.log_buffer_size, CachekitConfig.log_batch_size and CachekitConfig.log_flush_interval are removed, along with their env vars CACHEKIT_LOG_BUFFER_SIZE, CACHEKIT_LOG_BATCH_SIZE and CACHEKIT_LOG_FLUSH_INTERVAL. They configured the background log writer thread, which is deleted: structured log entries are now written synchronously. Passing one of these fields to the constructor now raises ValidationError. A still-exported env var is ignored, so startup is unaffected. +* **config:** refuse a malformed deployment UUID at load without quoting it (LAB-8772) ([#570](https://github.com/cachekit-io/cachekit-py/issues/570)) +* **config:** refuse an invalid assignment to the loaded settings and write nothing (LAB-8739) ([#567](https://github.com/cachekit-io/cachekit-py/issues/567)) +* **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)) +* **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. +* **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. +* **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. +* **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()`. +* **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`. +* **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. +* **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. +* **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. + +### Bug Fixes + +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **config:** refuse a malformed deployment UUID at load without quoting it (LAB-8772) ([#570](https://github.com/cachekit-io/cachekit-py/issues/570)) ([277aac3](https://github.com/cachekit-io/cachekit-py/commit/277aac3206d5095ec6b48e266747056be68b907e)) +* **config:** refuse an invalid assignment to the loaded settings and write nothing (LAB-8739) ([#567](https://github.com/cachekit-io/cachekit-py/issues/567)) ([29c6a59](https://github.com/cachekit-io/cachekit-py/commit/29c6a5946c38a15c6457910ca502b1ee9e37a410)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **decorators:** start no SWR refresh thread in a child forked without at-fork hooks (LAB-9129) ([#573](https://github.com/cachekit-io/cachekit-py/issues/573)) ([dc69e28](https://github.com/cachekit-io/cachekit-py/commit/dc69e2811794d0d8b50f88f68dd5f31931e9d93b)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **logging:** write structured entries synchronously so a forked child keeps them (LAB-9130) ([#572](https://github.com/cachekit-io/cachekit-py/issues/572)) ([e06cc4c](https://github.com/cachekit-io/cachekit-py/commit/e06cc4c785ce8dd3eca2ac2daabe8017dbbe919e)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) + + +### Performance Improvements + +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) +* **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)) + ## [0.22.0](https://github.com/cachekit-io/cachekit-py/compare/v0.21.0...v0.22.0) (2026-10-04) diff --git a/Cargo.lock b/Cargo.lock index ffaa3406..d351b87d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,7 +280,7 @@ dependencies = [ [[package]] name = "cachekit-rs" -version = "0.22.0" +version = "0.23.0" dependencies = [ "cachekit-core", "criterion 0.8.2", diff --git a/pyproject.toml b/pyproject.toml index fc36e5c2..9b58ebb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "cachekit" -version = "0.22.0" +version = "0.23.0" description = "Backend-agnostic caching for Python — intent-based decorators with circuit breaker, distributed locking, Prometheus metrics, and optional zero-knowledge AES-256-GCM encryption, on a Rust-powered core. Zero-config L1 in-memory; scales to Redis, Memcached, File, or CachekitIO." readme = "README.md" license = {text = "MIT"} diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 7976a3d9..dbd57ecc 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cachekit-rs" -version = "0.22.0" +version = "0.23.0" edition = "2021" authors = ["cachekit Contributors"] description = "High-performance storage engine for caching with compression and encryption" diff --git a/src/cachekit/__init__.py b/src/cachekit/__init__.py index 2ad50d33..0d191679 100644 --- a/src/cachekit/__init__.py +++ b/src/cachekit/__init__.py @@ -68,7 +68,7 @@ def custom_function(): ``` """ -__version__ = "0.22.0" +__version__ = "0.23.0" from collections.abc import Callable from typing import Any, TypeVar diff --git a/uv.lock b/uv.lock index 96b855e4..e41be542 100644 --- a/uv.lock +++ b/uv.lock @@ -214,7 +214,7 @@ filecache = [ [[package]] name = "cachekit" -version = "0.22.0" +version = "0.23.0" source = { editable = "." } dependencies = [ { name = "blake3" },