Skip to content

fix(streaming): report a truncated final article as corruption instead of a bare EOF - #945

Merged
javi11 merged 1 commit into
mainfrom
fix-streaming-high-cpu
Sep 9, 2026
Merged

javi11 merged 1 commit into
mainfrom
fix-streaming-high-cpu

Conversation

@javi11

@javi11 javi11 commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

A user on 026bd93 reported high CPU while streaming. Their log was one client retrying the same 4760-byte tail range ~15×/s for the whole window, every attempt failing with unexpected EOF: reader ended at offset N before the requested end.

Root cause: the file's final article holds fewer bytes than its metadata claims (truncated last part), so the advertised FileSize can never be served. Since #940 the no-progress guard returned a bare io.ErrUnexpectedEOF. WebDAV had already committed a 206 with the full Content-Length, the body came up short, nothing reached the health pipeline, and the player (MKV cues live at the tail) retried forever. Each retry rebuilt the reader and fetched the final article twice.

Fix:

  • truncatedTailError(at) wraps the verdict in a usenet.DataCorruptionError (NoRetry, FileOffset at the truncation point).
  • Both stalled sites (Read, shared ReadAtContext) route it through classifyReadError → health update → repair trigger → CorruptedFileError (503). Follow-up requests return 404 without touching Usenet.
  • The ephemeral ReadAt path applies the same verdict when a fresh reader inside the advertised size yields nothing.
  • The error still unwraps to io.ErrUnexpectedEOF, so the fix(streaming): read the padded final AES block; stop rotating readers that make no progress #940 tests keep passing.

Test plan

  • New tests in internal/nzbfilesystem/truncated_tail_test.go (RED before, GREEN after): Read path, ReadAt path, and health/repair trigger with the real SQLite health repo.
  • go test ./internal/nzbfilesystem/... ./internal/webdav/... ./internal/usenet/... pass; go vet clean.
  • Live replay against a fixture built from a real 8.9 GB file with a 4760-byte truncated last segment: first request logs "Streaming failure detected" and returns 503; a replayed storm of 420 requests all return 404 with no article fetches.
  • Local golangci-lint is broken with Go 1.27 (export-data version mismatch); relying on CI lint.

…d of a bare EOF

A file whose metadata advertises more bytes than its last article holds
could never serve its tail. Since #940 the reader returned io.ErrUnexpectedEOF
there, which the WebDAV layer passed through as a 206 with a short body and
which never reached the health pipeline. Players that want the tail (MKV cues)
retried the same range ~15 times a second, and every attempt rebuilt the
reader and fetched the final article twice.

Wrap the no-progress verdict in a DataCorruptionError (NoRetry, FileOffset at
the truncation point) and route it through classifyReadError, so the failure
records file health, triggers repair, and returns CorruptedFileError (503).
Follow-up requests get a 404 without touching Usenet. The ephemeral ReadAt
path applies the same verdict when a fresh reader inside the advertised size
yields nothing. The error still unwraps to io.ErrUnexpectedEOF.
@javi11
javi11 merged commit 757e5d9 into main Sep 9, 2026
2 checks passed
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.

1 participant