[Hardening] Replace builtin hash() with a deterministic digest in lore match chunk indexing #88
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
The lore-extraction pipeline checkpoints per-type match results in
lore_extract_cacheusing synthetic negativechunk_indexvalues derived from Python's builtinhash():webapp/backend/app/tasks/reminder_tasks.py:661— stores match rows attype_index = -(abs(hash(entry_type)) % 10_000 + 1)(comment at 657-658 explains the negative-index scheme distinguishes them from real chunk rows, which are >= 0)webapp/backend/app/tasks/reminder_tasks.py:723-725— the consolidation step recomputesexpected_indices = {-(abs(hash(t)) % 10_000 + 1) for t in expected_types}and waits until all are presentPython's
hash()on strings is salted byPYTHONHASHSEED, which is randomized per interpreter process.PYTHONHASHSEEDis not pinned anywhere in this repo (verified). Celery prefork children inherit the parent's seed, so the happy path works — every process in one worker instance computes the same indices.Current behavior
If the Celery worker restarts mid-pipeline (deploy, OOM, crash), the new interpreter has a new hash seed. Match rows already written under the old seed become unfindable:
lore_consolidate_proposalsrecomputes differentexpected_indices, never sees the checkpointed rows, retries toMaxRetriesExceeded, and fails the run ("Consolidation timed out"). This defeats the exact crash-recovery purpose of the checkpoint cache. The same mismatch occurs with more than one worker container, since each process has its own seed.Fix / Spec
hash(...)expressions with a deterministic digest, e.g.: (Or use the ordinal of the sortedexpected_typeslist — either is fine; keep the negative-index scheme so match rows stay distinguishable from chunk rows.):661and:723-725) so they cannot drift.lore_extract_cacheis a transient checkpoint cache. In-flight rows written under the oldhash()scheme will simply miss once and the pipeline re-runs that step.Acceptance criteria
hash(usage for cache indexing inreminder_tasks.py.References
webapp/backend/app/tasks/reminder_tasks.py:657-672(write side),:716-726(read side /expected_indices)PYTHONHASHSEEDpin existsFiled from the July 2026 full-project review.
Picking this up as part of a v3.3.0 push. Landing on branch
hardening/backendtogether with #90, #97, #106, and #109 (grouped by component to keep the diffs reviewable).Fixed on
main(commit7977af7, merged via1c9c19f). Added a singlelore_match_chunk_index()helper (sha1-based, keeps the negative-index scheme) used at both the write site (lore_match_category) and read site (lore_consolidate_proposals) so they can't drift. Tests assert exact precomputed constants (npc=-2264,location=-8788,concept=-5198,item=-6992), stability across two subprocesses with differingPYTHONHASHSEED(and confirm builtinhash()genuinely differs across those seeds, so the test isn't vacuous), that consolidation finds rows written under the helper's index, and that nohash(remains for cache indexing. Backend suite green (358 passed), ruff clean.