diff --git a/CLAUDE.md b/CLAUDE.md index fc1fde5..04c7bdd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,7 @@ uv run pytest tests/test_basic.py::test_cache_hit -v **Test across Python versions:** ```bash -make test-matrix -j # Parallel across 3.10-3.14 +make test-matrix -j # Parallel across 3.9-3.14 make test PYTHON=3.13 # Specific version ``` @@ -38,12 +38,12 @@ make test PYTHON=3.13 # Specific version - **`store.rs`** — In-process backend: `CachedFunction` uses sharded `hashbrown::HashMap` with passthrough hasher (avoids re-hashing Python's precomputed hash) + GIL-conditional locking (`GilCell` under GIL for zero-cost, `parking_lot::RwLock` under free-threaded Python). The `__call__` hot path uses `BorrowedArgs` to look up via borrowed pointer (no `CacheKey` allocation on hits), with `CacheKey` only materialized on cache miss for storage - **`serde.rs`** — Fast-path binary serialization for common primitives (None, bool, int, float, str, bytes, flat tuples); avoids pickle overhead for the shared backend - **`shared_store.rs`** — Cross-process backend: `SharedCachedFunction` holds `ShmCache` directly (no Mutex), with cached `max_key_size`/`max_value_size` fields and a pre-built `ahash::RandomState`. Serializes via serde.rs (with pickle fallback), stores in mmap'd shared memory -- **`entry.rs`** — `CacheEntry` { value, created_at, visited } +- **`entry.rs`** — `SieveEntry` { value, created_at, visited } - **`key.rs`** — `CacheKey` wraps `Py` + precomputed hash; uses raw `ffi::PyObject_RichCompareBool` for equality. Also provides `BorrowedArgs` (zero-alloc borrowed key for hit-path lookups via hashbrown's `Equivalent` trait) - **`shm/`** — Shared memory infrastructure: - `mod.rs` — `ShmCache`: create/open, get/set with serialized bytes. Uses interior mutability (`&self` methods): reads are lock-free (seqlock), writes acquire seqlock internally. `next_unique_id` is `AtomicU64` - `layout.rs` — Header + SlotHeader structs, memory offsets - - `region.rs` — `ShmRegion`: mmap file management (`$TMPDIR/warp_cache/{name}.cache`) + - `region.rs` — `ShmRegion`: mmap file management (`$TMPDIR/warp_cache/{name}.data` + `{name}.lock`) - `lock.rs` — `ShmSeqLock`: seqlock (optimistic reads + TTAS spinlock) in shared memory - `hashtable.rs` — Open-addressing with linear probing (power-of-2 capacity, bitmask) - `ordering.rs` — SIEVE eviction: intrusive linked list + `sieve_evict()` hand scan @@ -69,5 +69,5 @@ make test PYTHON=3.13 # Specific version ## Linting -- Python: ruff (rules: E, F, W, I, UP, B, SIM; line-length=100; target py310) +- Python: ruff (rules: E, F, W, I, UP, B, SIM; line-length=100; target py39) - Rust: `cargo clippy -- -D warnings` diff --git a/Makefile b/Makefile index 184da34..430395f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help fmt lint typecheck build build-debug test test-rust test-only bench bench-quick bench-all bench-report bench-sieve clean publish publish-test setup all +.PHONY: help fmt lint typecheck build build-debug test test-rust test-only test-matrix bench bench-quick bench-all bench-report bench-sieve clean publish publish-test setup all # Optional: specify Python version, e.g. make build PYTHON=3.14 PYTHON ?= diff --git a/benchmarks/_bench_runner.py b/benchmarks/_bench_runner.py index 1b6c10e..35ea633 100644 --- a/benchmarks/_bench_runner.py +++ b/benchmarks/_bench_runner.py @@ -233,12 +233,6 @@ def fmt(ops: float) -> str: return f"{ops:>7.0f} " -def ratio_str(a: float, b: float) -> str: - if b == 0: - return " inf" - return f"{a / b:.2f}x" - - def _time_loop(fn, keys: list[int]) -> float: """Time a cache function over a list of keys, return elapsed seconds.""" t0 = time.perf_counter() diff --git a/pyproject.toml b/pyproject.toml index 724c98e..f9b9169 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "warp_cache" dynamic = ["version"] -description = "Thread-safe Python caching decorator backed by a Rust extension" +description = "Thread-safe Python caching decorator powered by a Rust extension. Implements SIEVE eviction for scan-resistant, near-optimal hit rates with zero-cost locking under the GIL — entire cache lookup in a single Rust __call__, no Python wrapper overhead. 16–23M ops/s single-threaded, 25× faster than cachetools, with cross-process shared memory support." readme = "README.md" license = "MIT" requires-python = ">=3.9" diff --git a/src/shared_store_stub.rs b/src/shared_store_stub.rs index aa67a39..d0e3872 100644 --- a/src/shared_store_stub.rs +++ b/src/shared_store_stub.rs @@ -30,10 +30,9 @@ pub struct SharedCachedFunction; #[pymethods] impl SharedCachedFunction { #[new] - #[pyo3(signature = (_fn_obj, _strategy, _max_size, _ttl=None, _max_key_size=512, _max_value_size=4096, _shm_name=None))] + #[pyo3(signature = (_fn_obj, _max_size, _ttl=None, _max_key_size=512, _max_value_size=4096, _shm_name=None))] fn new( _fn_obj: Py, - _strategy: u8, _max_size: usize, _ttl: Option, _max_key_size: usize, diff --git a/src/shm/region.rs b/src/shm/region.rs index 2a49c7d..18a7d5a 100644 --- a/src/shm/region.rs +++ b/src/shm/region.rs @@ -23,12 +23,9 @@ fn shm_dir() -> PathBuf { /// The full shared-memory region, owning the mmap handle and providing /// raw accessors to the structures within. -#[allow(dead_code)] pub struct ShmRegion { pub mmap: MmapMut, - pub path: PathBuf, pub lock_mmap: MmapMut, - pub lock_path: PathBuf, } impl ShmRegion { @@ -130,22 +127,7 @@ impl ShmRegion { mmap.flush()?; lock_mmap.flush()?; - Ok(ShmRegion { - mmap, - path: data_path, - lock_mmap, - lock_path, - }) - } - - /// Open an existing shared memory region. - #[allow(dead_code)] - pub fn open(name: &str) -> io::Result { - let dir = shm_dir(); - let data_path = dir.join(format!("{name}.data")); - let lock_path = dir.join(format!("{name}.lock")); - - Self::open_paths(&data_path, &lock_path) + Ok(ShmRegion { mmap, lock_mmap }) } fn open_paths(data_path: &Path, lock_path: &Path) -> io::Result { @@ -171,12 +153,7 @@ impl ShmRegion { )); } - Ok(ShmRegion { - mmap, - path: data_path.to_path_buf(), - lock_mmap, - lock_path: lock_path.to_path_buf(), - }) + Ok(ShmRegion { mmap, lock_mmap }) } /// Create if doesn't exist, otherwise open. @@ -227,11 +204,6 @@ impl ShmRegion { unsafe { &*(self.mmap.as_ptr() as *const Header) } } - #[allow(dead_code)] - pub fn header_mut(&mut self) -> &mut Header { - unsafe { &mut *(self.mmap.as_mut_ptr() as *mut Header) } - } - pub fn lock(&self) -> ShmSeqLock { unsafe { ShmSeqLock::from_existing(self.lock_mmap.as_ptr() as *mut u8) } } @@ -240,16 +212,4 @@ impl ShmRegion { self.mmap.as_ptr() } - #[allow(dead_code)] - pub fn base_mut_ptr(&mut self) -> *mut u8 { - self.mmap.as_mut_ptr() - } - - /// Remove the backing files. - #[allow(dead_code)] - pub fn unlink(&self) -> io::Result<()> { - let _ = fs::remove_file(&self.path); - let _ = fs::remove_file(&self.lock_path); - Ok(()) - } } diff --git a/warp_cache/_decorator.py b/warp_cache/_decorator.py index 4e8db33..9702a55 100644 --- a/warp_cache/_decorator.py +++ b/warp_cache/_decorator.py @@ -3,7 +3,7 @@ import asyncio import warnings from collections.abc import Callable -from typing import Any +from typing import Any, TypeVar from warp_cache._strategies import Backend from warp_cache._warp_cache_rs import ( @@ -13,6 +13,8 @@ SharedCacheInfo, ) +F = TypeVar("F", bound=Callable[..., Any]) + class AsyncCachedFunction: """Async wrapper around a Rust CachedFunction or SharedCachedFunction. @@ -73,7 +75,7 @@ def cache( backend: str | int | Backend = Backend.MEMORY, max_key_size: int | None = None, max_value_size: int | None = None, -) -> Callable[[Callable[..., Any]], CachedFunction | SharedCachedFunction | AsyncCachedFunction]: +) -> Callable[[F], F]: """Caching decorator backed by a Rust store. Supports both sync and async functions. The async detection happens @@ -93,7 +95,7 @@ def cache( """ resolved_backend = _resolve_backend(backend) - def decorator(fn): + def decorator(fn: F) -> F: if resolved_backend == Backend.SHARED: inner = SharedCachedFunction( fn, @@ -116,8 +118,8 @@ def decorator(fn): inner = CachedFunction(fn, max_size, ttl=ttl) if asyncio.iscoroutinefunction(fn): - return AsyncCachedFunction(fn, inner) + return AsyncCachedFunction(fn, inner) # type: ignore[return-value] - return inner + return inner # type: ignore[return-value] return decorator