fix(lfs): failed upload deletes a concurrent upload's meta object (#38693)

UploadHandler creates the LFS meta object only as the last step of
uploadOrVerify, after the content is already in the store, so a request
that errors has never created a row of its own. The removal on the error
path was a real compensating action when it was added in #14726, where
the meta object was created before contentStore.Put, but #16865 moved
creation after the Put and left the removal behind.

Since then it can only ever delete a row created by a different request:
a stalled git-lfs PUT that fails after its own retry has already
succeeded wipes the winner's meta object, leaving the content in the
store unreachable and eligible for orphan cleanup.

Drop the removal and add a regression test asserting that a failing
upload keeps a pre-existing meta object.

Fixes: #38424
Assisted-by: Claude Code:claude-opus-5
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
hsdfat
2026-07-31 11:05:41 +00:00
committed by GitHub
co-authored by wxiaoguang
parent bcf45803af
commit 7ca64566f5
5 changed files with 66 additions and 46 deletions
+14 -12
View File
@@ -24,7 +24,7 @@ var (
// ContentStore provides a simple file system based storage.
type ContentStore struct {
storage.ObjectStorage
ObjectStorage storage.ObjectStorage
}
// NewContentStore creates the default ContentStore
@@ -36,7 +36,7 @@ func NewContentStore() *ContentStore {
// Get takes a Meta object and retrieves the content from the store, returning
// it as an io.ReadSeekCloser.
func (s *ContentStore) Get(pointer Pointer) (storage.Object, error) {
f, err := s.Open(pointer.RelativePath())
f, err := s.ObjectStorage.Open(pointer.RelativePath())
if err != nil {
log.Error("Whilst trying to read LFS OID[%s]: Unable to open Error: %v", pointer.Oid, err)
return nil, err
@@ -53,7 +53,7 @@ func (s *ContentStore) Put(pointer Pointer, r io.Reader) error {
// now pass the wrapped reader to Save - if there is a size mismatch or hash mismatch then
// the errors returned by the newHashingReader should percolate up to here
written, err := s.Save(p, wrappedRd, pointer.Size)
written, err := s.ObjectStorage.Save(p, wrappedRd, pointer.Size)
if err != nil {
log.Error("Whilst putting LFS OID[%s]: Failed to copy to tmpPath: %s Error: %v", pointer.Oid, p, err)
return err
@@ -69,7 +69,7 @@ func (s *ContentStore) Put(pointer Pointer, r io.Reader) error {
// if the upload failed, try to delete the file
if err != nil {
if errDel := s.Delete(p); errDel != nil {
if errDel := s.ObjectStorage.Delete(p); errDel != nil {
log.Error("Cleaning the LFS OID[%s] failed: %v", pointer.Oid, errDel)
}
}
@@ -77,16 +77,18 @@ func (s *ContentStore) Put(pointer Pointer, r io.Reader) error {
return err
}
// Exists returns true if the object exists in the content store.
func (s *ContentStore) Stat(pointer Pointer) (os.FileInfo, error) {
return s.ObjectStorage.Stat(pointer.RelativePath())
}
func (s *ContentStore) Exists(pointer Pointer) (bool, error) {
_, err := s.ObjectStorage.Stat(pointer.RelativePath())
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
_, err := s.Stat(pointer)
if os.IsNotExist(err) {
return false, nil
} else if err == nil {
return true, nil
}
return true, nil
return false, err
}
// Verify returns true if the object exists in the content store and size is correct.