# s3fifo

[![Release](https://img.shields.io/github/v/release/psiace/s3fifo)](https://img.shields.io/github/v/release/psiace/s3fifo)
[![Build status](https://img.shields.io/github/actions/workflow/status/psiace/s3fifo/main.yml?branch=main)](https://github.com/psiace/s3fifo/actions/workflows/main.yml?query=branch%3Amain)
[![codecov](https://codecov.io/gh/psiace/s3fifo/branch/main/graph/badge.svg)](https://codecov.io/gh/psiace/s3fifo)
[![License](https://img.shields.io/github/license/psiace/s3fifo)](https://img.shields.io/github/license/psiace/s3fifo)

Python implementation of the S3-FIFO cache algorithm.

It provides:

- `S3FIFOCache`: low-level cache container.
- `s3fifo_cache`: sync decorator with an API similar to `functools.lru_cache`.
- `as3fifo_cache`: async decorator inspired by `async-lru`.

## Installation

```bash
pip install s3fifo
```

## Quick Start

### Sync decorator

```python
from s3fifo import s3fifo_cache


@s3fifo_cache(maxsize=128)
def load_user(user_id: int) -> dict:
    return {"id": user_id}


load_user(1)
load_user(1)
print(load_user.cache_info())
# CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)
```

### Async decorator

```python
import asyncio

from s3fifo import as3fifo_cache


@as3fifo_cache(maxsize=128)
async def fetch_user(user_id: int) -> dict:
    await asyncio.sleep(0.01)
    return {"id": user_id}


async def main() -> None:
    await asyncio.gather(fetch_user(1), fetch_user(1), fetch_user(1))
    print(fetch_user.cache_info())
    # CacheInfo(hits=2, misses=1, maxsize=128, currsize=1)

    await fetch_user.cache_close()


asyncio.run(main())
```

`as3fifo_cache` deduplicates concurrent calls for the same key and shares one in-flight task.

## Configuration

The S3-FIFO policy parameters are configurable across all entry points and default to the paper/reference values:

- `small_size_ratio=0.10`
- `ghost_size_ratio=0.90`
- `move_to_main_threshold=2`

Example:

```python
from s3fifo import s3fifo_cache


@s3fifo_cache(
    maxsize=256,
    small_size_ratio=0.20,
    ghost_size_ratio=0.50,
    move_to_main_threshold=3,
)
def f(x: int) -> int:
    return x
```

You can inspect active settings via `cache_parameters()`.

## API Summary

### `S3FIFOCache`

```python
S3FIFOCache(
    maxsize: int,
    *,
    small_size_ratio: float = 0.10,
    ghost_size_ratio: float = 0.90,
    move_to_main_threshold: int = 2,
)
```

Methods:

- `get(key, default=None)`
- `put(key, value)`
- `remove(key, include_ghost=True)`
- `clear()`
- `len(cache)`
- `key in cache`

### `s3fifo_cache`

Usage:

- `@s3fifo_cache`
- `@s3fifo_cache(maxsize=128, typed=False, ...)`

Methods on decorated function:

- `cache_info()`
- `cache_clear()`
- `cache_parameters()`

Notes:

- `maxsize=0` disables storage and still counts misses.
- `typed=True` includes argument types in cache keys.

### `as3fifo_cache`

Usage:

- `@as3fifo_cache`
- `@as3fifo_cache(maxsize=128, typed=False, ...)`

Methods on decorated function:

- `cache_info()`
- `cache_clear()`
- `cache_invalidate(*args, **kwargs)`
- `cache_parameters()`
- `await cache_close(wait=False)`

## Limitations

- `as3fifo_cache` has event-loop affinity: a single cache instance must be used on one event loop.
- TTL and jitter policies are not implemented.

## Differences from Origin

- **Capacity is measured differently.**
  The original implementation is byte-size based, while this package is entry-count based.
  In practice, `maxsize` means “maximum number of cached keys”, not memory bytes.

- **The programming model is Python-first.**
  The original design is request/object-size oriented; here you work with a plain Python cache object
  (`S3FIFOCache`) and decorators (`s3fifo_cache`, `as3fifo_cache`).

- **Overwriting an existing key is treated as an update.**
  Calling `put` with an existing key updates the value in place and does not trigger an extra eviction cycle.

## Development

```bash
make install
make check
make test
```

## License

[Apache-2.0](./LICENSE)
