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
7 changes: 2 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ Note that 0.3.3 was never released; 0.3.4 follows 0.3.2.
after it raises `SessionNotFoundError`, as after an ordinary `session_ttl`
expiry, instead of `SessionExpiredError`.
([#164](https://github.com/allen0099/FastAPI-CacheX/issues/164))

- **`MemoryBackend` restarts its cleanup task on a new event loop.** The task
stayed tied to the loop of the first cache call. If that loop was closed
without cancelling it, a backend reused on another loop never cleaned up
Expand All @@ -160,14 +159,13 @@ Note that 0.3.3 was never released; 0.3.4 follows 0.3.2.
`delete_if_equals()` raised `TypeError`. A failed server is now taken out of
rotation at once and tried again after one second, instead of after 60.
([#197](https://github.com/allen0099/FastAPI-CacheX/issues/197))

- **`SessionConfig` warns when `cookie_same_site="none"` is set without
`cookie_https_only=True`.** Browsers reject a `SameSite=None` cookie that is
not `Secure`, so the session cookie was silently never stored. The
combination is still accepted.
([#167](https://github.com/allen0099/FastAPI-CacheX/issues/167))

- **Memcached `get_and_delete()` uses CAS deletion to avoid deleting concurrent writes.**
- **Memcached `get_and_delete()` uses CAS deletion to avoid deleting concurrent
writes.**
The get-then-delete sequence allowed a concurrent writer to update the key
between the two calls, causing `get_and_delete()` to delete the new value
while returning the old one. It now issues `gets` and a `cas` write with
Expand All @@ -176,7 +174,6 @@ Note that 0.3.3 was never released; 0.3.4 follows 0.3.2.
`CacheXError` if retries run out.
([#175](https://github.com/allen0099/FastAPI-CacheX/issues/175))


## [0.3.7] - 2026-09-25

### Added
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Backend keys are namespaced automatically (default prefix: `fastapi_cachex:`).

Four non-abstract atomic primitives live on the base class with non-atomic fallbacks, and every built-in backend overrides them (see `docs/BACKENDS.md` "Atomic backend primitives"):
- `increment(key, delta=1, ttl=None) -> int`: fixed-window counter; `ttl` applies only when the counter is created. Redis runs a registered Lua script, Memcached uses `ADD` + `INCR`/`DECR`, memory works under its lock. A counter reads back through `get()` as a `CacheEntry` with `COUNTER_FINGERPRINT` (`types.py`).
- `get_and_delete(key) -> CacheEntry | None`: one-shot retrieval (Redis `GETDEL`, Memcached `gets` + `cas(..., exptime=-1)` with retry). `StateManager.consume_state`, `delete_state`, `CacheManager.delete` and `invalidate()` use it. `delete()` keeps returning `None` for 0.3.x compatibility.
- `get_and_delete(key) -> CacheEntry | None`: one-shot retrieval (Redis `GETDEL`, Memcached `gets` + `cas(..., exptime=-1)`, retried up to 16 times, then `CacheXError`). `StateManager.consume_state`, `delete_state`, `CacheManager.delete` and `invalidate()` use it. `delete()` keeps returning `None` for 0.3.x compatibility.
- `set_if_absent(key, value, ttl=None) -> bool`: claim-if-free for locks/slots. Redis `SET NX EX`, Memcached `ADD`, memory under its lock.
- `delete_if_equals(key, expected) -> bool`: release only while the key still holds `expected` (compared as decoded `CacheEntry`). Redis compares in Python then deletes via a Lua script that re-checks the raw bytes; Memcached uses `GETS` + `CAS` with exptime `-1` (immediate expiry), since classic `DELETE` has no CAS.

Expand Down
4 changes: 3 additions & 1 deletion docs/BACKENDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ if await backend.set_if_absent(f"stream:{user_id}", owner, ttl=300):
`set(key, counter_entry(n))` can be incremented on every backend.
- `get_and_delete(key) -> CacheEntry | None` — Memory pops under its lock, Redis
uses `GETDEL` (server 6.2+) and Memcached uses `GETS` + a `CAS` write with
`exptime=-1` (retrying if another writer replaced the value in between).
`exptime=-1` (retrying if another writer replaced the value in between). If
writers keep replacing it for 16 attempts in a row, Memcached raises
`CacheXError` rather than report the key as missing.
`StateManager.consume_state`, `StateManager.delete_state`,
`CacheManager.delete` and `invalidate()` are built on it.
- `set_if_absent(key, value, ttl=None) -> bool` — stores `value` only when
Expand Down
3 changes: 2 additions & 1 deletion docs/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ CacheXError
- **The one-time guarantee comes from the backend's atomic operation.** `get_and_delete()` is
`GETDEL` on Redis (requires Redis server 6.2 or newer), `gets` followed by
`cas(..., exptime=-1)` on Memcached (retrying if another writer replaced the value in between),
and a `pop` under the lock on the memory backend.
and a `pop` under the lock on the memory backend. If writers keep replacing the value for
16 attempts in a row, Memcached raises `CacheXError` rather than report the state as missing.
A custom backend that implements only the abstract methods falls back to
`BaseCacheBackend`'s non-atomic version, so a concurrent replay could succeed on both
sides. Override `get_and_delete()` in that case.
Expand Down
2 changes: 1 addition & 1 deletion i18n/zh-TW/docs/BACKENDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ if await backend.set_if_absent(f"stream:{user_id}", owner, ttl=300):
```

- `increment(key, delta=1, ttl=None) -> int`:記憶體後端在鎖內執行讀取—修改—寫入,Redis 執行 Lua 腳本(`EXISTS` + `INCRBY` + `EXPIRE`),Memcached 則使用 `ADD` + `INCR`/`DECR`(Memcached 的計數器最低停在 0)。計數器可透過 `get()` 讀到,形式為 fingerprint 為 `COUNTER_FINGERPRINT`、內容為十進位數值的 `CacheEntry`,因此 `delete`/`clear*` 與監控路由都會把它當成一般項目處理。對存放快取回應的鍵執行 increment 會拋出 `CacheXError`。
- `get_and_delete(key) -> CacheEntry | None`:記憶體後端在鎖內 pop,Redis 使用 `GETDEL`(伺服器 6.2 以上),Memcached 使用 `GETS` + `exptime=-1` 的 `CAS` 寫入(若中間有其他寫入者替換了值則會重試)。`StateManager.consume_state`、`StateManager.delete_state`、`CacheManager.delete` 與 `invalidate()` 都建立在它之上。
- `get_and_delete(key) -> CacheEntry | None`:記憶體後端在鎖內 pop,Redis 使用 `GETDEL`(伺服器 6.2 以上),Memcached 使用 `GETS` + `exptime=-1` 的 `CAS` 寫入(若中間有其他寫入者替換了值則會重試;連續 16 次都被替換時會拋出 `CacheXError`,而不是當成鍵不存在)。`StateManager.consume_state`、`StateManager.delete_state`、`CacheManager.delete` 與 `invalidate()` 都建立在它之上。
- `set_if_absent(key, value, ttl=None) -> bool`:只在 `key` 不存在時儲存 `value`(已過期的鍵視為不存在),並回報是否有寫入。記憶體後端在鎖內檢查,Redis 使用 `SET NX EX`,Memcached 使用 `ADD`。
- `delete_if_equals(key, expected) -> bool`:只在 `key` 仍存放 `expected` 時才移除它,因此項目已過期的持有者無法釋放已被他人取得的鎖。請在你儲存的項目中放入唯一的權杖,並以同一個項目釋放。記憶體後端在鎖內比較,Redis 透過 Lua 腳本刪除,並在腳本中重新檢查先前比較過的值,Memcached 則使用 `GETS` + 一個讓項目立即過期的 `CAS` 寫入(傳統協定的 `DELETE` 不接受 CAS 權杖)。

Expand Down
2 changes: 1 addition & 1 deletion i18n/zh-TW/docs/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,6 @@ CacheXError
## 注意事項 {#notes}

- **後端必須在多個行程之間共用。** 多 worker 部署請使用 Redis 或 Memcached。使用 `MemoryBackend` 時,state 只存在於建立它的行程中,因此落到其他 worker 的授權回呼會失敗。
- **一次性保證來自後端的原子操作。** `get_and_delete()` 在 Redis 上是 `GETDEL`(需要 Redis 伺服器 6.2 或更新版本);在 Memcached 上是 `gets` 後接 `cas(..., exptime=-1)`(若中間有其他寫入者替換了值則會重試);在記憶體後端上則是在鎖內 `pop`。只實作抽象方法的自訂後端會退回使用 `BaseCacheBackend` 的非原子性版本,因此並行的重送可能兩邊都成功。這種情況請覆寫 `get_and_delete()`。
- **一次性保證來自後端的原子操作。** `get_and_delete()` 在 Redis 上是 `GETDEL`(需要 Redis 伺服器 6.2 或更新版本);在 Memcached 上是 `gets` 後接 `cas(..., exptime=-1)`(若中間有其他寫入者替換了值則會重試;連續 16 次都被替換時會拋出 `CacheXError`,而不是當成 state 不存在);在記憶體後端上則是在鎖內 `pop`。只實作抽象方法的自訂後端會退回使用 `BaseCacheBackend` 的非原子性版本,因此並行的重送可能兩邊都成功。這種情況請覆寫 `get_and_delete()`。
- 不要在 state 中存放敏感資料。`metadata` 會以明文 JSON 存放在快取後端中。
- **日誌中絕不會出現 state 本身。** 來自 `fastapi_cachex.state.manager` 的日誌以 `state_ref` 識別 state,也就是其 SHA-256 的前 12 個十六進位字元;你可以從已知的 state 計算出它來比對。未知或已過期的 state 以 INFO 等級記錄,因為那是常見的用戶端輸入;格式錯誤的儲存資料則以 WARNING 等級記錄一次。
Loading