Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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<PyAny>` + 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
Expand All @@ -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`
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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 ?=
Expand Down
6 changes: 0 additions & 6 deletions benchmarks/_bench_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 1 addition & 2 deletions src/shared_store_stub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PyAny>,
_strategy: u8,
_max_size: usize,
_ttl: Option<f64>,
_max_key_size: usize,
Expand Down
44 changes: 2 additions & 42 deletions src/shm/region.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Self> {
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<ShmRegion> {
Expand All @@ -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.
Expand Down Expand Up @@ -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) }
}
Expand All @@ -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(())
}
}
12 changes: 7 additions & 5 deletions warp_cache/_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -13,6 +13,8 @@
SharedCacheInfo,
)

F = TypeVar("F", bound=Callable[..., Any])


class AsyncCachedFunction:
"""Async wrapper around a Rust CachedFunction or SharedCachedFunction.
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Loading