Startup reconciliation of stranded app installation states (#124)#178
Open
ClaydeCode wants to merge 6 commits into
Open
Startup reconciliation of stranded app installation states (#124)#178ClaydeCode wants to merge 6 commits into
ClaydeCode wants to merge 6 commits into
Conversation
The installation queue lives only in memory, so a restart between enqueue and completion strands installed_apps rows in INSTALLATION_QUEUED / INSTALLING / REINSTALLATION_QUEUED / REINSTALLING forever — the uninstall side already got startup reconciliation (7cf1073) but the install side did not. Add reconcile_interrupted_installs(), run right after the uninstall reconciliation in the lifespan (enqueue-only, so it stays clear of the worker-not-started-yet deadlock window). For each stranded row: - install with its zip still on disk -> re-enqueue "install from zip" - store/config install whose zip is gone -> re-enqueue "install from store" (the worker re-downloads) - custom install whose zip is gone -> mark ERROR; the source is unrecoverable, and ERROR stops it looking half-installed and blocking a fresh install - reinstall -> re-enqueue "reinstall"; it always re-downloads from the store and the worker self-heals to ERROR if the app is gone The status is reset to the matching *_QUEUED value before enqueueing so the worker's status assertion passes. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Decision-matrix tests (db tier, worker not started) assert the resulting DB status and the enqueued task type for every stranded status, plus that settled apps are left untouched. A full-lifespan test proves a row stranded in INSTALLATION_QUEUED / INSTALLING with missing files no longer boot-loops core and ends in ERROR. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Record reconcile_interrupted_installs() alongside the uninstall equivalent, and correct the now-stale claim that a missing-metadata row boot-loops core (write_traefik_dyn_config skips it defensively since #158). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…before traefik
Review-panel follow-ups:
- Reinstall is now settled to ERROR instead of re-enqueued. _reinstall_app
deletes the app's files before re-downloading, so resuming it unattended
could destroy a working install if the app store is briefly unreachable at
boot — exactly the case the API's app_exists_in_store precheck guards. ERROR
is a non-destructive terminal state the user retries manually.
- reconcile_interrupted_{uninstalls,installs} now run before
write_traefik_dyn_config, so a row reset to a non-routable status (e.g.
INSTALLING -> INSTALLATION_QUEUED) is excluded from the boot's Traefik config
rather than briefly routed at a half-installed container.
- An UNKNOWN installation_reason with no zip is resumed from the store (the
worker self-heals to ERROR if it is not there) rather than marked ERROR.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Reinstall statuses now assert ERROR (non-destructive settle) rather than re-queue. - Add a reconcile->worker->STOPPED lifespan test that drives a zip-on-disk install to completion, pinning that reconcile's task_type and status reset agree with the worker's status assertion end to end. - Mock the app store in the ERROR lifespan test so a misclassified custom install would reach STOPPED, giving the ERROR assertion teeth; extend it to the reinstall statuses. - Cover the zip-precedence-over-reason and UNKNOWN-reason branches. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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.
Closes #124.
What
Startup reconciliation for app-installation rows stranded by a restart. The install queue is an in-memory
asyncio.Queue, so a stop between enqueue and completion leavesinstalled_appsrows in an in-flight status forever. The uninstall side already gotreconcile_interrupted_uninstalls()(#161-era); this adds the install/reinstall side.New
reconcile_interrupted_installs(), run in the lifespan beforewrite_traefik_dyn_config(enqueue-only, so it stays clear of the worker-not-started-yet deadlock window):INSTALLATION_QUEUED, re-enqueue"install from zip"INSTALLATION_QUEUED, re-enqueue"install from store"(worker re-downloads; self-heals toERRORif the app is gone)ERROR(source is unrecoverable)REINSTALLATION_QUEUED/REINSTALLING) →ERROR._reinstall_appdeletes the app's files before re-downloading, so resuming it unattended could destroy a working install if the store is briefly unreachable at boot; the user retries manually.The boot-loop half of the issue (a stranded
INSTALLINGrow with missing metadata raisingMetadataNotFoundout of the lifespan) was already fixed by #158's defensivetry/exceptinwrite_traefik_dyn_config;agents.mdis corrected to match.Verify
tests/test_app_install_crash_recovery.py— 23 tests. Db-tier decision matrix (worker not started,enqueuemocked) asserts DB status + task type for every stranded status; two full-lifespan tests: one proves an unrecoverable row boots and settles toERROR(store mocked, so a misclassified row would reachSTOPPED— theERRORassertion has teeth), one drives a zip-on-disk install through reconcile → worker →STOPPED.test_traefik_dyn_spec/test_tours/test_app_lifecyclereproduce identically on the base commit with this branch's changes reverted and are environmental to the test VM (postgres container collision on :5433, image-pull/timing) — main CI is green. CI on this PR is the real gate.Recommended reading order
shard_core/app_factory.py— lifespan wiring/ordershard_core/service/app_installation/__init__.py—reconcile_interrupted_installs+ helperstests/test_app_install_crash_recovery.pyagents.mdReview panel
Ran: adversarial (always), test adversary (real logic added), DevEx/readability (new methods + tests). Skipped — no trigger: security, DB/migration, API-contract, UX.
Adversarial — blocking findings: none. Advisories addressed:
ERROR, never re-triggers the destructive path.write_traefik_dyn_config; a half-installedINSTALLINGrow with present metadata gets a stale router (502s that boot) → fixed (e6a642c): reconcile moved before the traefik write, so the not-routable reset takes effect.UNKNOWNreason + no zip markedERRORinstead of resumed → fixed (e6a642c):UNKNOWNresumes from store.STOPPEDwrite is markedERRORthough effectively installed → dismissed: cannot disambiguate "old intact install" from "partial new extraction" without a completion marker; conservativeERRORis safe and the user re-uploads. Narrow window.Test adversary — blocking findings: none (verified the db-tier tests have teeth by neutering reconcile → 10/10 active-status params failed). Advisories addressed:
test_unrecoverable_stranded_row_boots_and_settles_to_error, claim scoped to the load-bearingERRORtransition.ERRORcould pass for the wrong reason (custom misclassified as store → worker 404 →ERROR) → fixed (b71824e): store is now mocked, so a misclassified row reachesSTOPPEDand fails the assertion.STOPPEDend-to-end test → fixed (b71824e): addedtest_reconciled_install_completes_on_boot.DevEx — blocking findings: none. Advisories: added a one-line comment on the load-bearing status reset (b71824e/e6a642c); the flagged
_place_zipcross-test cleanup concern was verified moot (path_rootis a per-test tmp dir).This PR description was AI-drafted and human-reviewed. If verifying any part needs real digging on your side, say so and we'll do a call instead.