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
+25 -10
View File
@@ -5,9 +5,8 @@ package integration
import (
"bytes"
"fmt"
"net/http"
"path"
"strconv"
"strings"
"testing"
@@ -59,6 +58,7 @@ func TestAPILFSMediaType(t *testing.T) {
}
func createLFSTestRepository(t *testing.T, repoName string) *repo_model.Repository {
t.Helper()
ctx := NewAPITestContext(t, "user2", repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
t.Run("CreateRepo", doAPICreateRepository(ctx, false))
@@ -281,7 +281,7 @@ func TestAPILFSBatch(t *testing.T) {
assert.Equal(t, git_model.ErrLFSObjectNotExist, err)
// Cleanup
err = contentStore.Delete(p.RelativePath())
err = contentStore.ObjectStorage.Delete(p.RelativePath())
assert.NoError(t, err)
})
@@ -334,13 +334,11 @@ func TestAPILFSUpload(t *testing.T) {
defer test.MockVariableValue(&setting.LFS.StartServer, true)()
repo := createLFSTestRepository(t, "lfs-upload-repo")
oid := storeObjectInRepo(t, repo.ID, "dummy3")
defer git_model.RemoveLFSMetaObjectByOid(t.Context(), repo.ID, oid)
session := loginUser(t, "user2")
newRequest := func(t testing.TB, p lfs.Pointer, content string) *RequestWrapper {
return NewRequestWithBody(t, "PUT", path.Join("/user2/lfs-upload-repo.git/info/lfs/objects/", p.Oid, strconv.FormatInt(p.Size, 10)), strings.NewReader(content))
reqUrl := fmt.Sprintf("/user2/lfs-upload-repo.git/info/lfs/objects/%s/%d", p.Oid, p.Size)
return NewRequestWithBody(t, "PUT", reqUrl, strings.NewReader(content))
}
t.Run("InvalidPointer", func(t *testing.T) {
@@ -382,15 +380,14 @@ func TestAPILFSUpload(t *testing.T) {
})
// Cleanup
err = contentStore.Delete(p.RelativePath())
err = contentStore.ObjectStorage.Delete(p.RelativePath())
assert.NoError(t, err)
})
t.Run("MetaAlreadyExists", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
oid := storeObjectInRepo(t, repo.ID, "123456")
req := newRequest(t, lfs.Pointer{Oid: oid, Size: 6}, "")
session.MakeRequest(t, req, http.StatusOK)
})
@@ -410,6 +407,24 @@ func TestAPILFSUpload(t *testing.T) {
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
})
t.Run("ConcurrentFailureKeepsExistingMeta", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
pointer, err := lfs.GeneratePointer(strings.NewReader("any-content"))
assert.NoError(t, err)
// mock a record in database: it should not happen in real world (no existing file in the store)
// !!for testing purpose only!! to verify a failed upload should not remove a valid record,
_, err = git_model.NewLFSMetaObject(t.Context(), repo.ID, pointer)
assert.NoError(t, err)
// make an invalid request, the existing lfs record should not be removed
req := newRequest(t, lfs.Pointer{Oid: pointer.Oid, Size: 1}, "invalid content")
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
meta, err := git_model.GetLFSMetaObjectByOid(t.Context(), repo.ID, pointer.Oid)
assert.NoError(t, err)
assert.NotNil(t, meta)
})
t.Run("Success", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
+2 -6
View File
@@ -26,18 +26,14 @@ import (
)
func storeObjectInRepo(t *testing.T, repositoryID int64, content string) string {
t.Helper()
pointer, err := lfs.GeneratePointer(strings.NewReader(content))
assert.NoError(t, err)
_, err = git_model.NewLFSMetaObject(t.Context(), repositoryID, pointer)
assert.NoError(t, err)
contentStore := lfs.NewContentStore()
exist, err := contentStore.Exists(pointer)
err = lfs.NewContentStore().Put(pointer, strings.NewReader(content))
assert.NoError(t, err)
if !exist {
err := contentStore.Put(pointer, strings.NewReader(content))
assert.NoError(t, err)
}
return pointer.Oid
}