feat(imports): recover an import whose background job was killed - #8
feat(imports): recover an import whose background job was killed#8Rl0007 wants to merge 1 commit into
Conversation
A worker killed mid-job left the Import in a running status forever: nothing clears it, the UI keeps showing an in-flight job, and the only way back was editing `status` by hand. - parse / remediate / generate clear `error` when a run starts, so a traceback from an earlier run isn't displayed against a fresh run; - `retry_import` restarts a Failed or stale-running import — re-enqueuing the parse when nothing was parsed, else handing it back to the stage before the lost job. A live job is never interrupted; - `wikify.tasks.fail_stuck_imports` (cron, every 15 minutes) fails an import that has been in a running status with no progress for 30 minutes. Jobs publish progress on every page, so a long-running pass is never touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126A9jCM6LTxnmCPH3hWbTR
Greptile SummaryThis PR adds recovery for imports abandoned by killed background workers, clears stale job errors, and schedules detection of imports with no recent progress.
Confidence Score: 2/5The recovery paths need correction before merging because they can restore imports to the wrong workflow gate, expose incomplete parses as review-ready, and race with workers that resume reporting progress. The scheduler discards the stage needed for generation recovery, retry relies on source_document before the parse pipeline is necessarily complete, and both stale-recovery paths mutate state after non-atomic liveness checks. Files Needing Attention: wikify/api/imports.py, wikify/tasks.py
|
| Filename | Overview |
|---|---|
| wikify/api/imports.py | Adds retry orchestration, but loses generation-stage provenance after scheduled failure and mistakes a partially completed parse for a review-ready artifact. |
| wikify/tasks.py | Adds stale-import detection, but replaces stage-specific state with generic Failed and performs the failure write without atomically revalidating staleness. |
| wikify/jobs/parse.py | Clears stale errors at job start; its existing ordering demonstrates that source_document can be present before tree construction completes. |
| wikify/jobs/remediate.py | Clears a previous error when remediation starts, with no independent issue identified. |
| wikify/jobs/generate.py | Clears a previous error when generation starts, with no independent issue identified. |
| wikify/hooks.py | Registers the new stale-import task on a 15-minute cron schedule. |
| wikify/tests/test_job_recovery.py | Covers individual retry and scheduler branches but misses scheduler-to-retry generation recovery, partial parse completion, and concurrent progress races. |
Prompt To Fix All With AI
### Issue 1
wikify/api/imports.py:125-127
**Generation stage is lost**
When the scheduler marks a stale `Generating Wiki` import as `Failed`, this branch no longer knows which stage failed and returns the import to `Review` instead of `Graphed`, causing `generate_wiki` to reject the retry until the tree is manually approved again.
### Issue 2
wikify/api/imports.py:112-127
**Partial parses become review-ready**
When parsing dies after storing `source_document` but before remediation, tree rebuilding, or classification finishes, this check skips parse enqueueing and moves the import directly to `Review`, exposing an incomplete or empty section tree as ready for review.
### Issue 3
wikify/tasks.py:37-47
**Staleness check races progress**
If a delayed worker publishes progress after this query selects its import but before the unconditional failure write, the refreshed import is still marked `Failed`; the same read-check-write race in `retry_import` can reset or re-enqueue work while the original worker continues, causing conflicting statuses or concurrent parse jobs.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "feat(imports): recover an import whose b..." | Re-trigger Greptile
| resume_status = "Graphed" if imp.status == "Generating Wiki" else "Review" | ||
| imp.db_set({"status": resume_status, "error": None}) | ||
| publish_progress(import_name, 100, f"Ready to retry from {resume_status}", status=resume_status) |
There was a problem hiding this comment.
When the scheduler marks a stale Generating Wiki import as Failed, this branch no longer knows which stage failed and returns the import to Review instead of Graphed, causing generate_wiki to reject the retry until the tree is manually approved again.
Knowledge Base Used: Document processing pipeline
Prompt To Fix With AI
This is a comment left during a code review.
Path: wikify/api/imports.py
Line: 125-127
Comment:
**Generation stage is lost**
When the scheduler marks a stale `Generating Wiki` import as `Failed`, this branch no longer knows which stage failed and returns the import to `Review` instead of `Graphed`, causing `generate_wiki` to reject the retry until the tree is manually approved again.
**Knowledge Base Used:** [Document processing pipeline](https://app.greptile.com/bwh-tech/-/custom-context/knowledge-base/bwhtech/wikify/-/docs/document-processing-pipeline.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| if not imp.source_document: | ||
| imp.db_set({"status": "Queued", "error": None}) | ||
| publish_progress(import_name, 0, "Queued for retry", status="Queued") | ||
| frappe.enqueue( | ||
| "wikify.jobs.parse.run", | ||
| queue="long", | ||
| timeout=3600, | ||
| import_name=import_name, | ||
| ) | ||
| return import_name | ||
|
|
||
| # The parse result (and any approved tree) is intact — hand the import back to the | ||
| # stage it was in before the lost job so the user can re-run it from the UI. | ||
| resume_status = "Graphed" if imp.status == "Generating Wiki" else "Review" | ||
| imp.db_set({"status": resume_status, "error": None}) | ||
| publish_progress(import_name, 100, f"Ready to retry from {resume_status}", status=resume_status) |
There was a problem hiding this comment.
Partial parses become review-ready
When parsing dies after storing source_document but before remediation, tree rebuilding, or classification finishes, this check skips parse enqueueing and moves the import directly to Review, exposing an incomplete or empty section tree as ready for review.
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: wikify/api/imports.py
Line: 112-127
Comment:
**Partial parses become review-ready**
When parsing dies after storing `source_document` but before remediation, tree rebuilding, or classification finishes, this check skips parse enqueueing and moves the import directly to `Review`, exposing an incomplete or empty section tree as ready for review.
**Knowledge Base Used:**
- [Import and source parsing](https://app.greptile.com/bwh-tech/-/custom-context/knowledge-base/bwhtech/wikify/-/docs/import-and-parsing.md)
- [Document processing pipeline](https://app.greptile.com/bwh-tech/-/custom-context/knowledge-base/bwhtech/wikify/-/docs/document-processing-pipeline.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| stuck = frappe.get_all( | ||
| "Wikify Import", | ||
| filters={"status": ("in", RUNNING_STATUSES), "modified": ("<", stale_cutoff())}, | ||
| fields=["name", "status"], | ||
| ) | ||
| for imp in stuck: | ||
| message = ( | ||
| f"No progress for {STALE_MINUTES} minutes while {imp.status} — the worker running " | ||
| f"this import was lost. Retry the import to restart it." | ||
| ) | ||
| frappe.db.set_value("Wikify Import", imp.name, {"status": "Failed", "error": message}) |
There was a problem hiding this comment.
Staleness check races progress
If a delayed worker publishes progress after this query selects its import but before the unconditional failure write, the refreshed import is still marked Failed; the same read-check-write race in retry_import can reset or re-enqueue work while the original worker continues, causing conflicting statuses or concurrent parse jobs.
Knowledge Base Used: Backend API and realtime services
Prompt To Fix With AI
This is a comment left during a code review.
Path: wikify/tasks.py
Line: 37-47
Comment:
**Staleness check races progress**
If a delayed worker publishes progress after this query selects its import but before the unconditional failure write, the refreshed import is still marked `Failed`; the same read-check-write race in `retry_import` can reset or re-enqueue work while the original worker continues, causing conflicting statuses or concurrent parse jobs.
**Knowledge Base Used:** [Backend API and realtime services](https://app.greptile.com/bwh-tech/-/custom-context/knowledge-base/bwhtech/wikify/-/docs/backend-api-and-realtime-services.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Three small changes so a killed worker doesn't leave an Import unrecoverable. A worker
died three times mid-job on a 398-page import last night; each time the Import was left
in
Remediatingat 0% with no way back except hand-editingstatusthrough the API.1. The stale error is cleared when a run starts
erroris written injobs/parse.py,jobs/remediate.pyandjobs/generate.py(each inan
exceptblock) and was never cleared again — a traceback from a failed run weeks agostayed on the import and was displayed while a fresh run was in progress. Each job now
clears it alongside the status flip that starts the run.
2.
retry_import— a whitelisted way backtrigger_remediationonly runs fromReview, andgenerate_wikionly fromGraphed/Completed, so an import inFailedor stuck in a running status had nowhitelisted entry point at all.
wikify.api.imports.retry_import(import_name):Failed, or from a running status whose progress has gone stale — a live jobis never interrupted (
frappe.throwotherwise);to the stage before the one that died (
Graphedfor a lost wiki generation, elseReview) so remediation or generation can be re-run from the UI. The parse result andany approved tree are left intact.
3.
fail_stuck_imports— stuck jobs stop being invisibleNew scheduled task (
wikify/tasks.py), wired as a cron every 15 minutes, that marks animport
Failedwhen it has been inQueued/Parsing/Remediating/Generating Wikiwith no progress written for 30 minutes, and logs the reason to theimport's log.
It keys off time-since-last-progress, not total runtime:
publish_progresswrites onevery page, so the real 398-page remediation (~50 minutes wall clock, a write every few
seconds) is never touched, while a worker that died 30 minutes ago is.
Registered on the dev site after
bench migrate:Tests
wikify/tests/test_job_recovery.py— 10 tests over the real DB covering all threechanges: both jobs clearing the earlier error, each
retry_importbranch (resume,re-parse, refuse a live job, refuse a completed import, recover a stale one), and the
scheduled task failing a stuck import while leaving a slow-but-reporting job and an
import at rest alone.
Both error-clearing tests were verified red before the change:
Full suite (
bench --site wikify.localhost run-tests --app wikify):test_renders_with_wiki_rendereralso fails onmain(the installed wiki app no longeremits
<pre class="mermaid">) and is unrelated to this change. Everything else passes.No UI is wired to
retry_importin this PR — it is the API half only.🤖 Generated with Claude Code
https://claude.ai/code/session_0126A9jCM6LTxnmCPH3hWbTR