Metadata-Version: 2.5
Name: flightlock
Version: 0.1.1
Summary: Single-flight cache stampede protection for Python, with pluggable backends and TTL jitter.
Project-URL: Homepage, https://github.com/raoamogh/flightlock
Project-URL: Repository, https://github.com/raoamogh/flightlock
Project-URL: Issues, https://github.com/raoamogh/flightlock/issues
Project-URL: Changelog, https://github.com/raoamogh/flightlock/blob/main/CHANGELOG.md
Author-email: Amogha Rao <amoghagrao@gmail.com>
License: MIT
License-File: LICENSE
Keywords: cache,caching,distributed-systems,redis,singleflight,stampede
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: prometheus-client>=0.20.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
Requires-Dist: pytest>=7.4.0; extra == 'dev'
Requires-Dist: redis>=5.0.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Provides-Extra: metrics
Requires-Dist: prometheus-client>=0.20.0; extra == 'metrics'
Provides-Extra: redis
Requires-Dist: redis>=5.0.0; extra == 'redis'
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/raoamogh/flightlock/main/assets/banner.svg" alt="flightlock banner" width="100%">
</p>

<p align="center">
  <a href="https://github.com/raoamogh/flightlock/actions/workflows/ci.yml"><img src="https://github.com/raoamogh/flightlock/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
  <a href="https://pypi.org/project/flightlock/"><img src="https://img.shields.io/pypi/v/flightlock.svg" alt="PyPI version"></a>
  <img src="https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue.svg" alt="Python versions">
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="MIT License"></a>
  <img src="https://img.shields.io/badge/code%20style-ruff-black" alt="Code style: ruff">
  <img src="https://img.shields.io/badge/coverage-96%25-brightgreen" alt="Test coverage">
</p>

<p align="center">
  <img src="https://img.shields.io/github/stars/raoamogh/flightlock?style=social" alt="GitHub stars">
  <img src="https://img.shields.io/github/last-commit/raoamogh/flightlock" alt="Last commit">
  <img src="https://img.shields.io/github/repo-size/raoamogh/flightlock" alt="Repo size">
  <a href="https://github.com/raoamogh/flightlock/issues"><img src="https://img.shields.io/github/issues/raoamogh/flightlock" alt="Open issues"></a>
  <a href="CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg" alt="Contributor Covenant"></a>
</p>

<h1 align="center">flightlock</h1>

<p align="center"><b>Single-flight cache stampede protection for Python.</b></p>

When a cached value expires under concurrent load, most caching libraries let every waiting request independently recompute it — hammering your database or API with N identical calls at once (a "cache stampede" or "thundering herd"). **flightlock ensures only one caller recomputes the value while every other caller waits for and reuses that result.**

Named after the "singleflight" pattern used in production caching systems (e.g. Go's `golang.org/x/sync/singleflight`, `groupcache`).

## Table of contents

- [Why flightlock](#why-flightlock)
- [Quickstart](#quickstart)
- [Benchmark](#benchmark)
- [How it works](#how-it-works)
- [Features](#features)
- [Installation](#installation)
- [Comparison](#comparison-with-alternatives)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [Security](#security)
- [Support](#support)
- [License](#license)

## Why flightlock

Picture a cache key with a 60-second TTL backing an expensive DB query. The moment it expires, if 500 concurrent requests arrive for that key before it's repopulated, a naive cache sends all 500 straight to your database at once. flightlock coordinates those 500 callers so exactly **one** hits the database, and the other 499 wait milliseconds for that one result instead.

## Quickstart

```python
from flightlock import cached

@cached(ttl=60)
def get_user(user_id: int) -> dict:
    print("hitting the database...")
    return expensive_db_call(user_id)

get_user(1)  # prints "hitting the database...", takes 300ms
get_user(1)  # instant — cache hit, no print
```

With Redis and metrics enabled:

```python
from flightlock import cached
from flightlock.backends.redis import RedisBackend

backend = RedisBackend(host="localhost", port=6379)

@cached(ttl=60, backend=backend, metrics=True)
def get_user(user_id: int) -> dict:
    return expensive_db_call(user_id)
```

## Benchmark

Run it yourself: `python benchmarks/stampede_benchmark.py`

Simulating 200 concurrent requests for the same cache key, with a 0.3s simulated origin latency (e.g. a slow DB query):
| | Without Flightlock (Naive) | With Flightlock |
| --- | --- | --- |
| Origin Calls | 200 | 1 |
| Wall Time | 0.309s | 0.312s |

**Result:** 9.5% fewer origin calls with flightlock
(200 -> 1 calls for 200 concurrent requests)

Every request still returns the correct value — flightlock just ensures only one caller does the work while the other 199 wait for and share the result.

## How it works

1. A per-key lock registry tracks in-flight computations.
2. When a caller arrives for a missing/expired key, it either becomes the **leader** (first arrival — runs the function) or a **follower** (waits for the leader's result).
3. The leader runs the function *outside* the lock, so unrelated keys never block each other.
4. When the leader finishes, every waiting follower is woken simultaneously and receives the same result (or the same exception, if it failed).

See [`src/flightlock/core.py`](src/flightlock/core.py) for the implementation.

## Features

- ✅ Single-flight stampede protection (in-process)
- ✅ Pluggable backends: in-memory, Redis
- ✅ TTL jitter to prevent synchronized mass-expiry across many keys
- ✅ Prometheus-compatible metrics (hits, misses, errors, latency)
- ✅ Custom cache key functions
- 🔜 Async support
- 🔜 Cross-process distributed locking (currently: stampede protection is per-process; Redis backend shares *storage* across processes, not the *lock*)

## Installation

```bash
pip install flightlock                  # core, zero dependencies
pip install flightlock[redis]           # + Redis backend
pip install flightlock[metrics]         # + Prometheus metrics
pip install flightlock[redis,metrics]   # everything
```

## Comparison with alternatives

| | flightlock | `functools.lru_cache` | `cachetools` |
|---|---|---|---|
| Stampede protection | ✅ | ❌ | ❌ |
| TTL support | ✅ | ❌ | ✅ |
| Redis backend | ✅ | ❌ | ❌ (needs extra glue) |
| TTL jitter | ✅ | ❌ | ❌ |
| Metrics hook | ✅ | ❌ | ❌ |

flightlock isn't trying to replace general-purpose caching libraries — it solves one specific, real production problem (concurrent cache-miss stampedes) thoroughly.

## Roadmap

- [x] Project scaffolding, CI, packaging config
- [x] Core single-flight lock implementation
- [x] In-memory backend
- [x] The `@cached()` decorator
- [x] TTL jitter
- [x] Redis backend
- [x] Metrics hook
- [x] Benchmark proving stampede protection under load
- [ ] Publish to PyPI
- [ ] Async support

## Contributing

Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.

## Security

See [SECURITY.md](SECURITY.md) for how to report a vulnerability.

## Support

See [SUPPORT.md](SUPPORT.md) for how to get help.

## AI-assisted development

This project was built with AI pair-programming assistance for scaffolding, boilerplate, and code review. See [AI_POLICY.md](AI_POLICY.md) for details on how AI was used and what was independently written, tested, and verified.

## License

MIT — see [LICENSE](LICENSE).
