Skip to content

fix(server): retry FileSink writes instead of latching off on first failure#136

Merged
Sootopolis merged 1 commit into
mainfrom
fix/52-filesink-retry
Jun 26, 2026
Merged

fix(server): retry FileSink writes instead of latching off on first failure#136
Sootopolis merged 1 commit into
mainfrom
fix/52-filesink-retry

Conversation

@Sootopolis

Copy link
Copy Markdown
Owner

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):

  • On a write/open failure the sink enters a suppressed state, counts the dropped line, and reopens a fresh CREATE,APPEND writer — gated by retryAfterLines (50) or retryAfterNanos (5s), whichever first, so a permanent cause (disk-full) doesn't reopen per line and stderr sees one trace per failure episode.
  • On recovery it writes a file logging resumed after N dropped line(s) marker; on close() a N log line(s) dropped summary. Both flow through the File-tail logs endpoint GET /api/jobs/{id}/logs #47 tail for free. droppedLineCount exposes the running total.
  • Recovers transient open failures that later clear (the logDir reappears) and write/flush failures that later clear (disk freed, perms restored).
  • Out of scope, documented: file deleted under an open fd (Linux keeps writing to the unlinked inode — no failure fires), FileTail offset desync after delete-recreate, disk-full mid-flush partial line.

Internals: the primary constructor is now private[jobs] with injected openWriter / nowNanos / thresholds — the deterministic test seam. make/close signatures are unchanged, so JobRunner integration is untouched, and a clean run produces byte-identical file output to before. All mutable state moved to vars guarded by the existing fileLock monitor (every access under the lock; init safely published via ZIO fiber handoff).

Fixes

Resolves #52.

Testing

  • New 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.
  • Reworded 2 now-misleading tests in ccas.utils.TestJobLogSink (latch→closed, disabled→suppressed); assertions unchanged.
  • ccas.server.jobs.TestJobRunner (FileSink integration) green; full sbt test green (960 passed).

Follow-up (not in this PR)

Surface droppedLineCount via a job_run column so the count shows in the jobs API, not only in-file — needs a JobRun case-class + sql/ migration change. Deferred.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ETHm6wCYdzFuB3kNEBayjF

…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
@Sootopolis
Sootopolis merged commit 64ef741 into main Jun 26, 2026
1 check passed
@Sootopolis
Sootopolis deleted the fix/52-filesink-retry branch June 26, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FileSink: revisit sustained write-failure handling

1 participant