diff --git a/core/artwork/worker.go b/core/artwork/worker.go index a66734509..9365cf50c 100644 --- a/core/artwork/worker.go +++ b/core/artwork/worker.go @@ -267,8 +267,9 @@ func (b *breaker) allow() bool { func (b *breaker) record(err error) { b.mu.Lock() defer b.mu.Unlock() - // A not-found is a definitive answer, not a fault; only real errors trip the breaker. - if err == nil || errors.Is(err, model.ErrNotFound) { + // A not-found (from either package) is a definitive answer, not a fault; only real + // errors trip the breaker. Must stay consistent with isTransientExternal. + if err == nil || errors.Is(err, model.ErrNotFound) || errors.Is(err, agents.ErrNotFound) { b.failures = 0 return } diff --git a/core/artwork/worker_test.go b/core/artwork/worker_test.go index 7a072aa1e..cf9b7cddf 100644 --- a/core/artwork/worker_test.go +++ b/core/artwork/worker_test.go @@ -283,6 +283,25 @@ var _ = Describe("Worker", func() { Expect(calls).To(Equal(5), "the breaker should have re-closed after the success") }) + It("does not open the breaker on a run of agent not-found misses", func() { + // Regression: agents.ErrNotFound is a definitive miss, not a fault. A run of + // artless items must never trip the breaker, or they'd loop in retry instead of + // settling absent. Uses the real gate, not passthroughGate. + notFound := func() (io.ReadCloser, string, error) { return nil, "", agents.ErrNotFound } + for range breakerThreshold + 3 { + _, _, err := w.gate("A", notFound) + Expect(err).To(MatchError(agents.ErrNotFound), "a miss passes through, never errBreakerOpen") + } + + var calls int + counting := func() (io.ReadCloser, string, error) { + calls++ + return nil, "", errors.New("boom") + } + _, _, _ = w.gate("A", counting) + Expect(calls).To(Equal(1), "the breaker stayed closed, so the step still runs") + }) + It("isolates each agent's breaker: one open gate does not block another", func() { failing := func() (io.ReadCloser, string, error) { return nil, "", errors.New("boom") } for range breakerThreshold {