From 8a2cabb99859ce7de700f518e234cf715144aa9c Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 20 May 2026 14:58:54 +0200 Subject: [PATCH] refactor: redundancy getter exhaust --- pkg/replicas/getter.go | 20 +++++++++++++++++--- pkg/replicas/getter_test.go | 6 ++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/pkg/replicas/getter.go b/pkg/replicas/getter.go index b08b5c780e8..7f0c8aa4159 100644 --- a/pkg/replicas/getter.go +++ b/pkg/replicas/getter.go @@ -24,7 +24,13 @@ import ( // then the probability of Swarmageddon is less than 0.000001 // assuming the error rate of chunk retrievals stays below the level expressed // as depth by the publisher. -var ErrSwarmageddon = errors.New("swarmageddon has begun") +var ( + ErrSwarmageddon = errors.New("swarmageddon has begun") + // errGetterExhausted is returned when the retry loop exhausts all levels without + // receiving a result or enough errors to trigger ErrSwarmageddon. + // This path should never be reached under normal operation. + errGetterExhausted = errors.New("replicas getter: exhausted all levels without result (unexpected)") +) // getter is the private implementation of storage.Getter, an interface for // retrieving chunks. This getter embeds the original simple chunk getter and extends it @@ -48,6 +54,9 @@ func NewGetter(g storage.Getter, level redundancy.Level) storage.Getter { // Get makes the getter satisfy the storage.Getter interface func (g *getter) Get(ctx context.Context, addr swarm.Address) (ch swarm.Chunk, err error) { + if g.level == redundancy.NONE { + return g.Getter.Get(ctx, addr) + } ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -77,7 +86,11 @@ func (g *getter) Get(ctx context.Context, addr swarm.Address) (ch swarm.Chunk, e target := 2 // the number of replicas attempted to download in this batch total := g.level.GetReplicaCount() - // + // The replicator feeds replica addresses in batches that double each RetryInterval + // (2, 2, 4, 8, 16 for PARANOID). The replicator exhausts exactly at the last iteration, + // which then drains a nil from `next` and becomes an indefinite wait on resultC/errc. + // The loop therefore always terminates via resultC (success) or Swarmageddon (all fail), + // never by falling through to the return below. rr := newReplicator(addr, g.level) next := rr.c var wait <-chan time.Time // nil channel to disable case @@ -142,5 +155,6 @@ func (g *getter) Get(ctx context.Context, addr swarm.Address) (ch swarm.Chunk, e } } - return nil, nil + // unreachable: the loop always exits via resultC or Swarmageddon (see comment above) + return nil, errGetterExhausted } diff --git a/pkg/replicas/getter_test.go b/pkg/replicas/getter_test.go index d1d727dd5fd..33b586fc189 100644 --- a/pkg/replicas/getter_test.go +++ b/pkg/replicas/getter_test.go @@ -194,8 +194,10 @@ func TestGetter(t *testing.T) { } t.Run("returns correct error", func(t *testing.T) { - if !errors.Is(err, replicas.ErrSwarmageddon) { - t.Fatalf("incorrect error. want Swarmageddon. got %v", err) + if tc.level > 0 { + if !errors.Is(err, replicas.ErrSwarmageddon) { + t.Fatalf("incorrect error. want Swarmageddon. got %v", err) + } } if !errors.Is(err, tc.failure.err) { t.Fatalf("incorrect error. want it to wrap %v. got %v", tc.failure.err, err)