feat: retry gateway kafka publishes

This commit is contained in:
lingniu
2026-07-01 22:54:47 +08:00
parent e7c8c57296
commit 7e7066dd86
4 changed files with 178 additions and 2 deletions

View File

@@ -0,0 +1,80 @@
package eventbus
import (
"context"
"time"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
)
type RetryConfig struct {
Attempts int
Backoff time.Duration
AttemptTimeout time.Duration
}
type RetryingSink struct {
delegate Sink
cfg RetryConfig
}
func NewRetryingSink(delegate Sink, cfg RetryConfig) *RetryingSink {
if delegate == nil {
panic("retry delegate sink must not be nil")
}
if cfg.Attempts <= 0 {
cfg.Attempts = 1
}
return &RetryingSink{delegate: delegate, cfg: cfg}
}
func (s *RetryingSink) PublishRaw(ctx context.Context, env envelope.FrameEnvelope) error {
return s.withRetry(ctx, func(attemptCtx context.Context) error {
return s.delegate.PublishRaw(attemptCtx, env)
})
}
func (s *RetryingSink) PublishUnified(ctx context.Context, env envelope.FrameEnvelope) error {
return s.withRetry(ctx, func(attemptCtx context.Context) error {
return s.delegate.PublishUnified(attemptCtx, env)
})
}
func (s *RetryingSink) Close() error {
return s.delegate.Close()
}
func (s *RetryingSink) withRetry(ctx context.Context, publish func(context.Context) error) error {
var lastErr error
for attempt := 1; attempt <= s.cfg.Attempts; attempt++ {
attemptCtx := ctx
cancel := func() {}
if s.cfg.AttemptTimeout > 0 {
attemptCtx, cancel = context.WithTimeout(ctx, s.cfg.AttemptTimeout)
}
err := publish(attemptCtx)
cancel()
if err == nil {
return nil
}
lastErr = err
if ctx.Err() != nil {
return ctx.Err()
}
if attempt == s.cfg.Attempts {
break
}
if s.cfg.Backoff > 0 {
timer := time.NewTimer(s.cfg.Backoff)
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return ctx.Err()
case <-timer.C:
}
}
}
return lastErr
}

View File

@@ -0,0 +1,80 @@
package eventbus
import (
"context"
"errors"
"testing"
"time"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
)
func TestRetryingSinkRetriesRawUntilSuccess(t *testing.T) {
delegate := &flakySink{rawFailures: 1}
sink := NewRetryingSink(delegate, RetryConfig{Attempts: 3})
err := sink.PublishRaw(context.Background(), envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808})
if err != nil {
t.Fatalf("PublishRaw() error = %v", err)
}
if delegate.rawCalls != 2 {
t.Fatalf("raw calls = %d, want 2", delegate.rawCalls)
}
}
func TestRetryingSinkReturnsLastErrorAfterAttempts(t *testing.T) {
delegate := &flakySink{rawFailures: 3}
sink := NewRetryingSink(delegate, RetryConfig{Attempts: 2})
err := sink.PublishRaw(context.Background(), envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808})
if !errors.Is(err, errFlakyPublish) {
t.Fatalf("PublishRaw() error = %v, want %v", err, errFlakyPublish)
}
if delegate.rawCalls != 2 {
t.Fatalf("raw calls = %d, want 2", delegate.rawCalls)
}
}
func TestRetryingSinkStopsWhenContextCanceledDuringBackoff(t *testing.T) {
delegate := &flakySink{rawFailures: 10}
sink := NewRetryingSink(delegate, RetryConfig{Attempts: 5, Backoff: time.Minute})
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := sink.PublishRaw(ctx, envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808})
if !errors.Is(err, context.Canceled) {
t.Fatalf("PublishRaw() error = %v, want context canceled", err)
}
if delegate.rawCalls != 1 {
t.Fatalf("raw calls = %d, want 1", delegate.rawCalls)
}
}
var errFlakyPublish = errors.New("publish failed")
type flakySink struct {
rawFailures int
unifiedFailures int
rawCalls int
unifiedCalls int
}
func (s *flakySink) PublishRaw(context.Context, envelope.FrameEnvelope) error {
s.rawCalls++
if s.rawCalls <= s.rawFailures {
return errFlakyPublish
}
return nil
}
func (s *flakySink) PublishUnified(context.Context, envelope.FrameEnvelope) error {
s.unifiedCalls++
if s.unifiedCalls <= s.unifiedFailures {
return errFlakyPublish
}
return nil
}
func (s *flakySink) Close() error {
return nil
}