Skip to content

K2GO-390 fix(kiwix): self-healing catalog to end the stale-download loop - #554

Merged
luisguzman-adfa merged 7 commits into
mainfrom
feat/K2GO-390-kiwix-catalog-freshness
Sep 6, 2026
Merged

K2GO-390 fix(kiwix): self-healing catalog to end the stale-download loop#554
luisguzman-adfa merged 7 commits into
mainfrom
feat/K2GO-390-kiwix-catalog-freshness

Conversation

@luisguzman-adfa

Copy link
Copy Markdown
Collaborator

What

The Kiwix ZIM catalog shipped as a fixed asset with no freshness. Kiwix prunes
old dated ZIMs, so a baked entry points at a URL that 404s (aria2 code 3). A
failed item was never removed from the wishlist, so the post-install drain
re-attempted it every ~2s forever.

This makes the catalog self-healing and bounds the retry:

  • KiwixCatalog is overlay-aware: it prefers a pulled CSV over the APK asset (the
    asset stays the offline baseline), reusing the Kolibri freshness core (manifest
    • ETag + overlay + scheduler + worker; no tree machinery -- Kiwix is flat).
  • On a download 404 the guard forces an expedited, TTL-bypassing refresh. The
    date-free wishlist key re-resolves to the current dated file on the next drain
    -> heal. Failures are counted against the catalog version (overlay mtime); a
    refreshed catalog renews the budget, and after 5 failures against an unchanging
    catalog the item is dropped -> the loop is bounded.
  • CatalogRefreshWorker/Scheduler gain a force flag (bypass TTL) + an expedited
    forceRefresh; catalog-agnostic, so Kolibri benefits too.
  • build_kiwix_catalog.py --manifest/--csv-url emit kiwix.manifest.json for the
    hosting pipeline.

Two catalog-constructor bugs found and fixed while verifying on-device:

  • Re-release dates carry a letter (2026-07a). The generator only treated
    \d{4}-\d{2} as a date, so the letter leaked into the flavour and destabilized
    the self-heal key across a roll-over. Fixed in build_kiwix_catalog.py (a shared
    DATE_RE).
  • The runtime entry key joined creator+flavour with an invisible U+0001 written
    inside a "" literal. It works internally (all sites copy the key) but reads as
    an empty separator and breaks any key built from parts. Now an explicit
    KEY_SEP constant (same char, no migration).

Why

Offline-first: with no network the asset/overlay serve with no error. No UI
change and no size re-confirm (month-to-month delta is negligible). Authority:
controller/docs/ADR-390-kiwix-catalog-freshness.md.

Verification

Device (OnePlus, arm64, API 35), with a real stale entry
(bulbagarden_en_all_nopic 2026-07, 404 on the mirror):

  • Pre-fix build: the 404 re-drains forever (35+ attempts, never stops).
  • Heal: 404 -> forceRefresh pulls the overlay -> re-resolves to the live 2026-05
    -> downloads.
  • Bounded (no newer version): 404 -> exactly 5 attempts -> item dropped -> the
    loop stops.

Coordination / follow-up

Ops: regenerate and publish the catalog (asset + hosted CSV/manifest) by
re-running the fixed generator, so the letter-date entries collapse and can heal.
This re-runs the fixed constructor -- no manual data patching.

…irst)

Design authority for the fix to the "infinite loop attempting to download
content" bug: the Kiwix catalog ships as a fixed asset with no freshness, so a
pruned dated ZIM URL 404s and the provisioning drain re-attempts it forever.

Decision: a self-healing FLAT catalog. Keep the asset as the offline baseline;
layer a light, connectivity-guarded freshness on top, reusing Kolibri's
catalog-agnostic core (manifest + ETag + overlay + store + scheduler, ADFA-5094)
-- and NOT its N-level tree machinery, which Kiwix does not need. Hybrid trigger:
a TTL-gated conditional (ETag/304) check at catalog open, plus a forced refresh on
a 404. The wishlist key is date-free (project|lang|flavour), so the same selection
re-resolves to the current dated file after a refresh; the refresh itself is the
classifier (changed file = was stale, heal + retry; unchanged = real failure,
stop bounded). No UI, no size re-confirm. The infinite loop is fixed as a
consequence. Requires publishing catalogs/kiwix.manifest.json + the refreshed CSV
(build_kiwix_catalog.py already exists).
… loop

