Skip to content

Commit dc523ae

Browse files
nicklaslclaude
andauthored
test(providers): add retry exhaustion, shutdown, and transport tests (#468)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5178f0b commit dc523ae

4 files changed

Lines changed: 614 additions & 0 deletions

File tree

openfeature-provider/go/confidence/internal/flag_logger/grpc_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,34 @@ func TestGrpcWasmFlagLogger_FailureStats_NoLogBeforeWindow(t *testing.T) {
241241
}
242242
}
243243

244+
func TestGrpcWasmFlagLogger_FailureStats_AllFail(t *testing.T) {
245+
var buf bytes.Buffer
246+
testLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn}))
247+
248+
mockStub := &mockInternalFlagLoggerServiceClient{
249+
writeFlagLogsFunc: func(ctx context.Context, req *resolverv1.WriteFlagLogsRequest) (*resolverv1.WriteFlagLogsResponse, error) {
250+
return nil, errors.New("unavailable")
251+
},
252+
}
253+
254+
logger := NewGrpcWasmFlagLogger(mockStub, "test-secret", testLogger)
255+
256+
for i := 0; i < 10; i++ {
257+
logger.Write(&resolverv1.WriteFlagLogsRequest{
258+
FlagAssigned: make([]*resolverv1.FlagAssigned, 1),
259+
})
260+
}
261+
logger.Shutdown()
262+
263+
output := buf.String()
264+
if !strings.Contains(output, "Flag log write failures") {
265+
t.Error("Expected failure log after window with all failures")
266+
}
267+
if !strings.Contains(output, "failures=10") {
268+
t.Errorf("Expected failures=10 in output, got: %s", output)
269+
}
270+
}
271+
244272
func TestNoOpWasmFlagLogger(t *testing.T) {
245273
logger := NewNoOpWasmFlagLogger()
246274

openfeature-provider/go/confidence/retry_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"sync/atomic"
99
"testing"
10+
"time"
1011

1112
"github.com/spotify/confidence-resolver/openfeature-provider/go/confidence"
1213
fl "github.com/spotify/confidence-resolver/openfeature-provider/go/confidence/internal/flag_logger"
@@ -91,6 +92,168 @@ func TestNoRetryOnPermissionDenied(t *testing.T) {
9192
}
9293
}
9394

