Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,34 @@ func TestGrpcWasmFlagLogger_FailureStats_NoLogBeforeWindow(t *testing.T) {
}
}

func TestGrpcWasmFlagLogger_FailureStats_AllFail(t *testing.T) {
var buf bytes.Buffer
testLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn}))

mockStub := &mockInternalFlagLoggerServiceClient{
writeFlagLogsFunc: func(ctx context.Context, req *resolverv1.WriteFlagLogsRequest) (*resolverv1.WriteFlagLogsResponse, error) {
return nil, errors.New("unavailable")
},
}

logger := NewGrpcWasmFlagLogger(mockStub, "test-secret", testLogger)

for i := 0; i < 10; i++ {
logger.Write(&resolverv1.WriteFlagLogsRequest{
FlagAssigned: make([]*resolverv1.FlagAssigned, 1),
})
}
logger.Shutdown()

output := buf.String()
if !strings.Contains(output, "Flag log write failures") {
t.Error("Expected failure log after window with all failures")
}
if !strings.Contains(output, "failures=10") {
t.Errorf("Expected failures=10 in output, got: %s", output)
}
}

func TestNoOpWasmFlagLogger(t *testing.T) {
logger := NewNoOpWasmFlagLogger()

Expand Down
163 changes: 163 additions & 0 deletions openfeature-provider/go/confidence/retry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"sync/atomic"
"testing"
"time"

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

func TestAllRetriesExhausted(t *testing.T) {
var callCount atomic.Int32
srv := grpc.NewServer()
resolverv1.RegisterInternalFlagLoggerServiceServer(srv, &retryTestService{
callCount: &callCount,
failUntil: 100,
})

lis := bufconn.Listen(1024 * 1024)
go srv.Serve(lis)
t.Cleanup(srv.Stop)

conn, err := grpc.NewClient("passthrough:///bufconn",
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
return lis.DialContext(ctx)
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(confidence.RetryServiceConfig),
)
if err != nil {
t.Fatalf("Failed to dial: %v", err)
}
t.Cleanup(func() { conn.Close() })

stub := resolverv1.NewInternalFlagLoggerServiceClient(conn)
logger := fl.NewGrpcWasmFlagLogger(stub, "test-secret", slog.New(slog.NewTextHandler(os.Stderr, nil)))

logger.Write(&resolverv1.WriteFlagLogsRequest{
FlagAssigned: []*resolverv1.FlagAssigned{{ResolveId: "r1"}},
})
logger.Shutdown()

if got := callCount.Load(); got != 3 {
t.Errorf("Expected 3 calls (max retries exhausted), got %d", got)
}
}

func TestShutdownDuringRetries(t *testing.T) {
var callCount atomic.Int32
srv := grpc.NewServer()
resolverv1.RegisterInternalFlagLoggerServiceServer(srv, &retryTestService{
callCount: &callCount,
failUntil: 100,
})

lis := bufconn.Listen(1024 * 1024)
go srv.Serve(lis)
t.Cleanup(srv.Stop)

conn, err := grpc.NewClient("passthrough:///bufconn",
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
return lis.DialContext(ctx)
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(confidence.RetryServiceConfig),
)
if err != nil {
t.Fatalf("Failed to dial: %v", err)
}
t.Cleanup(func() { conn.Close() })

stub := resolverv1.NewInternalFlagLoggerServiceClient(conn)
logger := fl.NewGrpcWasmFlagLogger(stub, "test-secret", slog.New(slog.NewTextHandler(os.Stderr, nil)))

logger.Write(&resolverv1.WriteFlagLogsRequest{
FlagAssigned: []*resolverv1.FlagAssigned{{ResolveId: "r1"}},
})

done := make(chan struct{})
go func() {
logger.Shutdown()
close(done)
}()

select {
case <-done:
case <-time.After(30 * time.Second):
t.Fatal("Shutdown did not complete within 30 seconds")
}
}

func TestTransportHooksPreserveRetry(t *testing.T) {
var callCount atomic.Int32
srv := grpc.NewServer()
resolverv1.RegisterInternalFlagLoggerServiceServer(srv, &retryTestService{
callCount: &callCount,
failUntil: 1,
})

lis := bufconn.Listen(1024 * 1024)
go srv.Serve(lis)
t.Cleanup(srv.Stop)

baseOpts := []grpc.DialOption{
grpc.WithDefaultServiceConfig(confidence.RetryServiceConfig),
}
opts := append([]grpc.DialOption{}, baseOpts...)
opts = append(opts,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
return lis.DialContext(ctx)
}),
)

conn, err := grpc.NewClient("passthrough:///bufconn", opts...)
if err != nil {
t.Fatalf("Failed to dial: %v", err)
}
t.Cleanup(func() { conn.Close() })

stub := resolverv1.NewInternalFlagLoggerServiceClient(conn)
logger := fl.NewGrpcWasmFlagLogger(stub, "test-secret", slog.New(slog.NewTextHandler(os.Stderr, nil)))

logger.Write(&resolverv1.WriteFlagLogsRequest{
FlagAssigned: []*resolverv1.FlagAssigned{{ResolveId: "r1"}},
})
logger.Shutdown()

if got := callCount.Load(); got != 2 {
t.Errorf("Expected 2 calls (retry should work with custom hooks), got %d", got)
}
}

func TestMultipleWritesWithRetryConfig(t *testing.T) {
var callCount atomic.Int32
srv := grpc.NewServer()
resolverv1.RegisterInternalFlagLoggerServiceServer(srv, &retryTestService{
callCount: &callCount,
failUntil: 0,
})

lis := bufconn.Listen(1024 * 1024)
go srv.Serve(lis)
t.Cleanup(srv.Stop)

conn, err := grpc.NewClient("passthrough:///bufconn",
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
return lis.DialContext(ctx)
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(confidence.RetryServiceConfig),
)
if err != nil {
t.Fatalf("Failed to dial: %v", err)
}
t.Cleanup(func() { conn.Close() })

stub := resolverv1.NewInternalFlagLoggerServiceClient(conn)
logger := fl.NewGrpcWasmFlagLogger(stub, "test-secret", slog.New(slog.NewTextHandler(os.Stderr, nil)))

for i := 0; i < 5; i++ {
logger.Write(&resolverv1.WriteFlagLogsRequest{
FlagAssigned: []*resolverv1.FlagAssigned{{ResolveId: "r1"}},
})
}
logger.Shutdown()

if got := callCount.Load(); got != 5 {
t.Errorf("Expected 5 calls (all successful, no retries), got %d", got)
}
}

type retryTestService struct {
resolverv1.UnimplementedInternalFlagLoggerServiceServer
callCount *atomic.Int32
Expand Down
Loading