@@ -60,13 +60,13 @@ func TestPublish(t *testing.T) {
6060 logger := logptest .NewTestingLogger (t , "" )
6161 logConsumer , err := consumer .NewLogs (consumeFn )
6262 assert .NoError (t , err )
63- consumer := & otelConsumer {
63+ return & otelConsumer {
6464 observer : outputs .NewNilObserver (),
6565 logsConsumer : logConsumer ,
6666 beatInfo : beatInfo ,
6767 log : logger .Named ("otelconsumer" ),
68+ retry : retryConfig {init : 1 * time .Millisecond , max : 2 * time .Millisecond },
6869 }
69- return consumer
7070 }
7171
7272 t .Run ("ack batch on consumer success" , func (t * testing.T ) {
@@ -266,6 +266,103 @@ func TestPublish(t *testing.T) {
266266 assert .Equal (t , outest .BatchRetry , batch .Signals [0 ].Tag )
267267 })
268268
269+ t .Run ("retries are delayed by exponential backoff" , func (t * testing.T ) {
270+ const (
271+ initBackoff = 50 * time .Millisecond
272+ maxBackoff = 500 * time .Millisecond
273+ )
274+
275+ otelConsumer := makeOtelConsumer (t , func (ctx context.Context , ld plog.Logs ) error {
276+ return errors .New ("retryable error" )
277+ })
278+ otelConsumer .retry = retryConfig {init : initBackoff , max : maxBackoff }
279+
280+ // Measure the duration of each Publish call. Each call blocks during
281+ // backoff Wait(), so elapsed time reflects the actual backoff delay.
282+ var durations []time.Duration
283+ for range 3 {
284+ batch := outest .NewBatch (event1 )
285+ start := time .Now ()
286+ err := otelConsumer .Publish (ctx , batch )
287+ durations = append (durations , time .Since (start ))
288+ require .NoError (t , err , "Publish should not return an error" )
289+ assert .Equal (t , outest .BatchRetry , batch .Signals [0 ].Tag , "batch should be retried" )
290+ }
291+
292+ assert .GreaterOrEqual (t , durations [0 ], initBackoff , "first retry delay should be at least ~init" )
293+ assert .GreaterOrEqual (t , durations [1 ], 2 * initBackoff , "second retry delay should be at least ~2*init (exponential growth)" )
294+ assert .GreaterOrEqual (t , durations [2 ], 4 * initBackoff , "third retry delay should be at least ~4*init (exponential growth)" )
295+ })
296+
297+ t .Run ("backoff resets on success" , func (t * testing.T ) {
298+ const (
299+ initBackoff = 50 * time .Millisecond
300+ maxBackoff = 500 * time .Millisecond
301+ )
302+
303+ callCount := 0
304+ otelConsumer := makeOtelConsumer (t , func (ctx context.Context , ld plog.Logs ) error {
305+ callCount ++
306+ if callCount == 3 {
307+ return nil
308+ }
309+ return errors .New ("retryable error" )
310+ })
311+ otelConsumer .retry = retryConfig {init : initBackoff , max : maxBackoff }
312+
313+ // Two failures grow the backoff past init level.
314+ batch1 := outest .NewBatch (event1 )
315+ err := otelConsumer .Publish (ctx , batch1 )
316+ require .NoError (t , err )
317+ assert .Equal (t , outest .BatchRetry , batch1 .Signals [0 ].Tag , "first batch should be retried" )
318+
319+ batch2 := outest .NewBatch (event1 )
320+ err = otelConsumer .Publish (ctx , batch2 )
321+ require .NoError (t , err )
322+ assert .Equal (t , outest .BatchRetry , batch2 .Signals [0 ].Tag , "second batch should be retried" )
323+
324+ // Third call succeeds, triggering backoff Reset().
325+ batch3 := outest .NewBatch (event1 )
326+ err = otelConsumer .Publish (ctx , batch3 )
327+ require .NoError (t , err )
328+ assert .Equal (t , outest .BatchACK , batch3 .Signals [0 ].Tag , "third batch should be acked" )
329+
330+ // Next failure should use init-level backoff ([init, 2*init) = [50ms, 100ms)),
331+ // not the grown level which would be [4*init, 8*init) = [200ms, 400ms).
332+ batch4 := outest .NewBatch (event1 )
333+ start := time .Now ()
334+ err = otelConsumer .Publish (ctx , batch4 )
335+ duration := time .Since (start )
336+ require .NoError (t , err )
337+ assert .Equal (t , outest .BatchRetry , batch4 .Signals [0 ].Tag , "fourth batch should be retried" )
338+ // In equal jitter backoff strategy, initial backoff is between initBackoff and 2*initBackoff.
339+ const margin = 10 * time .Millisecond
340+ assert .Less (t , duration , 2 * initBackoff + margin , "after success, backoff should reset to init level, not continue growing (got %v)" , duration )
341+ })
342+
343+ t .Run ("cancels batch when context is cancelled during backoff" , func (t * testing.T ) {
344+ batch := outest .NewBatch (event1 )
345+
346+ cancelCtx , cancelFn := context .WithCancel (context .Background ())
347+ otelConsumer := makeOtelConsumer (t , func (ctx context.Context , ld plog.Logs ) error {
348+ return errors .New ("retryable error" )
349+ })
350+ otelConsumer .retry = retryConfig {init : 10 * time .Second , max : 10 * time .Second }
351+
352+ publishDone := make (chan struct {})
353+ go func () {
354+ _ = otelConsumer .Publish (cancelCtx , batch )
355+ close (publishDone )
356+ }()
357+
358+ time .Sleep (50 * time .Millisecond )
359+ cancelFn ()
360+ <- publishDone
361+
362+ assert .Len (t , batch .Signals , 1 )
363+ assert .Equal (t , outest .BatchCancelled , batch .Signals [0 ].Tag )
364+ })
365+
269366 t .Run ("sets the elasticsearchexporter doc id attribute from metadata" , func (t * testing.T ) {
270367 batch := outest .NewBatch (event4 )
271368
0 commit comments