95+
func TestAllRetriesExhausted(t *testing.T) {
96+
var callCount atomic.Int32
97+
srv := grpc.NewServer()
98+
resolverv1.RegisterInternalFlagLoggerServiceServer(srv, &retryTestService{
99+
callCount: &callCount,
100+
failUntil: 100,
101+
})
102+
103+
lis := bufconn.Listen(1024 * 1024)
104+
go srv.Serve(lis)
105+
t.Cleanup(srv.Stop)
106+
107+
conn, err := grpc.NewClient("passthrough:///bufconn",
108+
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
109+
return lis.DialContext(ctx)
110+
}),
111+
grpc.WithTransportCredentials(insecure.NewCredentials()),
112+
grpc.WithDefaultServiceConfig(confidence.RetryServiceConfig),
113+
)
114+
if err != nil {
115+
t.Fatalf("Failed to dial: %v", err)
116+
}
117+
t.Cleanup(func() { conn.Close() })
118+
119+
stub := resolverv1.NewInternalFlagLoggerServiceClient(conn)
120+
logger := fl.NewGrpcWasmFlagLogger(stub, "test-secret", slog.New(slog.NewTextHandler(os.Stderr, nil)))
121+
122+
logger.Write(&resolverv1.WriteFlagLogsRequest{
123+
FlagAssigned: []*resolverv1.FlagAssigned{{ResolveId: "r1"}},
124+
})
125+
logger.Shutdown()
126+
127+
if got := callCount.Load(); got != 3 {
128+
t.Errorf("Expected 3 calls (max retries exhausted), got %d", got)
129+
}
130+
}
131+
132+
func TestShutdownDuringRetries(t *testing.T) {
133+
var callCount atomic.Int32
134+
srv := grpc.NewServer()
135+
resolverv1.RegisterInternalFlagLoggerServiceServer(srv, &retryTestService{
136+
callCount: &callCount,
137+
failUntil: 100,
138+
})
139+
140+
lis := bufconn.Listen(1024 * 1024)
141+
go srv.Serve(lis)
142+
t.Cleanup(srv.Stop)
143+
144+
conn, err := grpc.NewClient("passthrough:///bufconn",
145+
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
146+
return lis.DialContext(ctx)
147+
}),
148+
grpc.WithTransportCredentials(insecure.NewCredentials()),
149+
grpc.WithDefaultServiceConfig(confidence.RetryServiceConfig),
150+
)
151+
if err != nil {
152+
t.Fatalf("Failed to dial: %v", err)
153+
}
154+
t.Cleanup(func() { conn.Close() })
155+
156+
stub := resolverv1.NewInternalFlagLoggerServiceClient(conn)
157+
logger := fl.NewGrpcWasmFlagLogger(stub, "test-secret", slog.New(slog.NewTextHandler(os.Stderr, nil)))
158+
159+
logger.Write(&resolverv1.WriteFlagLogsRequest{
160+
FlagAssigned: []*resolverv1.FlagAssigned{{ResolveId: "r1"}},
161+
})
162+
163+
done := make(chan struct{})
164+
go func() {
165+
logger.Shutdown()
166+
close(done)
167+
}()
168+
169+
select {
170+
case <-done:
171+
case <-time.After(30 * time.Second):
172+
t.Fatal("Shutdown did not complete within 30 seconds")
173+
}
174+
}
175+
176+
func TestTransportHooksPreserveRetry(t *testing.T) {
177+
var callCount atomic.Int32
178+
srv := grpc.NewServer()
179+
resolverv1.RegisterInternalFlagLoggerServiceServer(srv, &retryTestService{
180+
callCount: &callCount,
181+
failUntil: 1,
182+
})
183+
184+
lis := bufconn.Listen(1024 * 1024)
185+
go srv.Serve(lis)
186+
t.Cleanup(srv.Stop)
187+
188+
baseOpts := []grpc.DialOption{
189+
grpc.WithDefaultServiceConfig(confidence.RetryServiceConfig),
190+
}
191+
opts := append([]grpc.DialOption{}, baseOpts...)
192+
opts = append(opts,
193+
grpc.WithTransportCredentials(insecure.NewCredentials()),
194+
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
195+
return lis.DialContext(ctx)
196+
}),
197+
)
198+
199+
conn, err := grpc.NewClient("passthrough:///bufconn", opts...)
200+
if err != nil {
201+
t.Fatalf("Failed to dial: %v", err)
202+
}
203+
t.Cleanup(func() { conn.Close() })
204+
205+
stub := resolverv1.NewInternalFlagLoggerServiceClient(conn)
206+
logger := fl.NewGrpcWasmFlagLogger(stub, "test-secret", slog.New(slog.NewTextHandler(os.Stderr, nil)))
207+
208+
logger.Write(&resolverv1.WriteFlagLogsRequest{
209+
FlagAssigned: []*resolverv1.FlagAssigned{{ResolveId: "r1"}},
210+
})
211+
logger.Shutdown()
212+
213+
if got := callCount.Load(); got != 2 {
214+
t.Errorf("Expected 2 calls (retry should work with custom hooks), got %d", got)
215+
}
216+
}
217+
218+
func TestMultipleWritesWithRetryConfig(t *testing.T) {
219+
var callCount atomic.Int32
220+
srv := grpc.NewServer()
221+
resolverv1.RegisterInternalFlagLoggerServiceServer(srv, &retryTestService{
222+
callCount: &callCount,
223+
failUntil: 0,
224+
})
225+
226+
lis := bufconn.Listen(1024 * 1024)
227+
go srv.Serve(lis)
228+
t.Cleanup(srv.Stop)
229+
230+
conn, err := grpc.NewClient("passthrough:///bufconn",
231+
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
232+
return lis.DialContext(ctx)
233+
}),
234+
grpc.WithTransportCredentials(insecure.NewCredentials()),
235+
grpc.WithDefaultServiceConfig(confidence.RetryServiceConfig),
236+
)
237+
if err != nil {
238+
t.Fatalf("Failed to dial: %v", err)
239+
}
240+
t.Cleanup(func() { conn.Close() })
241+
242+
stub := resolverv1.NewInternalFlagLoggerServiceClient(conn)
243+
logger := fl.NewGrpcWasmFlagLogger(stub, "test-secret", slog.New(slog.NewTextHandler(os.Stderr, nil)))
244+
245+
for i := 0; i < 5; i++ {
246+
logger.Write(&resolverv1.WriteFlagLogsRequest{
247+
FlagAssigned: []*resolverv1.FlagAssigned{{ResolveId: "r1"}},
248+
})
249+
}
250+
logger.Shutdown()
251+
252+
if got := callCount.Load(); got != 5 {
253+
t.Errorf("Expected 5 calls (all successful, no retries), got %d", got)
254+
}
255+
}
256+
94257
type retryTestService struct {
95258
resolverv1.UnimplementedInternalFlagLoggerServiceServer
96259
callCount *atomic.Int32

0 commit comments

Comments
 (0)