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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2025-12-23

### Changed
- **Major Architecture Overhaul**: The library is now fully async-native.
- `TTLCache`, `SWRCache`, and `BGCache` now support `async def` functions natively using `await`.
- Synchronous functions are still supported via intelligent inspection, maintaining backward compatibility.
- **Unified Scheduling**: `SWRCache` (in sync mode) and `BGCache` now use `APScheduler` (`SharedScheduler` and `SharedAsyncScheduler`) for all background tasks, replacing ad-hoc threading.
- **Testing**: Integration tests rewritten to use `pytest-asyncio` with `mode="auto"`.

### Added
- `AsyncTTLCache`, `AsyncStaleWhileRevalidateCache`, `AsyncBackgroundCache` classes (aliased to `TTLCache`, `SWRCache`, `BGCache`).
- `SharedAsyncScheduler` for managing async background jobs.
- `pytest-asyncio` configuration in `pyproject.toml`.

## [0.1.6] - 2025-12-15

### Changed
Expand Down
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,35 @@ uv pip install "advanced-caching[redis]" # Redis support
```python
from advanced_caching import TTLCache, SWRCache, BGCache

# Sync function
@TTLCache.cached("user:{}", ttl=300)
def get_user(user_id: int) -> dict:
return db.fetch(user_id)

# Async function (works natively)
@TTLCache.cached("user:{}", ttl=300)
async def get_user_async(user_id: int) -> dict:
return await db.fetch(user_id)

# Stale-While-Revalidate (Sync)
@SWRCache.cached("product:{}", ttl=60, stale_ttl=30)
def get_product(product_id: int) -> dict:
return api.fetch_product(product_id)

# Background refresh
# Stale-While-Revalidate (Async)
@SWRCache.cached("async:product:{}", ttl=60, stale_ttl=30)
async def get_product_async(product_id: int) -> dict:
return await api.fetch_product(product_id)

# Background refresh (Sync)
@BGCache.register_loader("inventory", interval_seconds=300)
def load_inventory() -> list[dict]:
return warehouse_api.get_all_items()

# Async works too
@TTLCache.cached("user:{}", ttl=300)
async def get_user_async(user_id: int) -> dict:
return await db.fetch(user_id)
# Background refresh (Async)
@BGCache.register_loader("inventory_async", interval_seconds=300)
async def load_inventory_async() -> list[dict]:
return await warehouse_api.get_all_items()
```

---
Expand Down
61 changes: 23 additions & 38 deletions docs/benchmarking-and-profiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

This repo includes a small, reproducible benchmark harness and a profiler-friendly workload script.

- Benchmark runner: `tests/benchmark.py`
- Benchmark suite: `tests/benchmark.py`
- Profiler workload: `tests/profile_decorators.py`
- Benchmark log (append-only JSON-lines): `benchmarks.log`
- Run comparison helper: `tests/compare_benchmarks.py`

## 1) Benchmarking (step-by-step)

Expand All @@ -17,14 +16,14 @@ This repo uses `uv`. From the repo root:
uv sync
```

### Step 1 — Run the default benchmark
### Step 1 — Run the benchmark suite

```bash
uv run python tests/benchmark.py
```

What you get:
- A printed table for **cold** (always miss), **hot** (always hit), and **mixed** (hits + misses).
- Printed tables for **hot cache hits** (comparing TTLCache, SWRCache, BGCache).
- A new JSON entry appended to `benchmarks.log` with the config + median/mean/stdev per strategy.

### Step 2 — Tune benchmark parameters (optional)
Expand All @@ -35,60 +34,42 @@ What you get:
- `BENCH_WORK_MS` (default `5.0`) — simulated I/O latency (sleep)
- `BENCH_WARMUP` (default `10`)
- `BENCH_RUNS` (default `300`)
- `BENCH_MIXED_KEY_SPACE` (default `100`)
- `BENCH_MIXED_RUNS` (default `500`)

Examples:

```bash
BENCH_RUNS=1000 BENCH_MIXED_RUNS=2000 uv run python tests/benchmark.py
```

```bash
# Focus on decorator overhead (no artificial sleep)
BENCH_WORK_MS=0 BENCH_RUNS=200000 BENCH_MIXED_RUNS=300000 uv run python tests/benchmark.py
BENCH_RUNS=1000 uv run python tests/benchmark.py
```

### Step 3 — Compare two runs

There are two ways to select runs:

- Relative: `last` / `last-N`
- Explicit: integer indices (0-based; negatives allowed)

List run indices quickly:
The benchmark appends JSON lines to `benchmarks.log`. A quick helper to list runs:

```bash
uv run python - <<'PY'
import json
from pathlib import Path
runs=[]
if not Path('benchmarks.log').exists():
print("No benchmarks.log found")
exit(0)
for line in Path('benchmarks.log').read_text(encoding='utf-8', errors='replace').splitlines():
line=line.strip()
if not line.startswith('{'):
continue
try:
obj=json.loads(line)
except Exception:
continue
if isinstance(obj,dict) and 'results' in obj:
runs.append(obj)
line=line.strip()
if not line.startswith('{'):
continue
try:
obj=json.loads(line)
except Exception:
continue
if isinstance(obj,dict) and 'sections' in obj:
runs.append(obj)
print('count',len(runs))
for i,r in enumerate(runs):
print(i,r.get('ts'))
print(i,r.get('ts'))
PY
```

Compare (example: index 2 vs index 11):

```bash
uv run python tests/compare_benchmarks.py --a 2 --b 11
```

What to look at:
- **Hot TTL/SWR** medians: these are the pure “cache-hit overhead” numbers.
- **Mixed** medians: reflect a real-ish distribution; watch for regressions here.
- Ignore small (<5–10%) deltas unless they repeat across multiple clean runs.
To compare two indices (e.g., 2 vs 11), load the JSON objects in a notebook or script and diff the `sections` (hot medians for TTL/SWR/BG are the most sensitive to overhead changes).

### Step 4 — Make results stable (recommended practice)

Expand Down Expand Up @@ -163,6 +144,10 @@ PROFILE_N=5000000 \
- `SWRCache` hot: overhead of key generation + `get_entry()` + freshness checks.
- `BGCache` hot: overhead of key lookup + `get()` + return.

- **Async results (important)**
- Async medians include the cost of creating/awaiting a coroutine and event-loop scheduling.
- For AsyncBG/AsyncSWR, compare against the `async_baseline` row (plain `await` with no cache) to estimate *cache-specific* overhead.

- **Mixed path**
- A high mean + low median typically indicates occasional slow misses/refreshes.

Expand Down
Loading
Loading