Skip to content

fix: clean up application images when an upload fails - #1057

Open
SulimanAbdulrazzaq wants to merge 1 commit into
gotify:masterfrom
SulimanAbdulrazzaq:fix/concurrent-app-image-upload
Open

SulimanAbdulrazzaq wants to merge 1 commit into
gotify:masterfrom
SulimanAbdulrazzaq:fix/concurrent-app-image-upload

Conversation

@SulimanAbdulrazzaq

@SulimanAbdulrazzaq SulimanAbdulrazzaq commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Refs #1047

As suggested in the review, this drops the mutex and fixes the file-system side of image uploads:

  • Partial files: ctx.SaveUploadedFile copies the upload with no cleanup, so a copy that fails partway leaves a partial file in the image directory. The upload now goes through saveImage/writeImage, which removes the file if it can't be written completely.
  • Database update failure: the old image used to be deleted before UpdateApplication. If the update failed, the application still referenced a deleted image, and the new file was left behind unreferenced. The old image is now deleted only after the update succeeds. If the update fails, the new file is removed and the old image is kept.

Two uploads to the same application overlapping in time still need an atomic update of the image field. As discussed, that's better done with a transaction or a dedicated DB method, so it's left out here.

Tests:

  • Test_UploadAppImage_UpdateFails_KeepsExistingImage: on master the image directory ends up holding only the new, unreferenced file (expected: "existing.png", actual: "<new name>.png"). With this change the application keeps existing.png and it's the only file left.
  • Test_WriteImage_RemovesPartialFileOnError: a reader that fails after some data leaves no file behind.

make test-coverage (go test --race ./...) passes, and golangci-lint reports 0 issues.

@SulimanAbdulrazzaq
SulimanAbdulrazzaq requested a review from a team as a code owner September 26, 2026 09:32
@codecov

codecov Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.20%. Comparing base (d02796b) to head (efb90f3).

Files with missing lines Patch % Lines
api/application.go 83.33% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1057      +/-   ##
==========================================
+ Coverage   76.09%   76.20%   +0.10%     
==========================================
  Files          67       67              
  Lines        3619     3639      +20     
==========================================
+ Hits         2754     2773      +19     
  Misses        654      654              
- Partials      211      212       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@eternal-flame-AD eternal-flame-AD 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.

the core for this issue isn't the magic string in the database having race condition (it does not, it guarantees to store the most recent value). The problem is the file system has race conditions

Comment thread api/application.go Outdated

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.

This is implemented as an io.Copy() without a cleanup, so incomplete uploads and oversized uploads will still lead to a file being left over

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in efb90f3. The upload now goes through saveImage/writeImage, which removes the file if it can't be written completely. The old image is also only deleted after UpdateApplication succeeds; if the update fails, the new file is removed instead.

Comment thread api/application.go Outdated

// Read the application again, a concurrent request may have
// changed or deleted its image in the meantime.
app, err = a.DB.GetApplicationByID(id)

@eternal-flame-AD eternal-flame-AD Sep 26, 2026 •

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.

This is not atomic either (app could be updated in the mean time), it should be implemented as a DB transaction (see #1047) , a CAS primitive implemented in the database abstraction (BEGIN .. ;SELECT ... FOR UPDATE ... ; UPDATE ... ; COMMIT) , or a special method that only update this field.

I would leave this out as it doesn't resolve the core issue, software mutex around relational database actions is an anti-pattern that can lead to deadlocks, and can easily be incorporated after #1047 as transactions if necessary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I removed the mutex and the re-read. The overlapping-upload part can be done with a transaction or a dedicated DB method later, as you suggested.

A failed write left a partially written image file behind, and a failed
database update after saving the new image had already deleted the image
the application still referenced.

Write the uploaded image with a helper that removes the file if it can't
be written completely, and only delete the old image once the
application has been updated. If the update fails, remove the new image
instead.
@SulimanAbdulrazzaq
SulimanAbdulrazzaq force-pushed the fix/concurrent-app-image-upload branch from e2e2bc4 to efb90f3 Compare September 26, 2026 16:57
@SulimanAbdulrazzaq SulimanAbdulrazzaq changed the title fix: serialize application image changes fix: clean up application images when an upload fails Sep 26, 2026
@SulimanAbdulrazzaq

Copy link
Copy Markdown
Contributor Author

@eternal-flame-AD thanks for the review. I reworked this along the lines you suggested: no mutex, and cleanup of the files on failed uploads and failed updates. Details are in the updated description. Could you take another look?

@SulimanAbdulrazzaq

Copy link
Copy Markdown
Contributor Author

@eternal-flame-AD @jmattheis when you have a moment, could one of you take a look at this one? The checks are green on the current head.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants