fix(submit): 上传途中投稿被撤回或删除时,由上传自己删掉刚落地的 blob

This commit is contained in:
Lemon-miaow committed 2026-09-27 13:43:41 +08:00
1 parent 372c8972f9
commit c54bd2d9eb
4 files changed
+69 -4

No files matched your search

+11 -2
View File
@@ -656,8 +656,17 @@ func (m *Manager) UploadContext(ctx context.Context, id, submittedBy string, r i
return nil, err
}
if !won {
// Reviewed while the bytes streamed in. The blob was replaced anyway, but
// the approved digest no longer matches it, so its build refuses them.
// The row stopped being pending while the bytes streamed in.
if _, err := m.Store.GetSubmission(ctx, id); errors.Is(err, ErrNotFound) {
// Withdrawn or deleted: its reap ran before this blob landed, and no
// row will ever count or delete it, so the upload deletes it itself.
if err := m.deleteBlob(ctx, id); err != nil {
return nil, err
}
return nil, ErrNotFound
}
// Reviewed. The blob was replaced anyway, but the approved digest no
// longer matches it, so its build refuses them.
return nil, ErrAlreadyReviewed
}
sub.ContextSHA256 = digest
+54
View File
@@ -1246,6 +1246,60 @@ func (a approvingBlobs) Put(ctx context.Context, id string, r io.Reader) (int64,
return n, err
}
// A withdraw or delete that lands while an upload's bytes stream in reaps the
// submission before the blob exists. The upload finds its row gone and deletes
// the blob itself: nothing else would, and no row would ever count it.
func TestUploadRacingRemovalLeavesNoBlob(t *testing.T) {
for _, tc := range []struct {
name string
remove func(m *Manager, id string) error
}{
{"withdraw", func(m *Manager, id string) error {
_, err := m.Withdraw(context.Background(), id, "user-1")
return err
}},
{"delete", func(m *Manager, id string) error { _, err := m.Delete(context.Background(), id); return err }},
} {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
m, st, _ := newManager()
fb := newFakeBlobs()
m.Blobs = fb
seed, _ := m.Create(ctx, CreateRequest{DisplayName: "Pack", SubmittedBy: "user-1"})
if _, err := m.UploadContext(ctx, seed.ID, "user-1", strings.NewReader(gzBody("first"))); err != nil {
t.Fatalf("first upload: %v", err)
}
m.Blobs = racingBlobs{fakeBlobs: fb, race: func() {
if err := tc.remove(m, seed.ID); err != nil {
t.Fatalf("%s: %v", tc.name, err)
}
}}
if _, err := m.UploadContext(ctx, seed.ID, "user-1", strings.NewReader(gzBody("second"))); !errors.Is(err, ErrNotFound) {
t.Fatalf("racing upload = %v, want ErrNotFound", err)
}
if _, ok := st.subs[seed.ID]; ok {
t.Fatal("the row must stay gone")
}
if len(fb.stored) != 0 {
t.Fatalf("stored blobs = %v, want none", keysOf(fb.stored))
}
})
}
}
// racingBlobs runs race before Put stores the bytes, the window a withdraw or
// delete lands in while an upload streams.
type racingBlobs struct {
*fakeBlobs
race func()
}
func (r racingBlobs) Put(ctx context.Context, id string, rd io.Reader) (int64, error) {
r.race()
return r.fakeBlobs.Put(ctx, id, rd)
}
func sha256Hex(s string) string {
sum := sha256.Sum256([]byte(s))
return hex.EncodeToString(sum[:])