Files
KohakuHub/CHANGELOG.md
narugo1992 27271a8ded feat(fallback): strict-freshness cache contract — per-(user, tokens_hash) keying + 3-dim generation counters + repo CRUD invalidation (#79)
Locks down the strict-freshness cache contract designed in #79:

- Cache key now `(user_id|anon, tokens_hash, repo_type, namespace,
  name)`; new `compute_tokens_hash` derives a stable 16-hex prefix from
  the user's effective per-source tokens (DB + Authorization-header
  passed). Two users sharing a repo, or one user swapping external
  tokens between requests, get isolated cache buckets.
- Three monotonic generation counters (`global_gen`, `user_gens[uid]`,
  `repo_gens[(rt, ns, name)]`) guard against the "an invalidation
  event lands mid-probe and the probe writes a now-stale binding"
  race. `safe_set` reads a snapshot at probe entry and rejects the
  write if any of the three has been bumped — closing all three race
  classes (admin source mutation, user token rotation, repo CRUD).
- New cache primitives: `invalidate_repo(repo_type, ns, name)` (clears
  every user bucket for one repo, bumps `repo_gens[...]`) and
  `clear_user(user_id)` (clears every repo bucket for one user, bumps
  `user_gens[uid]`).
- Invalidation hooks wired into:
  - `api/auth/external_tokens.py` POST/DELETE/PUT bulk → `clear_user`
  - `api/repo/routers/crud.py` /repos/create, /repos/delete, /repos/move
    (both old and new ids) → `invalidate_repo`
  - `api/settings.py` PUT settings (only when `private` flips) →
    `invalidate_repo`
  - `api/admin/routers/fallback.py` POST /fallback-sources → `cache.clear()`
    (parallels existing UPDATE/DELETE)
- New admin endpoints for operational hygiene:
  - DELETE /admin/api/fallback-sources/cache/repo/{rt}/{ns}/{name}
  - DELETE /admin/api/fallback-sources/cache/user/{user_id}
- `try_fallback_*` operations and `_run_cached_then_chain` thread the
  authenticated `user` (or `None`) and `tokens_hash` through the cache
  layer; decorator wires `user=user` into all four op variants.

Test coverage:
- `cache.py` 100% (35 tests in `test_cache.py`: hash invariance,
  per-user/per-tokens isolation, eviction scope, gens, safe_set
  acceptance/rejection on each dimension, key-shape pinning)
- `operations.py` 100% (existing test suite + new
  `test_strict_freshness.py` 8 tests for per-user binding isolation
  and three race classes via the live op layer)
- API hooks: `test_cache_invalidation_hooks.py` 15 tests, one per
  documented event in the invalidation matrix
- HF-Hub end-to-end: `test_strict_consistency.py` adds two new tests
  using the real `huggingface_hub` library + scenario_hf_server to
  prove per-user bucket isolation and external-token-rotation
  eviction across the wire.

347 tests pass across the changed-file scope; cache.py + operations.py
both at 100% line coverage.

Closes #79
Refs: #77, #78

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:00:11 +08:00

4.3 KiB

Changelog

All notable changes to KohakuHub are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Added

  • Fallback cache: strict-freshness contract (#79). Cache key now includes user_id and a tokens_hash derived from the user's effective external tokens. Two users (or one user with different per-request Authorization: Bearer ...|url,token|... external tokens) can no longer share a binding. Three monotonic generation counters (global_gen, user_gens[uid], repo_gens[(rt, ns, name)]) drive a safe_set race-protection check that rejects cache writes whose pre-probe snapshot disagrees with the post-probe state — closing the admin-mutation / token-rotation / repo-CRUD races where a probe in flight could otherwise pin a stale binding after an invalidation.
  • Fallback cache: invalidation hooks (#79). cache.invalidate_repo(repo_type, ns, name) and cache.clear_user(user_id) provide the two new eviction primitives; hooked into local repo create/delete/move/visibility-toggle, user external-token POST/DELETE/PUT bulk, and admin source create (the last one parallels the existing UPDATE/DELETE that already evicted).
  • Admin endpoints: per-repo and per-user fallback cache eviction (#79). New DELETE /admin/api/fallback-sources/cache/repo/{repo_type}/{namespace}/{name} and DELETE /admin/api/fallback-sources/cache/user/{user_id} for operational hygiene.
  • Fallback: repo-grain binding. Within a single bind window every read against one repo_id goes to exactly one source. Eliminates cross-source mixing where the SPA showed source A's metadata while resolve served source B's bytes for the same repo. Implements the contract from #75. (#77)
  • Fallback: per-loop, per-repo binding lock. Concurrent cache-miss callers serialize on the lock so all observers agree on the same bound source instead of independently scanning the chain. (#77)
  • Fallback: three-state classifier FallbackDecision. BIND_AND_RESPOND / BIND_AND_PROPAGATE / TRY_NEXT_SOURCE in kohakuhub.api.fallback.utils.classify_upstream, mirroring huggingface_hub.utils.hf_raise_for_status priority order (X-Error-Code wins over numeric status). (#77)

Changed

  • Fallback: disabled upstream marker now classifies as TRY_NEXT_SOURCE instead of BIND_AND_PROPAGATE. When HuggingFace returns X-Error-Message: "Access to this resource is disabled." (moderation takedown), the chain now advances to the next source rather than immediately propagating DisabledRepoError. The aggregate response surfaces the disabled marker only when every source in the chain returns disabled. huggingface_hub clients are unaffected — they don't inspect chain internals — but tooling that depended on first-source disabled propagating verbatim must update. (#77)
  • Fallback: with_repo_fallback decorator gates the 404 fall-through on the local response's X-Error-Code. A local 404 carrying EntryNotFound or RevisionNotFound is now authoritative ("the local repo exists; this entry/revision is missing") and is not dispatched to the fallback chain. The chain is entered only when the local layer signals RepoNotFound or returns a 404 without X-Error-Code. Restores the user-stated guarantee that a local repo wins absolutely on its namespace, regardless of upstream state. (#77)

Tracked

Planned follow-up work surfaced during the #77 risk review:

  • #78 — lower default fallback cache TTL (KOHAKU_HUB_FALLBACK_CACHE_TTL) from 300 to 60, decouple chain-probe logic into a pure core.probe_chain function, and add admin endpoints + frontend panel for real / simulated chain testing.