Skip to content

v0.20.0

Choose a tag to compare

@cachekit-release-bot cachekit-release-bot released this 30 Sep 03:48
49f722c

0.20.0 (2026-09-30)

⚠ BREAKING CHANGES

  • reliability: removed cachekit.reliability.create_decorator_config and, from cachekit.reliability.profiles, get_decorator_kwargs, minimal_reliability_decorator, balanced_reliability_decorator and full_reliability_decorator. Their output raised TypeError when passed to @cache, so no working call is lost. cachekit.decorators.wrapper.create_cache_wrapper no longer accepts undeclared keyword arguments; it raises TypeError instead of ignoring them.
  • decorators: FeatureOrchestrator (exported from cachekit.decorators.__all__) no longer defines generate_correlation_id, create_correlation_id, set_correlation_id, clear_correlation_id, start_request, end_request, or the correlation_tracker property. FeatureOrchestrator.handle_cache_error no longer accepts a correlation_id keyword argument. An explicit caller passing correlation_id= now flows it through **extra_context, and its log record is unchanged. The decorator's own *_failed error records no longer carry a correlation_id key; that value was a per-call UUID, or None, and nothing read it. src/cachekit/monitoring/correlation_tracking.py (CorrelationTracker, LoggerIntegratedTracker, module-level generate_correlation_id/set_correlation_id/get_correlation_id/clear_correlation_id/correlation_context) is removed entirely.
  • metrics: AsyncMetricsCollector.record_cache_operation() and FeatureOrchestrator.record_cache_operation() no longer accept hit. Callers that pass it now raise TypeError. FeatureOrchestrator.record_success() no longer emits a metrics record.
  • config: presets apply the canonical default TTLs; drop CACHEKIT_DEFAULT_TTL (LAB-4641) (#318)
  • intent: .secure rejects integrity_checking=False on both paths (LAB-3970) (#359)
  • interop: Existing deployments that use ns or nsapi as an interop namespace on Redis, Memcached or File backends worked before this change and must now rename the namespace. Renaming means a full cache miss for that namespace.
  • backends: if your deployment used cachekit under more than one tenant (a call with no tenant set counts as default), purge the Redis entries that earlier releases wrote. In earlier releases a decorated function that resolved Redis from the environment (CACHEKIT_REDIS_URL, REDIS_URL or the localhost default), or a backend taken from RedisBackendProvider.get_backend(), stayed bound to the tenant current when that backend was first obtained, so every tenant's L2 writes through it landed under that one tenant's t:<tenant>: prefix. That tenant keeps reading those entries as its own after the upgrade, and some hold another tenant's value. Entries written with ttl=None, or kept alive by refresh_ttl_on_get=True, never expire, and a no-argument invalidate_cache() reaches only those the calling tenant has read in that process since it started, because no key registry recorded them. After the last process running an earlier release has stopped, delete every t:* key in each database cachekit uses. Run this with the Python that cachekit is installed in, since it uses cachekit's redis-py dependency; it prints 0 keys left when it is done: python -c 'import itertools, sys, redis; r = redis.Redis.from_url(sys.argv[1]); keys = r.scan_iter(match=sys.argv[2], count=1000); [r.unlink(*b) for b in iter(lambda: list(itertools.islice(keys, 1000)), [])]; print(sum(1 for _ in r.scan_iter(match=sys.argv[2], count=1000)), "keys left")' '<redis-url>' 't:*'. Do not use a redis-cli --scan pipeline: a key set through key= can contain a newline or a NUL byte, which the pipeline turns into names that match nothing, so the key survives and the pipeline still exits 0. Run FLUSHDB instead only if that database is dedicated to cachekit. Then restart every process, because L1 keeps any entry a process read before the purge for up to the function's ttl (300 s with ttl=None), and expect a cold cache. On a shared database t:* also matches any other application's keys that start with t:, so run the same command with 't:<tenant>:*' in place of 't:*' for default and for each tenant you have set, the tenant percent-encoded with urllib.parse.quote(tenant, safe=''), an int or UUID tenant as its str() first (tenant org:123 is 't:org%3A123:*'). A deployment that only ever used one tenant is unaffected. RedisBackendProvider.get_backend() no longer binds its backend to one tenant: the backend follows tenant_context on every operation and falls back to the tenant current at the get_backend() call only when the calling context has none; for a backend bound to one tenant, construct cachekit.backends.redis.provider.PerRequestRedisBackend(client, tenant) directly, with client a redis.Redis. A no-argument invalidate_cache() on the Redis backend now deletes only the calling tenant's L2 entries. Set tenant_context only to a str, bytes, int or uuid.UUID: the Redis backend checks each operation's tenant and raises TypeError for any other type, including a bool, an IntEnum member and a bytearray, so convert an IntEnum member with int() and a bytearray with bytes() first. int and UUID tenants now work, keyed by their str() form (42 and "42" share t:42:), which no earlier release did.
  • backends: the decorator namespace "ck" and any "ck:*" namespace are now reserved for cachekit's internal keys and raise ConfigurationError at decoration time. Rename any function cached under namespace="ck" or a "ck:" prefix before upgrading.
  • decorators: configure the live circuit breaker from @cache(circuit_breaker=...) (LAB-5340) (#340)
  • decorators: refuse encryption with backend=None — L1-only held plaintext (LAB-4665) (#322)
  • logging: cachekit.logging.JsonFormatter is removed; importing it now raises ImportError. StructuredLogger.circuit_breaker_state_change and StructuredLogger.set_correlation_id are removed and now raise AttributeError. No deprecation aliases. Use set_trace_id / clear_trace_id (or cachekit.monitoring.correlation_tracking) for request correlation, and your own logging.Formatter subclass for JSON output.
  • reliability: cachekit.reliability.create_optimized_decorator_config is renamed to cachekit.reliability.create_decorator_config. Update imports; no alias is provided. cachekit.hash_utils.fast_hash is renamed to blake3_hash (module-internal, never exported from the package).
  • keys: cache keys for any serializer other than the default change identity on upgrade — no configuration change is required to be affected. A deployment using serializer="auto", "orjson", "arrow", or any serializer passed as an instance was writing :1s keys and will now write :1a, :1o, :1w or an x-prefixed code. That function's entire working set recomputes once at deploy, so plan a cold cache or roll out behind existing warm-up / stampede controls. Deployments on the default serializer are unaffected: their keys were already :1s and stay :1s. Orphaned entries are also a retention question, not only a hit-rate one: once the key changes, invalidate_cache() computes the new key and can no longer reach the old copy, so a deletion for erasure, consent withdrawal or permission revocation reports success while the pre-upgrade entry survives to its TTL — or indefinitely where ttl=None. Flush the affected namespaces on upgrade if you cache personal data rather than relying on expiry.
  • config: CachekitConfig drops retry_on_timeout, max_retries, retry_delay_ms, early_refresh_ratio, enable_corruption_detection and max_key_size, and no longer reads CACHEKIT_RETRY_ON_TIMEOUT, CACHEKIT_MAX_RETRIES, CACHEKIT_RETRY_DELAY_MS, CACHEKIT_EARLY_REFRESH_RATIO, CACHEKIT_ENABLE_CORRUPTION_DETECTION or CACHEKIT_MAX_KEY_SIZE. None of them changed behaviour. Passing one to CachekitConfig(...) now raises ValidationError; CachekitConfig ignores a still-exported env var, so startup is unaffected. CachekitIOBackendConfig declares its own max_retries under the same prefix and still parses CACHEKIT_MAX_RETRIES; this release does not change it. Integrity checking is set per decorator (integrity_checking=), refresh-ahead timing by L1CacheConfig.swr_threshold_ratio, and Memcached retries by MemcachedBackendConfig.retry_attempts.
  • decorators: CachekitIOBackend(...) configuration errors (missing, empty or whitespace-containing API key; invalid API URL) now raise cachekit.config.ConfigurationError at construction instead of ValueError or pydantic ValidationError, so code catching ValueError there must catch ConfigurationError. @cache.io(backend=...) and @cache.io(config=...) now raise ConfigurationError instead of silently ignoring the argument. Under set_default_backend(), @cache(config=...) now uses the backend inside config= instead of the module default. cachekit.backends.cachekitio.client.get_sync_http_client() is replaced by lease_sync_http_client(), which returns a SyncClientLease: hold the lease for as long as its .client is used, because the client is closed when the lease is dropped.
  • encryption: single-tenant tenant_id defaults to "default", not a deployment UUID (LAB-4666) (#321)

Features

  • backends: server-side key registry for cross-process whole-function invalidation (LAB-651) (#343) (d918a19)
  • build: publish CPython 3.14 wheels (LAB-6297) (#361) (63fbe14)
  • encryption: warn once when CACHEKIT_MASTER_KEY auto-activates encryption (LAB-4642) (#319) (6c8b1da)

Bug Fixes

  • backends: scope each Redis operation to the calling tenant, not the first caller (LAB-4773) (#329) (0386d44)
  • cache_handler: evict on corrupt CK frame header, not just payload (LAB-4075) (#307) (ed799c1)
  • cachekitio: drain lock POST/DELETE so a cancel cannot orphan a granted lock (LAB-3648) (#345) (288cb0e)
  • cachekitio: pin the hpack logger at INFO so DEBUG logs cannot expose the API key (LAB-5914) (#351) (3738339)
  • cachekitio: reject API keys outside the RFC 6750 bearer-token charset (LAB-5943) (#352) (c9c9a0e)
  • cachekitio: reject reserved key segments client-side (LAB-2880) (#364) (44b36f1)
  • config: presets apply the canonical default TTLs; drop CACHEKIT_DEFAULT_TTL (LAB-4641) (#318) (f838c60)
  • config: redact inputs from backend config ValidationErrors (LAB-5038) (#334) (c949454)
  • decorators: @cache.io accepts api_key= and rejects backend= (LAB-4643) (#320) (4d36167)
  • decorators: accept l1_enabled on intent presets and keep config= L1 tuning (LAB-4828) (#358) (3f5c1e0)
  • decorators: async miss-store records carry serializer/hit like sync (LAB-3755) (#295) (b733196)
  • decorators: configure the live circuit breaker from @cache(circuit_breaker=...) (LAB-5340) (#340) (a20cc69)
  • decorators: invalidate_cache resolves the same key the write path wrote (LAB-4387) (#312) (bebfc1d)
  • decorators: keep a key re-recorded during whole-function invalidation (LAB-5774) (#348) (c1b125b)
  • decorators: keep L2 decrypt and integrity failures out of circuit-breaker accounting (LAB-5865) (1b30f43)
  • decorators: raise TypeError for an unsupported tenant id on sync as on async (LAB-5713) (6612f39)
  • decorators: refuse encryption with backend=None — L1-only held plaintext (LAB-4665) (#322) (f5340f6)
  • encryption: classify a non-string original_type header as corruption, not tamper (LAB-4350) (#310) (2403e7e)
  • encryption: single-tenant tenant_id defaults to "default", not a deployment UUID (LAB-4666) (#321) (c5da48a)
  • encryption: surface keyring config faults as KeyringConfigurationError (LAB-4818) (93cafd3)
  • intent: .secure rejects integrity_checking=False on both paths (LAB-3970) (#359) (4f25185)
  • interop: key str-subclass segments by their exact str value (LAB-6196) (#360) (d067787)
  • interop: reject reserved namespaces ns and nsapi (LAB-5876) (#350) (6ba98a6)
  • keys: put the real serializer identity in the cache key (LAB-4351) (#311) (ee65250)
  • l1: restart the background cleanup thread in forked children (LAB-4772) (#328) (593c194)
  • metrics: drain queued records before batched worker exits (LAB-6351) (#365) (385229b)
  • metrics: record each cache operation once in cache_operations_total (LAB-3761) (#346) (d317494)
  • metrics: share Prometheus metric objects across collectors (LAB-6355) (#366) (e41c868)
  • redis: classify ClusterDownError as TRANSIENT (LAB-5327) (#339) (e93b693)
  • redis: classify TryAgainError transient, InvalidResponse/LockError permanent (LAB-5353) (#344) (fb9baa6)
  • redis: release a lock won after acquire_lock cancellation (LAB-3606) (#293) (4cfdc63)
  • reliability: circuit breaker recovers from OPEN on the decorator path (LAB-5326) (#341) (9182092)
  • serializers: integrity-off AutoSerializer refuses envelope-shaped values at write (LAB-6401) (#377) (ea1a039)
  • serializers: refuse verified envelopes on integrity-off StandardSerializer reads (LAB-4329) (#379) (9502662)
  • serializers: settle the envelope format by agreement, not by a winner (LAB-2736) (#309) (165c9b5)
  • serializers: treat an entry with no serializer name as a mismatch (LAB-4432) (#373) (b3869a7)

Code Refactoring

  • config: remove six CachekitConfig knobs nothing reads (LAB-4740) (#324) (27e1f95)
  • decorators: remove dead correlation-ID surface (LAB-4733) (#323) (841dba6)
  • logging: remove circuit_breaker_state_change, set_correlation_id and JsonFormatter (LAB-4638) (#317) (0a71361)
  • reliability: remove create_decorator_config family (LAB-4637) (#338) (4214448)
  • reliability: rename create_optimized_decorator_config and fast_hash (LAB-4619) (#315) (8d49e38)