11package com .spotify .confidence ;
22
3+ import com .google .common .annotations .VisibleForTesting ;
34import com .spotify .confidence .shaded .flags .resolver .v1 .InternalFlagLoggerServiceGrpc ;
45import com .spotify .confidence .shaded .flags .resolver .v1 .WriteFlagLogsRequest ;
56import com .spotify .confidence .shaded .iam .v1 .AuthServiceGrpc ;
89import io .grpc .ManagedChannel ;
910import io .grpc .ManagedChannelBuilder ;
1011import java .time .Duration ;
12+ import java .util .ArrayList ;
13+ import java .util .List ;
1114import java .util .Optional ;
15+ import java .util .concurrent .ExecutorService ;
16+ import java .util .concurrent .Executors ;
1217import org .slf4j .Logger ;
1318import org .slf4j .LoggerFactory ;
1419
20+ @ FunctionalInterface
21+ interface FlagLogWriter {
22+ void write (WriteFlagLogsRequest request );
23+ }
24+
1525public class GrpcWasmFlagLogger implements WasmFlagLogger {
1626 private static final String CONFIDENCE_DOMAIN = "edge-grpc.spotify.com" ;
1727 private static final Logger logger = LoggerFactory .getLogger (GrpcWasmFlagLogger .class );
28+ // Max number of flag_assigned entries per chunk to avoid exceeding gRPC max message size
29+ private static final int MAX_FLAG_ASSIGNED_PER_CHUNK = 1000 ;
1830 private final InternalFlagLoggerServiceGrpc .InternalFlagLoggerServiceBlockingStub stub ;
31+ private final ExecutorService executorService ;
32+ private final FlagLogWriter writer ;
33+
34+ @ VisibleForTesting
35+ public GrpcWasmFlagLogger (ApiSecret apiSecret , FlagLogWriter writer ) {
36+ final var channel = createConfidenceChannel ();
37+ final AuthServiceGrpc .AuthServiceBlockingStub authService =
38+ AuthServiceGrpc .newBlockingStub (channel );
39+ final TokenHolder tokenHolder =
40+ new TokenHolder (apiSecret .clientId (), apiSecret .clientSecret (), authService );
41+ final Channel authenticatedChannel =
42+ ClientInterceptors .intercept (channel , new JwtAuthClientInterceptor (tokenHolder ));
43+ this .stub = InternalFlagLoggerServiceGrpc .newBlockingStub (authenticatedChannel );
44+ this .executorService = Executors .newCachedThreadPool ();
45+ this .writer = writer ;
46+ }
1947
2048 public GrpcWasmFlagLogger (ApiSecret apiSecret ) {
2149 final var channel = createConfidenceChannel ();
@@ -26,15 +54,88 @@ public GrpcWasmFlagLogger(ApiSecret apiSecret) {
2654 final Channel authenticatedChannel =
2755 ClientInterceptors .intercept (channel , new JwtAuthClientInterceptor (tokenHolder ));
2856 this .stub = InternalFlagLoggerServiceGrpc .newBlockingStub (authenticatedChannel );
57+ this .executorService = Executors .newCachedThreadPool ();
58+ this .writer =
59+ request ->
60+ executorService .submit (
61+ () -> {
62+ try {
63+ final var ignore = stub .writeFlagLogs (request );
64+ logger .debug (
65+ "Successfully sent flag log with {} entries" ,
66+ request .getFlagAssignedCount ());
67+ } catch (Exception e ) {
68+ logger .error ("Failed to write flag logs" , e );
69+ }
70+ });
2971 }
3072
3173 @ Override
3274 public void write (WriteFlagLogsRequest request ) {
33- if (request .getClientResolveInfoList ().isEmpty () && request .getFlagAssignedList ().isEmpty ()) {
75+ if (request .getClientResolveInfoList ().isEmpty ()
76+ && request .getFlagAssignedList ().isEmpty ()
77+ && request .getFlagResolveInfoList ().isEmpty ()) {
3478 logger .debug ("Skipping empty flag log request" );
3579 return ;
3680 }
37- final var ignore = stub .writeFlagLogs (request );
81+
82+ final int flagAssignedCount = request .getFlagAssignedCount ();
83+
84+ // If flag_assigned list is small enough, send everything as-is
85+ if (flagAssignedCount <= MAX_FLAG_ASSIGNED_PER_CHUNK ) {
86+ sendAsync (request );
87+ return ;
88+ }
89+
90+ // Split flag_assigned into chunks and send each chunk asynchronously
91+ logger .debug (
92+ "Splitting {} flag_assigned entries into chunks of {}" ,
93+ flagAssignedCount ,
94+ MAX_FLAG_ASSIGNED_PER_CHUNK );
95+
96+ final List <WriteFlagLogsRequest > chunks = createFlagAssignedChunks (request );
97+ for (WriteFlagLogsRequest chunk : chunks ) {
98+ sendAsync (chunk );
99+ }
100+ }
101+
102+ private List <WriteFlagLogsRequest > createFlagAssignedChunks (WriteFlagLogsRequest request ) {
103+ final List <WriteFlagLogsRequest > chunks = new ArrayList <>();
104+ final int totalFlags = request .getFlagAssignedCount ();
105+
106+ for (int i = 0 ; i < totalFlags ; i += MAX_FLAG_ASSIGNED_PER_CHUNK ) {
107+ final int end = Math .min (i + MAX_FLAG_ASSIGNED_PER_CHUNK , totalFlags );
108+ final WriteFlagLogsRequest .Builder chunkBuilder =
109+ WriteFlagLogsRequest .newBuilder ()
110+ .addAllFlagAssigned (request .getFlagAssignedList ().subList (i , end ));
111+
112+ // Include telemetry and resolve info only in the first chunk
113+ if (i == 0 ) {
114+ if (request .hasTelemetryData ()) {
115+ chunkBuilder .setTelemetryData (request .getTelemetryData ());
116+ }
117+ chunkBuilder
118+ .addAllClientResolveInfo (request .getClientResolveInfoList ())
119+ .addAllFlagResolveInfo (request .getFlagResolveInfoList ());
120+ }
121+
122+ chunks .add (chunkBuilder .build ());
123+ }
124+
125+ return chunks ;
126+ }
127+
128+ private void sendAsync (WriteFlagLogsRequest request ) {
129+ writer .write (request );
130+ }
131+
132+ /**
133+ * Shutdown the executor service. This will allow any pending async writes to complete. Call this
134+ * when the application is shutting down.
135+ */
136+ @ Override
137+ public void shutdown () {
138+ executorService .shutdown ();
38139 }
39140
40141 private static ManagedChannel createConfidenceChannel () {
0 commit comments