3
3
import static cloud .eppo .Utils .throwIfEmptyOrNull ;
4
4
5
5
import cloud .eppo .api .*;
6
+ import cloud .eppo .cache .AssignmentCacheEntry ;
6
7
import cloud .eppo .logging .Assignment ;
7
8
import cloud .eppo .logging .AssignmentLogger ;
8
9
import cloud .eppo .logging .BanditAssignment ;
@@ -35,6 +36,8 @@ public class BaseEppoClient {
35
36
private final String sdkName ;
36
37
private final String sdkVersion ;
37
38
private boolean isGracefulMode ;
39
+ private final IAssignmentCache assignmentCache ;
40
+ private final IAssignmentCache banditAssignmentCache ;
38
41
39
42
@ Nullable protected CompletableFuture <Boolean > getInitialConfigFuture () {
40
43
return initialConfigFuture ;
@@ -47,6 +50,9 @@ public class BaseEppoClient {
47
50
/** @noinspection FieldMayBeFinal */
48
51
private static EppoHttpClient httpClientOverride = null ;
49
52
53
+ // It is important that the bandit assignment cache expire with a short-enough TTL to last about
54
+ // one user session.
55
+ // The recommended is 10 minutes (per @Sven)
50
56
protected BaseEppoClient (
51
57
@ NotNull String apiKey ,
52
58
@ NotNull String sdkName ,
@@ -58,7 +64,9 @@ protected BaseEppoClient(
58
64
boolean isGracefulMode ,
59
65
boolean expectObfuscatedConfig ,
60
66
boolean supportBandits ,
61
- @ Nullable CompletableFuture <Configuration > initialConfiguration ) {
67
+ @ Nullable CompletableFuture <Configuration > initialConfiguration ,
68
+ @ Nullable IAssignmentCache assignmentCache ,
69
+ @ Nullable IAssignmentCache banditAssignmentCache ) {
62
70
63
71
if (apiKey == null ) {
64
72
throw new IllegalArgumentException ("Unable to initialize Eppo SDK due to missing API key" );
@@ -71,6 +79,9 @@ protected BaseEppoClient(
71
79
host = DEFAULT_HOST ;
72
80
}
73
81
82
+ this .assignmentCache = assignmentCache ;
83
+ this .banditAssignmentCache = banditAssignmentCache ;
84
+
74
85
EppoHttpClient httpClient = buildHttpClient (host , apiKey , sdkName , sdkVersion );
75
86
this .configurationStore =
76
87
configurationStore != null ? configurationStore : new ConfigurationStore ();
@@ -156,33 +167,51 @@ protected EppoValue getTypedAssignment(
156
167
}
157
168
158
169
if (assignedValue != null && assignmentLogger != null && evaluationResult .doLog ()) {
159
- String allocationKey = evaluationResult .getAllocationKey ();
160
- String experimentKey =
161
- flagKey
162
- + '-'
163
- + allocationKey ; // Our experiment key is derived by hyphenating the flag key and
164
- // allocation key
165
- String variationKey = evaluationResult .getVariation ().getKey ();
166
- Map <String , String > extraLogging = evaluationResult .getExtraLogging ();
167
- Map <String , String > metaData = buildLogMetaData (config .isConfigObfuscated ());
168
-
169
- Assignment assignment =
170
- new Assignment (
171
- experimentKey ,
172
- flagKey ,
173
- allocationKey ,
174
- variationKey ,
175
- subjectKey ,
176
- subjectAttributes ,
177
- extraLogging ,
178
- metaData );
170
+
179
171
try {
180
- assignmentLogger .logAssignment (assignment );
172
+ String allocationKey = evaluationResult .getAllocationKey ();
173
+ String experimentKey =
174
+ flagKey
175
+ + '-'
176
+ + allocationKey ; // Our experiment key is derived by hyphenating the flag key and
177
+ // allocation key
178
+ String variationKey = evaluationResult .getVariation ().getKey ();
179
+ Map <String , String > extraLogging = evaluationResult .getExtraLogging ();
180
+ Map <String , String > metaData = buildLogMetaData (config .isConfigObfuscated ());
181
+
182
+ Assignment assignment =
183
+ new Assignment (
184
+ experimentKey ,
185
+ flagKey ,
186
+ allocationKey ,
187
+ variationKey ,
188
+ subjectKey ,
189
+ subjectAttributes ,
190
+ extraLogging ,
191
+ metaData );
192
+
193
+ // Deduplication of assignment logging is possible by providing an `IAssignmentCache`.
194
+ // Default to true, only avoid logging if there's a cache hit.
195
+ boolean logAssignment = true ;
196
+ AssignmentCacheEntry cacheEntry = AssignmentCacheEntry .fromVariationAssignment (assignment );
197
+ if (assignmentCache != null ) {
198
+ if (assignmentCache .hasEntry (cacheEntry )) {
199
+ logAssignment = false ;
200
+ }
201
+ }
202
+
203
+ if (logAssignment ) {
204
+ assignmentLogger .logAssignment (assignment );
205
+
206
+ if (assignmentCache != null ) {
207
+ assignmentCache .put (cacheEntry );
208
+ }
209
+ }
210
+
181
211
} catch (Exception e ) {
182
- log .warn ("Error logging assignment: {}" , e .getMessage (), e );
212
+ log .error ("Error logging assignment: {}" , e .getMessage (), e );
183
213
}
184
214
}
185
-
186
215
return assignedValue != null ? assignedValue : defaultValue ;
187
216
}
188
217
@@ -428,7 +457,23 @@ public BanditResult getBanditAction(
428
457
banditResult .getActionAttributes ().getCategoricalAttributes (),
429
458
buildLogMetaData (config .isConfigObfuscated ()));
430
459
431
- banditLogger .logBanditAssignment (banditAssignment );
460
+ // Log, only if there is no cache hit.
461
+ boolean logBanditAssignment = true ;
462
+ AssignmentCacheEntry cacheEntry =
463
+ AssignmentCacheEntry .fromBanditAssignment (banditAssignment );
464
+ if (banditAssignmentCache != null ) {
465
+ if (banditAssignmentCache .hasEntry (cacheEntry )) {
466
+ logBanditAssignment = false ;
467
+ }
468
+ }
469
+
470
+ if (logBanditAssignment ) {
471
+ banditLogger .logBanditAssignment (banditAssignment );
472
+
473
+ if (banditAssignmentCache != null ) {
474
+ banditAssignmentCache .put (cacheEntry );
475
+ }
476
+ }
432
477
} catch (Exception e ) {
433
478
log .warn ("Error logging bandit assignment: {}" , e .getMessage (), e );
434
479
}
0 commit comments