Decouple container startup from catalog source rescans - #18
Merged
Conversation
Remove the migrate-gated import_sources/update_database/update_dfcs block from the django entrypoint - a purely schema-only migration (e.g. Tag.display_name) shouldn't trigger a multi-source rescan that can take tens of minutes and blocks gunicorn from binding, as happened in today's incident. Only migrate + import_sources (both fast) now run before gunicorn; ongoing content sync is covered by the pre-existing daily/weekly django-q schedules, plus a narrow async bootstrap-scan guard for a genuinely fresh instance. Also bounds the previously fully sequential per-source scan loop with concurrency, and isolates one source's failure from crashing the whole run (root cause of today's crash). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016i9S7LQsCL3FGaih3ZTRBJ
WilfordGrimley
marked this pull request as ready for review
July 14, 2026 21:34
WilfordGrimley
added a commit
that referenced
this pull request
Jul 14, 2026
Time-to-bind ~29s, first parallel scan 6m9s/252 sources/0 429s, schedule liveness evidence — closes out PR #18's deploy-verification report. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016i9S7LQsCL3FGaih3ZTRBJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the plan reviewed in
journal/2026-07-14-startup-scan-decoupling-plan.md(local, gitignored — summary below) for today's incident: merging PR #11/#14/#15 triggered a migration-gated full catalog rescan on restart, which crashed django (unhandledIntegrityError) and left the API down for ~1.5h until manually caught and restarted.Investigation findings (see journal for full detail):
update_databaseschedule (+ weeklyupdate_dfcs/import_canonical_card_data) already exists via django-q, seeded by migrations0043/0048, confirmed via productionTaskhistory: 7 consecutive successful daily runs. The entrypoint's boot-time block was a redundant duplicate of this for every restart, not a necessary fallback.modifiedTimeis already fetched per file, but the cost is in the listing call itself, which isn't shortened by post-listing filtering.Changes
docker/django/entrypoint.sh: removed themigrate --check-gated block. Now:migrate(always, fast) →import_sources(always, cheap, local CSV) →execgunicorn, unconditionally. Catalog scanning never blocks the API from binding again, regardless of duration or failure.import_sources.py): after syncingSourcerows, ifSource.objects.exists() and not Card.objects.exists(), enqueues one asyncupdate_databaserun via django-q. Narrow safety net for a genuinely fresh instance losing the race against the daily schedule's first (already near-immediate) firing — not aBOOTSTRAPenv flag.update_database.py): the outer per-source loop is now aThreadPoolExecutor(max_workers=MAX_SOURCE_WORKERS=8)instead of fully sequential. Drive's 200 req/s quota has ample headroom (today's sequential run was latency-bound, not quota-bound).update_database.py): wraps each source's scan so one source's exception (e.g. today's duplicate-key race) is logged and skipped rather than aborting the other ~250 sources — this is the direct fix for today's crash, independent of the parallelism change.docs/infrastructure.md: new "Startup vs. scheduled catalog sync" section recording the incident, the fix, and why; updated the rebuild-from-scratch note (manualupdate_databaseafter a rebuild is now optional, not required).Test plan
test_import_sources.py(3 cases covering the bootstrap guard's emptiness-check logic),test_sources.py(2 new cases: multi-source concurrent scan correctness, and one-source-failure isolation — both usingLocalFilesources, no network dependency, usingtransactional_dbsince the parallelized code now writes from worker threads on separate DB connections).mastertoo).bash -n docker/django/entrypoint.sh— syntax clean.Deliberately not in this PR
Taskresults) — noted as a small follow-up in the plan, not required for this fix.Holding as draft for review per your instructions — not merging or deploying to production without separate explicit go-ahead.
🤖 Generated with Claude Code
https://claude.ai/code/session_016i9S7LQsCL3FGaih3ZTRBJ