fix(server): retry FileSink writes instead of latching off on first failure#136
Merged
Conversation
…ailure The per-job log sink latched permanently on the first file-write failure, dropping every subsequent line for the rest of the job — a real data-loss window when the cause was transient (brief I/O hiccup, dir momentarily missing). #47's log-tail endpoint now makes that gap visible to operators, so the trade against transient failures is worth fixing. Replace the permanent kill-switch with a suppress-and-retry gate: on a write or open failure the sink reopens a fresh CREATE,APPEND writer, gated by lines-dropped or time-elapsed (whichever first) so a permanent cause like disk-full doesn't reopen per line and stderr sees one trace per episode. On recovery it records a resume marker; on close, a dropped-line summary. Both markers surface through the #47 tail; droppedLineCount exposes the running total. Transient open/write failures that later clear now recover; file-deleted-under-open-fd stays out of scope (documented). Resolves #52. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETHm6wCYdzFuB3kNEBayjF
This was referenced Jun 26, 2026
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.
What
FileSink(per-job log sink) latched permanently on the first file-write failure, silently dropping every subsequent line for the rest of the job (the stdout tee kept working). One transient blip — brief I/O hiccup, dir momentarily missing — killed file logging for the whole job: a real data-loss window in the per-job log, now visible to operators because the file-tail logs endpoint (#47) is wired.This replaces the permanent kill-switch with a suppress-and-retry gate + a drop counter (issue #52 options 1 + 3):
CREATE,APPENDwriter — gated byretryAfterLines(50) orretryAfterNanos(5s), whichever first, so a permanent cause (disk-full) doesn't reopen per line and stderr sees one trace per failure episode.file logging resumed after N dropped line(s)marker; onclose()aN log line(s) droppedsummary. Both flow through the File-tail logs endpoint GET /api/jobs/{id}/logs #47 tail for free.droppedLineCountexposes the running total.logDirreappears) andwrite/flushfailures that later clear (disk freed, perms restored).Internals: the primary constructor is now
private[jobs]with injectedopenWriter/nowNanos/ thresholds — the deterministic test seam.make/closesignatures are unchanged, soJobRunnerintegration is untouched, and a clean run produces byte-identical file output to before. All mutable state moved tovars guarded by the existingfileLockmonitor (every access under the lock; init safely published via ZIO fiber handoff).Fixes
Resolves #52.
Testing
ccas.server.jobs.TestFileSink(6 sleep-free tests, count-gate as the deterministic trigger): transient open-failure recovery, mid-job flush-failure recovery + stderr-once, counter accuracy/no-double-count, close-summary (file + stderr fallback + idempotent), no-reopen-after-close.ccas.utils.TestJobLogSink(latch→closed, disabled→suppressed); assertions unchanged.ccas.server.jobs.TestJobRunner(FileSink integration) green; fullsbt testgreen (960 passed).Follow-up (not in this PR)
Surface
droppedLineCountvia ajob_runcolumn so the count shows in the jobs API, not only in-file — needs aJobRuncase-class +sql/migration change. Deferred.🤖 Generated with Claude Code
https://claude.ai/code/session_01ETHm6wCYdzFuB3kNEBayjF