From dade1717c88c8ae58fd5d06b0c70b13515e3c148 Mon Sep 17 00:00:00 2001 From: allen0099 Date: Wed, 14 Oct 2026 08:37:08 +0000 Subject: [PATCH] feat(routes): entry-named counts in /cached-hits; deprecate the *_hits fields (#336) /cached-hits reported cached_hits, total_hits, valid_hits and expired_hits, which count cache entries, not hits, so the docs had to explain the names. The response now carries entries, total_entries, valid_entries and expired_entries with the same values. The old fields stay until 0.6.0, filled as before, and are marked deprecated in the OpenAPI schema (pydantic Field(deprecated=...)), so generated clients flag them. Closes #336 --- changelog.d/336.added.md | 3 ++ changelog.d/336.deprecated.md | 4 ++ docs/HTTP_CACHING.md | 8 +++- fastapi_cachex/routes.py | 54 ++++++++++++++++++++------ i18n/zh-TW/docs/HTTP_CACHING.md | 2 +- tests/test_routes.py | 67 +++++++++++++++++++++++++++++++++ 6 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 changelog.d/336.added.md create mode 100644 changelog.d/336.deprecated.md diff --git a/changelog.d/336.added.md b/changelog.d/336.added.md new file mode 100644 index 0000000..ccb07ea --- /dev/null +++ b/changelog.d/336.added.md @@ -0,0 +1,3 @@ +**`/cached-hits` reports `entries`, `total_entries`, `valid_entries` and +`expired_entries`.** They list and count the cached route entries under names +that say so; the route never counted hits. diff --git a/changelog.d/336.deprecated.md b/changelog.d/336.deprecated.md new file mode 100644 index 0000000..b58b9f1 --- /dev/null +++ b/changelog.d/336.deprecated.md @@ -0,0 +1,4 @@ +**The `cached_hits`, `total_hits`, `valid_hits` and `expired_hits` fields of +`/cached-hits` are deprecated and will be removed in 0.6.0.** They keep their +values until then and are marked `deprecated` in the OpenAPI schema; read +`entries`, `total_entries`, `valid_entries` and `expired_entries` instead. diff --git a/docs/HTTP_CACHING.md b/docs/HTTP_CACHING.md index 9cc66e2..3e8a243 100644 --- a/docs/HTTP_CACHING.md +++ b/docs/HTTP_CACHING.md @@ -944,8 +944,12 @@ add_routes( - `GET {prefix}/cached-hits` — every cached entry split into method, host, path and query (`sha256:` for a query over 200 bytes), with its ETag and - expiry, plus counts of valid and expired entries - and the distinct cached paths. It does not count hits. + expiry, in `entries`, counted in `total_entries`, `valid_entries` and + `expired_entries`, plus the distinct cached paths. Despite the route's name, + it does not count hits. The older `cached_hits`, `total_hits`, `valid_hits` + and `expired_hits` hold the same values; they are deprecated (marked so in + the OpenAPI schema) and removed in 0.6.0, so read the `*entries` fields + ([#336](https://github.com/allen0099/FastAPI-CacheX/issues/336)). - `GET {prefix}/cached-records` — every cached record with its size, expiry, `media_type` (the stored response's media type, `null` if it had none) and, with `include_content_preview=True`, a preview of the first 100 bytes of the diff --git a/fastapi_cachex/routes.py b/fastapi_cachex/routes.py index 57ed650..0b8905d 100644 --- a/fastapi_cachex/routes.py +++ b/fastapi_cachex/routes.py @@ -5,8 +5,11 @@ from dataclasses import dataclass from dataclasses import field from typing import TYPE_CHECKING +from typing import Annotated from typing import Any +from pydantic import Field + from .cache_key import CacheKey from .exceptions import BackendNotFoundError from .proxy import BackendProxy @@ -42,19 +45,42 @@ class CacheHitSummary: cached_paths: list[str] +# Why the ``*_hits`` fields are deprecated, shown in the OpenAPI schema. +_HITS_DEPRECATED = ( + "Counts cache entries, not hits; use {0} instead. Deprecated in 0.5.1, " + "removed in 0.6.0 (#336)." +) + + @dataclass class CacheHitsResponse: """Response for the ``/cached-hits`` endpoint. - The ``*_hits`` fields count cached entries, not requests served from them. + ``entries``, ``total_entries``, ``valid_entries`` and ``expired_entries`` + list and count the cached route entries. The ``cached_hits`` and + ``*_hits`` fields hold the same values under names that suggested + requests served from the cache; they are deprecated (marked so in the + OpenAPI schema) and removed in 0.6.0 (#336). """ - cached_hits: list[CacheHitRecord] - total_hits: int - valid_hits: int - expired_hits: int + entries: list[CacheHitRecord] + total_entries: int + valid_entries: int + expired_entries: int unique_routes: int summary: CacheHitSummary + cached_hits: Annotated[ + list[CacheHitRecord], Field(deprecated=_HITS_DEPRECATED.format("entries")) + ] + total_hits: Annotated[ + int, Field(deprecated=_HITS_DEPRECATED.format("total_entries")) + ] + valid_hits: Annotated[ + int, Field(deprecated=_HITS_DEPRECATED.format("valid_entries")) + ] + expired_hits: Annotated[ + int, Field(deprecated=_HITS_DEPRECATED.format("expired_entries")) + ] @dataclass @@ -174,17 +200,23 @@ def _cached_hits(entries: list[_Entry]) -> CacheHitsResponse: valid_hits = [h for h in cached_hits if not h.is_expired] routes_hit = {h.path for h in valid_hits} + total = len(cached_hits) + valid = len(valid_hits) return CacheHitsResponse( - cached_hits=cached_hits, - total_hits=len(cached_hits), - valid_hits=len(valid_hits), - expired_hits=len(cached_hits) - len(valid_hits), + entries=cached_hits, + total_entries=total, + valid_entries=valid, + expired_entries=total - valid, unique_routes=len(routes_hit), summary=CacheHitSummary( - total_cached_entries=len(cached_hits), - active_entries=len(valid_hits), + total_cached_entries=total, + active_entries=valid, cached_paths=sorted(routes_hit), ), + cached_hits=cached_hits, + total_hits=total, + valid_hits=valid, + expired_hits=total - valid, ) diff --git a/i18n/zh-TW/docs/HTTP_CACHING.md b/i18n/zh-TW/docs/HTTP_CACHING.md index 2459ab5..b398fb0 100644 --- a/i18n/zh-TW/docs/HTTP_CACHING.md +++ b/i18n/zh-TW/docs/HTTP_CACHING.md @@ -544,7 +544,7 @@ add_routes( ) ``` -- `GET {prefix}/cached-hits`:列出每筆快取項目,拆分為方法、主機、路徑與查詢(超過 200 位元組的查詢為 `sha256:<十六進位>`),附上 ETag 與到期時間,另外統計有效與已過期的項目數,以及不重複的快取路徑。它不會計算命中次數。 +- `GET {prefix}/cached-hits`:列出每筆快取項目,拆分為方法、主機、路徑與查詢(超過 200 位元組的查詢為 `sha256:<十六進位>`),附上 ETag 與到期時間,放在 `entries` 中,並以 `total_entries`、`valid_entries` 與 `expired_entries` 統計項目數,另外列出不重複的快取路徑。雖然路由名稱有 hits,它並不會計算命中次數。舊的 `cached_hits`、`total_hits`、`valid_hits` 與 `expired_hits` 內容相同,但已棄用(OpenAPI schema 中會標示),並將在 0.6.0 移除,請改讀 `*entries` 欄位([#336](https://github.com/allen0099/FastAPI-CacheX/issues/336))。 - `GET {prefix}/cached-records`:列出每筆快取紀錄的大小、到期時間、`media_type`(儲存的回應的媒體類型,沒有時為 `null`),以及在 `include_content_preview=True` 時快取內容前 100 個位元組的預覽。預設 `content_preview` 為 `null`,不會有任何回應本文離開伺服器;鍵、大小與到期時間仍會回報。`content_type` 一律是 `"bytes"`,只為相容而保留;請改讀 `media_type`。 兩個路由都只列出路由項目(格式為 `http:v2|method|host|path|query` 的鍵);`CacheManager` 與鎖的鍵都會略過,未使用 `build_cache_key()` 的 `key_builder` 產生的鍵也一樣。 diff --git a/tests/test_routes.py b/tests/test_routes.py index 0fc4b82..9d6a96c 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -673,3 +673,70 @@ def test_routes_answer_empty_when_no_backend_is_configured(self, app, client): assert hits["summary"]["cached_paths"] == [] assert records["total_records"] == 0 assert records["summary"]["estimated_cache_size_kb"] == 0.0 + + +class TestEntryNamedFields: + """``/cached-hits`` counts entries under entry names (#336). + + The ``cached_hits`` and ``*_hits`` fields stay, with the same values, and + are marked deprecated in the OpenAPI schema until 0.6.0 removes them. + """ + + def test_new_fields_list_and_count_the_entries( + self, app: FastAPI, client: TestClient, setup_cache: MemoryBackend + ) -> None: + add_routes(app, dependencies=[]) + + @app.get("/a") + @cache(ttl=60) + async def a() -> dict[str, str]: + return {"a": "1"} + + @app.get("/b") + @cache(ttl=60) + async def b() -> dict[str, str]: + return {"b": "1"} + + client.get("/a") + client.get("/b") + data = client.get("/cached-hits").json() + + assert data["total_entries"] == 2 + assert data["valid_entries"] == 2 + assert data["expired_entries"] == 0 + assert {e["path"] for e in data["entries"]} == {"/a", "/b"} + # The deprecated names carry the same values. + assert data["cached_hits"] == data["entries"] + assert data["total_hits"] == data["total_entries"] + assert data["valid_hits"] == data["valid_entries"] + assert data["expired_hits"] == data["expired_entries"] + + def test_an_expired_entry_is_counted_as_expired( + self, app: FastAPI, client: TestClient, setup_cache: MemoryBackend + ) -> None: + add_routes(app, dependencies=[]) + _report_expired( + setup_cache, + "http:v2|GET|testserver|/old|", + CacheEntry(fingerprint='W/"old"', content=b"old"), + ) + + data = client.get("/cached-hits").json() + + assert data["total_entries"] == 1 + assert data["valid_entries"] == 0 + assert data["expired_entries"] == 1 + assert data["expired_hits"] == 1 + + def test_openapi_marks_only_the_hits_fields_deprecated( + self, app: FastAPI, client: TestClient + ) -> None: + add_routes(app, include_in_schema=True, dependencies=[]) + + schema = client.get("/openapi.json").json()["components"]["schemas"] + properties = schema["CacheHitsResponse"]["properties"] + + for name in ("cached_hits", "total_hits", "valid_hits", "expired_hits"): + assert properties[name]["deprecated"] is True, name + for name in ("entries", "total_entries", "valid_entries", "expired_entries"): + assert "deprecated" not in properties[name], name