Skip to content

registry/storage: do not write hash state when closing a cancelled upload - #5021

Merged
milosgajdos merged 1 commit into
distribution:mainfrom
fjvbn2003:fix-cancelled-upload-hashstate
Oct 18, 2026
Merged

milosgajdos merged 1 commit into
distribution:mainfrom
fjvbn2003:fix-cancelled-upload-hashstate

Conversation

@fjvbn2003

Copy link
Copy Markdown
Contributor

Cancelling a blob upload (DELETE /v2/<name>/blobs/uploads/<uuid>, or the cleanup after a failed PUT) leaves a directory behind that upload purging never removes.

What happens in main:

  1. blobWriter.Cancel cancels the file writer, then calls bw.Close(), then removeResources deletes _uploads/<uuid>/.
  2. The handler closes the upload again after the request (defer buh.Upload.Close() in CancelBlobUpload, and closeResources in blobUploadDispatcher).
  3. blobWriter.Close calls storeHashState, which PutContents _uploads/<uuid>/hashstates/sha256/<offset>. That recreates the directory removeResources just deleted, this time without data or startedat.
  4. getOutstandingUploads gives an upload without startedat the default start time now + 10000h, so PurgeUploads never treats it as old and the directory stays forever.

The Close() inside Cancel also always fails on the filesystem driver, because fileWriter.Cancel has already closed the file, so every cancelled upload logs error closing blobwriter: ... at error level.

I found this on a registry that uses the filesystem driver (Harbor v2.15.2, whose registry is a v2.8.3 fork with the same Cancel/Close code and the same deferred close in the handler):

  • 12 of 12 DELETE .../blobs/uploads/<uuid> requests (all 204) in the current log were followed by error closing blobwriter: sync .../_uploads/<uuid>/data: file already closed, and each of those 12 directories still exists with only hashstates/sha256/0 (108 bytes).
  • Across the registry there are 2,012 upload directories, all of them hashstates-only (no data, no startedat); 1,228 are older than 30 days.
  • Upload purging is enabled (age: 168h, interval: 24h); its latest run logged Purge uploads finished. Num deleted=0, num errors=0.

This is probably what #4524 described on S3.

The fix marks the writer as cancelled and makes Close skip storeHashState for a cancelled upload. Close still calls the driver's FileWriter.Close so drivers that release resources there (for example the S3 buffer pool) keep doing so; its error is ignored because the writer has already been cancelled. That also removes the spurious error log.

The new test TestBlobUploadCloseAfterCancel writes a few bytes, cancels, closes the upload the way the handler does, and checks that the upload path no longer exists. With the in-memory driver it fails before the change and passes after it:

Testing done:

  • Test only, without the fix (Go 1.27 (golang:1.27 image), Linux amd64, on main @ 042c3a9):
    --- FAIL: TestBlobUploadCloseAfterCancel (0.00s)
        blob_test.go:83: unexpected error closing cancelled upload: already closed
    FAIL	github.com/distribution/distribution/v3/registry/storage
    
  • With the fix: --- PASS: TestBlobUploadCloseAfterCancel
  • Whole packages with the fix: go test ./registry/storage/ ./registry/handlers/ → ok (8.6s, 64.7s); go vet ./registry/storage/ clean
  • gofmt -l is clean for the changed files.

This change was drafted with AI assistance and reviewed by me.

🤖 Generated with Claude Code

…load

Cancel removes the upload directory, but the handler then closes the
writer, and Close stores the hash state again. That recreates the
upload directory without a startedat file, so the upload purger can
never remove it, and Close reports "already closed". Skip storing the
hash state once the upload has been cancelled.

Signed-off-by: YoungjuKim <[email protected]>

@milosgajdos milosgajdos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is great!

@milosgajdos
milosgajdos merged commit 952d81c into distribution:main Oct 18, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants