fix(scheduler): recover truncated history without losing valid records - #103
Conversation
Encode each RunRecord into a buffer, write that one complete JSONL line, and return Close errors. Drop a leftover incomplete last line on read and trim it before the next append so crawlctl run, status, and logs stay usable after a short write. Complete corrupt lines still fail closed. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: needs changes before merge. Reviewed September 4, 2026, 5:21 AM ET / 09:21 UTC. ClawSweeper reviewWhat this changesThe PR repairs crawlctl scheduler-history recovery after interrupted JSONL writes while preserving valid final records without a newline and surfacing write cleanup errors. Merge readiness⛔ Needs changes before merge - 2 items remain Keep open: the prior valid-EOF data-loss blocker is fixed, but the recovery path still cannot process an interrupted final record over Scanner’s 64 KiB default limit, leaving scheduler commands blocked for that valid input shape. Priority: P2 Review scores
Verification
How this fits togethercrawlctl’s scheduler records completed refresh jobs in a local JSONL history file. The run, status, and logs commands consume that history, while a scheduled run appends the next record under a single-process lock. flowchart LR
Jobs[Configured refresh jobs] --> Run[Scheduler run]
Run --> History[Local JSONL history]
History --> Reader[History reader]
Reader --> Tail[Validate final record]
Tail --> Repair[Repair incomplete tail]
Repair --> Commands[Run status and logs]
Before merge
Findings
Agent review detailsSecurityNone. Review metrics
Root-cause clusterRelationship: Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Technical reviewBest possible solution: Make terminal-history recovery independent of Scanner’s default token ceiling, then retain the current valid-EOF, corruption, rollback, and Windows-handle guarantees. Do we have a high-confidence way to reproduce the issue? Yes, from source: configure a job whose serialized RunRecord exceeds 64 KiB, leave an interrupted final JSON value, then invoke scheduler run, status, or logs; Scanner errors before the new recovery branch can handle it. Is this the best way to solve the issue? No. The patch correctly addresses valid EOF records and ordinary interrupted tails, but it must also handle a terminal record beyond Scanner’s default limit for the advertised recovery behavior to hold. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against cf646f20a726. LabelsLabel changes:
Label justifications:
EvidenceAcceptance criteria:
What I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (7 earlier review cycles)
|
Integrate current main without rewriting the contributor history. Retain valid JSON at EOF and insert a separator before appending. Recover only a genuinely incomplete terminal JSON value, keep reads streaming, and inspect only the file tail during append. Return write, rollback and close errors. Regression coverage includes every interrupted byte boundary, missing newline/CRLF, complete corruption, physical partial writes, failed rollback and close errors. Built crawlctl proof covers status/logs/run and an actual kernel file-size-limit failure with byte-preserving rollback and subsequent recovery. Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
Go removes FILE_WRITE_DATA when opening O_APPEND handles on Windows, so recovery and failed-write rollback cannot truncate them. Open a read/write handle and seek to the validated append position under the scheduler's existing writer lock. The new cross-platform truncation and rollback regressions exposed the issue in Windows CI. Full local checks and real CLI file-size-limit recovery proof pass with the corrected handle mode. Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
Merge the dependency update from main, retaining both Unreleased entries and the tested history behavior. Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
Recover interrupted scheduler history writes without losing valid records. Current main rejects a truncated final JSON value, blocking
crawlctl run,status, andlogs. The original recovery proposal treated every missing newline as truncation, which also hid and later deleted valid JSON records at EOF.The corrected recovery distinguishes a genuinely incomplete terminal JSON value from a valid record or complete corrupt JSON. Valid EOF records retain their original bytes and receive a newline separator before the next record is appended. Complete corrupt records still fail closed. Reads remain streaming, and appends inspect only the trailing record instead of loading the whole history file. The file uses read/write access and an explicit append seek under the existing scheduler lock: Windows append-only handles cannot truncate. A partial write is rolled back to the pre-append length; write, rollback, and close failures are returned to the caller.
Validation:
make checkandGOWORK=off go test -count=1 ./...passed, including vet, deadcode, govulncheck, full tests/race tests, and 25 release guard tests.RLIMIT_FSIZEkernel write failure made the fixed CLI report “file too large,” restore the prior valid EOF record byte-for-byte, release its lock, and successfully append on the next unrestricted run.cf646f20a72664166107fa06d4ee2a41b3e439afpassed without actionable findings.Dependency PR #106 was integrated by normal merge commit cad6c55. Both changelog entries and the contributor's original commit remain in the branch. The combined candidate passed make check, actual CLI/kernel-write-failure proof, and a separate Go 1.27.0 storage/encryption consumer using SQLite 1.58.0 and its exact libc 1.75.6 pairing. Thanks @SebTardif (Sebastien Tardif) for the recovery fix and initial proof. The maintainer follow-up preserves that credit in its commit and the Unreleased changelog. Related prior proposal: #99.
The pre-existing Scanner record-size limit remains unchanged. A built-CLI comparison confirms both pre-fix main and this candidate reject valid and incomplete 70 KiB final records with the same scanner error; increasing supported record sizes is outside this fix.
No live archives, release workflows, or installed schedules were modified.
Final combined head
cad6c5533020e391c647196e78dd9bda0e1ae6cc, basec43a1e1b14f35cf4fec49e78c8d4d76fa789074e: Linux/Windows CI, CodeQL, and secret scan all passed. Combined full P0–P2 review passed with no actionable findings; uncached full Go tests also passed.