A stale baked kiwix_catalog.csv points a ZIM at a dated URL that Kiwix has pruned
(only the YYYY-MM date rolls over; the identity is stable). It 404s (aria2 code 3),
and because a FAILED item was never removed from the wishlist, the provisioning
drain re-attempted it every ~2s forever (device-proven). ADR-390.

Integral fix, reusing Kolibri's catalog-agnostic freshness core (no tree machinery,
Kiwix is flat):
- KiwixCatalog is overlay-aware (prefer the pulled CSV over the APK asset; the asset
  stays the offline baseline). It nudges the refresh once per process (weekly +
  opportunistic, TTL-gated) and loads under a lock.
- On a download failure the guard forces a refresh (expedited, network-constrained
  so offline is a no-op). The date-free wishlist key re-resolves to the current
  dated file on the next drain -> heal. Failures are counted against the catalog
  version (overlay mtime); a refreshed catalog renews the budget, and after
  MAX_HEAL_ATTEMPTS against an unchanging catalog the item is dropped -> the loop is
  bounded, no more infinite re-drain.
- CatalogRefreshWorker/Scheduler gain a force flag (bypass TTL) + an expedited
  forceRefresh; catalog-agnostic, so Kolibri benefits too.
- ContentDownloadSession.Host gains onItemError (default no-op; books unaffected).
- build_kiwix_catalog.py --manifest/--csv-url emit kiwix.manifest.json (sha256+url)
  for the hosting pipeline (mirrors kolibri.manifest.json).

Offline-first: no network -> asset/overlay serve with no error. No UI, no size
re-confirm (month-to-month delta is negligible). Compiles clean; two-pass review
applied (expedited heal, synchronized load, once-per-process nudge).
Review second pass: ZimDownloadService.onItemError re-derived the catalog
version by re-opening the overlay with a hardcoded basename, duplicating
KiwixCatalog's CSV_ASSET and its overlay-mtime notion. Expose
KiwixCatalog.catalogVersionTag(ctx) so the version identity has a single
owner; the service calls it. No behavior change.
…e flavour

Kiwix re-releases within a month add a letter suffix (2026-07a, 2026-07f).
parse_name only treated \d{4}-\d{2} as a date, so the letter leaked into the
flavour: terraria.wiki.gg_en_all_2026-07 and _2026-07a produced different
entry keys (all vs all_2026-07a). The self-heal key is creator+flavour, so a
letter roll-over broke re-resolution -- the item could not heal and was dropped.
Match \d{4}-\d{2}[a-z]? in one shared DATE_RE so both date checks agree and the
key stays stable across a re-release. Needs a catalog regen to take effect.
The catalog map key joins creator and flavour with a U+0001 control char so
"<creator><flavour>" keys cannot collide across rows. It was written as an
invisible char inside a "" literal, so the source read as an empty separator
and any key built from parts (a deep link, external tooling, a hand-staged
wishlist) silently failed to match the catalog. Spell it out as
KEY_SEP = "\u0001" -- the same char, so existing persisted keys still match
(no behavior change, no migration); it is now visible and greppable. Drop the
same stray char from the class-doc comment.
…tate

- ADR-390: Status Proposed -> Accepted (implemented, device-verified). Section 7
  hosting is now live (manifest + CSV published; a 404 self-healed end to end on
  device 2026-09-05), not a pending dependency. Record the re-release letter in
  the date (YYYY-MM[a-z]?) and the DATE_RE fix; correct the wishlist key to
  project|lang|<creator+flavour>; state the MAX_HEAL_ATTEMPTS (5) bound; mark the
  device verification done with results (unit coverage of the wiring is a
  follow-up).
- KiwixCatalog: the shape comment now shows the entry key joins creator and
  flavour with KEY_SEP (was misleading after the delimiter was made explicit).
- build_kiwix_catalog.py: document the --manifest ops path in the usage header.
@luisguzman-adfa
luisguzman-adfa merged commit c578a04 into main Sep 6, 2026
3 checks passed
@luisguzman-adfa
luisguzman-adfa deleted the feat/K2GO-390-kiwix-catalog-freshness branch September 6, 2026 03:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant