Skip to content

Refuse to publish a staging file whose write failed - #59

Merged
matt-edmondson merged 2 commits into
mainfrom
fix/faulted-staging-not-published
Sep 28, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
fix/faulted-staging-not-published

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #45

What was wrong

When the disk filled up on the last chunk of a download, the cache could publish a truncated object:

  • HashingStream added each chunk to the digest before writing it, so a chunk whose write failed still counted toward the digest.
  • StreamTee swallows the failure and keeps serving the client.
  • The download path then published without checking.

If the failed write was the last one, the digest still matched the oid, so the short file was published and every later cache hit served it.

Change

  • HashingStream adds a chunk to the digest only after the inner write succeeds, and records a failed write in Faulted.
  • StagingHandle.Faulted exposes that flag.
  • ObjectStore.PublishAsync discards a faulted handle before comparing digests and logs DiscardedIncompleteObject (event 1009). Because the check is in the store, it covers both the download path and the upload path. The upload path already checked SinkIsLive.

Test

ObjectStoreTests.PublishAsync_StagingWriteFailedOnTheLastChunk_DoesNotPublish sends an object through StreamTee into a staging stream whose final write throws IOException. It covers two cases: an object that fits in a single chunk, and a two-chunk object where the second chunk fails. The test checks that the client gets every byte, that nothing is published, and that the staging file is removed.

  • Without the fix, both cases fail on Assert.IsFalse(published). I checked this by stashing the GitLfsCache/ changes and running the test.
  • With the fix, the full suite passes: 328/328.

🤖 Generated with Claude Code

https://claude.ai/code/session_016wsoxnwaqMuAzvkm2xnzYh


Generated by Claude Code

HashingStream appended each chunk to the digest before writing it, so a write
that failed left bytes in the digest that never reached disk. StreamTee
abandons a failed staging sink and keeps serving the client, and the download
path published regardless, so when the failed write was the last one the
short file still matched its oid and was published. Every later hit served
the truncated object.

HashingStream now digests only after the inner write succeeds and records
that a write failed. StagingHandle exposes that as Faulted, and
ObjectStore.PublishAsync discards a faulted handle before comparing digests,
which covers the download and upload paths alike.

Fixes #45

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wsoxnwaqMuAzvkm2xnzYh
Comment thread GitLfsCache.Tests/Storage/FailingWriteStream.cs Fixed
@sonarqubecloud

Copy link
Copy Markdown

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.

A disk-full error on the final chunk of a download publishes a truncated object that passes hash verification

2 participants