Ask a failed part fetch once more before a scan fails, and name the stored object (issue 1536)

This commit is contained in:
2026-10-21 01:37:26 +02:00
parent 8295bc3894
commit 7bf63f9eff
2 changed files with 65 additions and 0 deletions
+4
View File
@@ -49,6 +49,10 @@ func (s *Service) ScanNow(ctx context.Context, source string, duplex bool) (map[
got := make([][]byte, 0, int(parts))
for i := 1; i <= int(parts); i++ {
p, err := s.fetchPart(ctx, key, i)
if err != nil && ctx.Err() == nil {
// A part fetch is a read, safe to repeat: one retry rides out a short bus outage (issue 1536).
p, err = s.fetchPart(ctx, key, i)
}
if err != nil {
return nil, fmt.Errorf("the scan is kept as %s, but its part %d could not be fetched: %w", key, i, err)
}
@@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@@ -334,3 +335,63 @@ func TestLoadSettingsLaysTheDefaultsUnder(t *testing.T) {
t.Fatalf("settings = %+v", s)
}
}
// flakyBus fails the first n document asks with an error answer, as a bus outage would end one wait, and passes
// everything else on.
type flakyBus struct {
*fakeAskBus
mu sync.Mutex
fails int
}
func (b *flakyBus) Publish(id string, body any) error {
raw, _ := json.Marshal(body)
var ask Ask
json.Unmarshal(raw, &ask)
b.mu.Lock()
fail := ask.Op == OpDocument && b.fails > 0
if fail {
b.fails--
}
b.mu.Unlock()
if fail {
b.answers.Create(id, map[string]any{"error": "nats: timeout"})
return nil
}
return b.fakeAskBus.Publish(id, body)
}
// 1536: a part fetch that fails once is asked again, and the scan is filed; one that fails twice is a failure
// naming the stored object, so it can be filed later.
func TestAFailedPartFetchIsAskedOnceMoreBeforeTheScanFails(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, _ := scanRig(t, f, Settings{Crop: false})
r.bus = nil
flaky := &flakyBus{fakeAskBus: &fakeAskBus{worker: r.worker, answers: r.answers}, fails: 1}
askBus = flaky
body, err := r.scan(t)
if err != nil {
t.Fatalf("one failed part fetch ended the scan: %v", err)
}
if _, err := os.Stat(body["path"].(string)); err != nil {
t.Errorf("the scan is not filed: %v", err)
}
f2 := newFakeScanner(t, fixture(t, "scan.jpg"))
r2, folder := scanRig(t, f2, Settings{Crop: false})
askBus = &flakyBus{fakeAskBus: &fakeAskBus{worker: r2.worker, answers: r2.answers}, fails: 2}
_, err = r2.scan(t)
if err == nil {
t.Fatal("two failed part fetches did not fail the scan")
}
key := ""
if got := *r2.said; len(got) == 1 {
key, _ = got[0].body["object"].(string)
}
if key == "" || !strings.Contains(err.Error(), key) {
t.Errorf("the failure %q does not name the stored object %q", err, key)
}
if entries, _ := os.ReadDir(folder); len(entries) != 0 {
t.Errorf("a scan whose parts failed was filed: %v", entries)
}
}