From 9f6ef06c73cff378c30be388425d0e5ea700756d Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Tue, 21 Jul 2026 01:05:02 +1000 Subject: [PATCH 1/2] docs(matrix): correct backend rows + add backend-abstraction section (LAB-273 audit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend-parity audit findings, verified against code (py 0.12.0, rs 0.3.0, ts 0.1.2): - DynamoDB ships in NO SDK. The Python ✅ traced to the custom-backend tutorial (docs/backends/custom.md) — an extension-point example, not shipped support. Row corrected to ❌ across the board. - TS Cloudflare Workers cell N/A → ❌: Workers is a plausible target for TS but blocked by NAPI-native crypto and ioredis; N/A misstates it. - New 'Backend Abstraction' section: required interface per SDK (health required py/rs, absent ts; close()+keyPrefix ts-only), optional capability interfaces, per-backend implementation coverage, and the lock-API shape divergence (py context-manager vs rs/ts lock_id token). - Reliability rows qualified per backend: Python locking is Redis+SaaS (not SaaS-only); rs/ts locking SaaS-only; TS TTL inspection SaaS-only. - Lock-id header migration (CWE-532) recorded complete in all three SDKs: py#131 closed, rs#24 closed, ts ships the header (ts#63 open only for an unrelated NAPI-rebuild item). - Backend-selection note: Python is the only SDK with env backend auto-detection; rs/ts env vars are SaaS-credential-only. Recorded as a finding, not a prescription. - py version 0.11.1 → 0.12.0 (v0.12.0 tag exists). Co-authored-by: multica-agent --- sdk-feature-matrix.md | 61 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/sdk-feature-matrix.md b/sdk-feature-matrix.md index 05c5fce..df8b48c 100644 --- a/sdk-feature-matrix.md +++ b/sdk-feature-matrix.md @@ -6,7 +6,7 @@ **Feature parity and compliance status across all CacheKit SDK implementations.** -*Last updated: 2026-07-20 — LAB-381 stale-while-revalidate (server stale-grace) specified in [spec/saas-api.md](spec/saas-api.md#stale-while-revalidate)* +*Last updated: 2026-07-21 — LAB-273 backend-parity audit: DynamoDB corrected (ships nowhere), backend-abstraction section added, locking/TTL rows qualified per backend, lock-id header migration recorded complete* @@ -18,6 +18,7 @@ - [Core Features](#core-features) - [Encryption](#encryption) - [Cache Backends](#cache-backends) +- [Backend Abstraction](#backend-abstraction) - [Reliability Features](#reliability-features) - [Developer Experience](#developer-experience) - [Protocol Compliance](#protocol-compliance) @@ -29,7 +30,7 @@ | SDK | Package | Version | Language | Status | | :--- | :--- | :---: | :--- | :---: | -| cachekit-py | `cachekit` (PyPI) | 0.11.1 | Python 3.10+ | ✅ Production | +| cachekit-py | `cachekit` (PyPI) | 0.12.0 | Python 3.10+ | ✅ Production | | cachekit-rs | `cachekit-rs` (crates.io) | 0.3.0 | Rust 1.82+ | ✅ Production | | cachekit-core | `cachekit-core` (crates.io) | 0.3.0 | Rust (shared core) | ✅ Production | | cachekit-ts | `@cachekit-io/cachekit` (npm) | 0.1.2 | TypeScript | ✅ Production | @@ -70,12 +71,48 @@ | Backend | Python | Rust | TypeScript | PHP | | :--- | :---: | :---: | :---: | :---: | -| Redis (direct) | ✅ | ✅ via fred | ✅ | ❌ | -| Memcached | ✅ (env auto-detect) | ❌ | ❌ | ❌ | -| File (local) | ✅ (env auto-detect) | ❌ | ❌ | ❌ | -| CacheKit SaaS (HTTP) | ✅ | ✅ reqwest (native) + fetch (Workers) | ✅ | 🔜 Planned | -| Cloudflare Workers | N/A | ✅ `workers` feature | N/A | N/A | -| DynamoDB | ✅ | ❌ | ❌ | ❌ | +| Redis (direct) | ✅ `backends/redis` (redis-py, required dep) | ✅ `redis` feature (fred) | ✅ `backends/redis.ts` (ioredis) | ❌ | +| Memcached | ✅ `backends/memcached` (`memcached` extra) | ❌ | ❌ | ❌ | +| File (local) | ✅ `backends/file` (stdlib + mmap) | ❌ | ❌ | ❌ | +| CacheKit SaaS (HTTP) | ✅ `backends/cachekitio` (httpx) | ✅ `cachekitio` feature (reqwest, default) | ✅ `backends/cachekitio.ts` (fetch) | 🔜 Planned | +| Cloudflare Workers | N/A | ✅ `workers` feature (`worker::Fetch`) | ❌ Node 20+ only¹ | N/A | +| DynamoDB | ❌² | ❌ | ❌ | ❌ | + +> ¹ The TS SDK cannot run on the Workers runtime today: crypto is a native NAPI module and the Redis backend is ioredis (TCP). Recorded as ❌ (a plausible target, unsupported) rather than N/A. +> +> ² DynamoDB has never shipped in any SDK. The previous Python ✅ traced to the [custom-backend tutorial](https://github.com/cachekit-io/cachekit-py/blob/main/docs/backends/custom.md), which shows how a *user* can implement the backend protocol against DynamoDB — that is an extension point, not shipped support (LAB-273). + +**Backend selection / env auto-detection:** Python is the only SDK that auto-detects *which* backend to use from the environment — a single unambiguous selector (`CACHEKIT_API_KEY` → SaaS, `CACHEKIT_REDIS_URL` → Redis, `CACHEKIT_MEMCACHED_SERVERS` → Memcached, `CACHEKIT_FILE_CACHE_DIR` → File; fallback `REDIS_URL` / localhost Redis; setting more than one selector raises `ConfigurationError`). Rust `CacheKit::from_env()` reads SaaS credentials only (`CACHEKIT_API_KEY` / `CACHEKIT_API_URL` / `CACHEKIT_MASTER_KEY` / `CACHEKIT_DEFAULT_TTL`) and always builds the CachekitIO backend — Redis is wired explicitly via `.backend()`. TypeScript has no backend auto-detection: each preset fixes the backend type and env vars (`CACHEKIT_API_KEY`, `CACHEKIT_MASTER_KEY`) serve only as credential fallbacks. Whether rs/ts should gain selector-style auto-detection is an open product question, not a recorded decision (LAB-273 finding). + +--- + +## Backend Abstraction + +The contract a storage backend must satisfy per SDK (bytes in / bytes out; serialization and encryption live above the backend). Audited against code 2026-07-21 (LAB-273). + +### Required interface + +| Aspect | Python (`BaseBackend`) | Rust (`Backend`) | TypeScript (`Backend`) | +| :--- | :--- | :--- | :--- | +| Form | PEP 544 structural protocol, `@runtime_checkable` | `#[async_trait]`, `Send + Sync` (auto `?Send` on wasm32 / `unsync` feature) | interface (structural) | +| Core ops | `get` / `set` / `delete` / `exists` | `get` / `set` / `delete` / `exists` | `get` / `set` / `delete` / `exists` | +| Health check | ✅ `health_check() -> (bool, details)` required | ✅ `health() -> HealthStatus` required | ❌ not in the interface (SaaS backend has an internal check) | +| Lifecycle | — | — | ✅ `close()` required | +| Interop key-prefix guard | — | — | ✅ optional `readonly keyPrefix` (interop mode fails closed on hidden prefixes) | + +### Optional capabilities — and which shipped backends implement them + +| Capability | Python | Rust | TypeScript | +| :--- | :--- | :--- | :--- | +| TTL inspect / refresh | `TTLInspectableBackend` — Redis ✅, SaaS ✅, Memcached/File ❌ | `TtlInspectable` — Redis ✅, SaaS ✅, Workers ❌ | `TTLBackend` — SaaS ✅ (`TTLCachekitIO`), Redis ❌ | +| Distributed locking | `LockableBackend` — Redis ✅ (`redis.lock.Lock`), SaaS ✅ | `LockableBackend` — SaaS ✅, Redis ❌, Workers ❌ | `LockableBackend` — SaaS ✅ (`LockableCachekitIO`), Redis ❌ | +| Per-operation timeout | `TimeoutConfigurableBackend` — Redis ✅ (SaaS ships a non-protocol `with_timeout` variant) | — no equivalent | — no equivalent | +| Zero-copy buffer read | `BufferReadableBackend` / `BufferHandle` — File ✅ (mmap; #171) | — no equivalent | — no equivalent | + +> [!NOTE] +> **Lock API shape divergence:** Python's `acquire_lock` is an async context manager yielding `bool` — the lock token stays internal and release is automatic. Rust and TypeScript return the raw `lock_id` capability token from `acquire_lock`/`acquireLock` and require an explicit `release_lock(key, lock_id)` — a direct mirror of the SaaS lock endpoint. All three pass the **bare cache key** (backends own any `:lock` namespace derivation). Porting a lockable backend across SDKs must bridge this shape difference. +> +> **Coverage, not shape, is the parity gap:** all three SDKs use the same required-base + optional-capability pattern, but only Python implements the optional capabilities on Redis. Rust's Redis backend lacks locking; TypeScript's Redis backend lacks both locking and TTL inspection; Rust's Workers backend lacks both despite speaking the same SaaS API (gap tickets under LAB-102). --- @@ -85,13 +122,13 @@ | :--- | :---: | :---: | :---: | :---: | | Circuit breaker | ✅ | ❌ | ✅ | ❌ | | Backpressure | ✅ | ❌ | ⚠️ Concurrent refresh limits | ❌ | -| Distributed locking | ✅ | ✅ SaaS backend (`LockableBackend`) | ✅ SaaS backend only | ❌ | +| Distributed locking | ✅ Redis + SaaS backends | ✅ SaaS backend only (`LockableBackend`; Redis/Workers lack impls) | ✅ SaaS backend only | ❌ | | L1/L2 dual-layer cache | ✅ | ✅ moka (native) / `l1` feature | ✅ | ❌ | | Cache stampede prevention | ✅ | ❌ | ✅ Version tokens + background L1 refresh | ❌ | -| TTL management | ✅ | ✅ `TtlInspectable` trait | ✅ | ❌ | +| TTL management | ✅ Redis + SaaS | ✅ Redis + SaaS (`TtlInspectable`; Workers ❌) | ✅ SaaS only (`TTLBackend`) | ❌ | | Stale-while-revalidate (server stale-grace) | 🚧 LAB-381 | ❌ | ❌ | ❌ | -> **Lock id transport (CWE-532):** the unlock call carries the lock capability token in the `X-CacheKit-Lock-Id` request header, never the `?lock_id=` query string (which leaks via access/proxy logs and OTel `http.url` spans). SaaS dual-reads both during the rollout; SDKs migrate to header-only — Python (#131), TypeScript (#63), Rust (#24). See [spec/saas-api.md](spec/saas-api.md#delete-v1cachekeylock). +> **Lock id transport (CWE-532):** the unlock call carries the lock capability token in the `X-CacheKit-Lock-Id` request header, never the `?lock_id=` query string (which leaks via access/proxy logs and OTel `http.url` spans). **Migration complete in all three SDKs** (verified 2026-07-21, LAB-273): Python (#131, closed), Rust (#24, closed), TypeScript ships the header (ts#63 remains open only for an unrelated NAPI-rebuild item). SaaS dual-reads both during the rollout window. See [spec/saas-api.md](spec/saas-api.md#delete-v1cachekeylock). --- @@ -154,7 +191,7 @@ its spec: - Hybrid Python-Rust architecture: decorators and orchestration in Python, ByteStorage and encryption in Rust (via PyO3) - The `cachekit-core` Rust crate is the canonical implementation for compression, checksums, and encryption - 4 serializers: Standard (cross-language), Auto (Python-optimized), Orjson (JSON), Arrow (columnar) -- Backends: Redis, Memcached, File (local), CacheKit SaaS — Memcached and File auto-detected from environment +- Backends: Redis, Memcached, File (local), CacheKit SaaS — backend auto-detected from a single unambiguous env selector (see [Cache Backends](#cache-backends)) - Config via pydantic-settings; secrets via `SecretStr` From 7e8e73a12abf8e2d6b1749bc4885dfb86fcf7e8d Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Tue, 21 Jul 2026 04:27:20 +1000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20coderabbit=20review=20?= =?UTF-8?q?=E2=80=94=20matrix=20date/coverage/backend-selection=20accuracy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Correct future-dated audit stamps (2026-07-21 → 2026-07-20) in the Last-updated banner, Backend Abstraction audit note, and lock-id migration note. - Fix Redis optional-capability summary to match the capability table: Python has both locking + TTL inspection, Rust Redis has TTL inspection only, TS Redis neither. - Restore the documented Python backend-selection fallbacks (REDIS_URL/localhost, ConfigurationError on ambiguous selectors) in the SDK summary bullet. CodeRabbit-Resolved: sdk-feature-matrix.md:9:Remove future-dated completion claims CodeRabbit-Resolved: sdk-feature-matrix.md:115:Correct the Redis capability summary CodeRabbit-Resolved: sdk-feature-matrix.md:194:Preserve backend-selection fallbacks --- sdk-feature-matrix.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk-feature-matrix.md b/sdk-feature-matrix.md index df8b48c..0e10e27 100644 --- a/sdk-feature-matrix.md +++ b/sdk-feature-matrix.md @@ -6,7 +6,7 @@ **Feature parity and compliance status across all CacheKit SDK implementations.** -*Last updated: 2026-07-21 — LAB-273 backend-parity audit: DynamoDB corrected (ships nowhere), backend-abstraction section added, locking/TTL rows qualified per backend, lock-id header migration recorded complete* +*Last updated: 2026-07-20 — LAB-273 backend-parity audit: DynamoDB corrected (ships nowhere), backend-abstraction section added, locking/TTL rows qualified per backend, lock-id header migration recorded complete* @@ -88,7 +88,7 @@ ## Backend Abstraction -The contract a storage backend must satisfy per SDK (bytes in / bytes out; serialization and encryption live above the backend). Audited against code 2026-07-21 (LAB-273). +The contract a storage backend must satisfy per SDK (bytes in / bytes out; serialization and encryption live above the backend). Audited against code 2026-07-20 (LAB-273). ### Required interface @@ -112,7 +112,7 @@ The contract a storage backend must satisfy per SDK (bytes in / bytes out; seria > [!NOTE] > **Lock API shape divergence:** Python's `acquire_lock` is an async context manager yielding `bool` — the lock token stays internal and release is automatic. Rust and TypeScript return the raw `lock_id` capability token from `acquire_lock`/`acquireLock` and require an explicit `release_lock(key, lock_id)` — a direct mirror of the SaaS lock endpoint. All three pass the **bare cache key** (backends own any `:lock` namespace derivation). Porting a lockable backend across SDKs must bridge this shape difference. > -> **Coverage, not shape, is the parity gap:** all three SDKs use the same required-base + optional-capability pattern, but only Python implements the optional capabilities on Redis. Rust's Redis backend lacks locking; TypeScript's Redis backend lacks both locking and TTL inspection; Rust's Workers backend lacks both despite speaking the same SaaS API (gap tickets under LAB-102). +> **Coverage, not shape, is the parity gap:** all three SDKs use the same required-base + optional-capability pattern, but Redis optional-capability coverage varies by SDK: Python has the broadest coverage (both locking and TTL inspection), Rust's Redis backend implements TTL inspection only (no locking), and TypeScript's Redis backend implements neither. Rust's Workers backend also lacks both despite speaking the same SaaS API (gap tickets under LAB-102). --- @@ -128,7 +128,7 @@ The contract a storage backend must satisfy per SDK (bytes in / bytes out; seria | TTL management | ✅ Redis + SaaS | ✅ Redis + SaaS (`TtlInspectable`; Workers ❌) | ✅ SaaS only (`TTLBackend`) | ❌ | | Stale-while-revalidate (server stale-grace) | 🚧 LAB-381 | ❌ | ❌ | ❌ | -> **Lock id transport (CWE-532):** the unlock call carries the lock capability token in the `X-CacheKit-Lock-Id` request header, never the `?lock_id=` query string (which leaks via access/proxy logs and OTel `http.url` spans). **Migration complete in all three SDKs** (verified 2026-07-21, LAB-273): Python (#131, closed), Rust (#24, closed), TypeScript ships the header (ts#63 remains open only for an unrelated NAPI-rebuild item). SaaS dual-reads both during the rollout window. See [spec/saas-api.md](spec/saas-api.md#delete-v1cachekeylock). +> **Lock id transport (CWE-532):** the unlock call carries the lock capability token in the `X-CacheKit-Lock-Id` request header, never the `?lock_id=` query string (which leaks via access/proxy logs and OTel `http.url` spans). **Migration complete in all three SDKs** (verified 2026-07-20, LAB-273): Python (#131, closed), Rust (#24, closed), TypeScript ships the header (ts#63 remains open only for an unrelated NAPI-rebuild item). SaaS dual-reads both during the rollout window. See [spec/saas-api.md](spec/saas-api.md#delete-v1cachekeylock). --- @@ -191,7 +191,7 @@ its spec: - Hybrid Python-Rust architecture: decorators and orchestration in Python, ByteStorage and encryption in Rust (via PyO3) - The `cachekit-core` Rust crate is the canonical implementation for compression, checksums, and encryption - 4 serializers: Standard (cross-language), Auto (Python-optimized), Orjson (JSON), Arrow (columnar) -- Backends: Redis, Memcached, File (local), CacheKit SaaS — backend auto-detected from a single unambiguous env selector (see [Cache Backends](#cache-backends)) +- Backends: Redis, Memcached, File (local), CacheKit SaaS — backend auto-detected from the environment: a single unambiguous selector, with `REDIS_URL`/localhost fallback, and ambiguous (multiple) selectors raising `ConfigurationError` (see [Cache Backends](#cache-backends)) - Config via pydantic-settings; secrets via `SecretStr`