Skip to content

Commit f263b50

Browse files
committed
use an assignment cache to deduplicate bandit action assignments
1 parent 6b8e782 commit f263b50

File tree

3 files changed

+59
-7
lines changed

3 files changed

+59
-7
lines changed

src/cache/abstract-assignment-cache.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ import { getMD5Hash } from '../obfuscation';
22

33
import { LRUCache } from './lru-cache';
44

5-
export type AssignmentCacheValue = {
5+
export type FlagAssignmentCacheValue = {
66
allocationKey: string;
77
variationKey: string;
88
};
99

10+
export type BanditAssignmentCacheValue = {
11+
banditKey: string;
12+
actionKey: string;
13+
};
14+
15+
type AssignmentCacheValue = FlagAssignmentCacheValue | BanditAssignmentCacheValue;
16+
1017
export type AssignmentCacheKey = {
1118
subjectKey: string;
1219
flagKey: string;
@@ -19,11 +26,18 @@ export function assignmentCacheKeyToString({ subjectKey, flagKey }: AssignmentCa
1926
return getMD5Hash([subjectKey, flagKey].join(';'));
2027
}
2128

22-
export function assignmentCacheValueToString({
23-
allocationKey,
24-
variationKey,
25-
}: AssignmentCacheValue): string {
26-
return getMD5Hash([allocationKey, variationKey].join(';'));
29+
export function assignmentCacheValueToString(cacheValue: AssignmentCacheValue): string {
30+
const fieldsToHash: string[] = [];
31+
32+
if ('allocationKey' in cacheValue && 'variationKey' in cacheValue) {
33+
fieldsToHash.push(cacheValue.allocationKey, cacheValue.variationKey);
34+
}
35+
36+
if ('banditKey' in cacheValue && 'actionKey' in cacheValue) {
37+
fieldsToHash.push(cacheValue.banditKey, cacheValue.actionKey);
38+
}
39+
40+
return getMD5Hash(fieldsToHash.join(';'));
2741
}
2842

2943
export interface AsyncMap<K, V> {

src/client/eppo-client-with-bandits.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ describe('EppoClient Bandits E2E test', () => {
480480

481481
beforeEach(() => {
482482
client.useNonExpiringInMemoryAssignmentCache();
483+
client.useNonExpiringInMemoryBanditAssignmentCache();
483484
});
484485

485486
afterEach(() => {
@@ -538,7 +539,7 @@ describe('EppoClient Bandits E2E test', () => {
538539
const fourthBanditAssignment = requestClientBanditAction();
539540

540541
expect(fourthBanditAssignment.variation).toBe('banner_bandit');
541-
expect(thirdBanditAssignment.action).toBe('toyota');
542+
expect(fourthBanditAssignment.action).toBe('toyota');
542543
expect(mockLogAssignment).toHaveBeenCalledTimes(2);
543544
expect(mockLogBanditAction).toHaveBeenCalledTimes(3);
544545

src/client/eppo-client.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export default class EppoClient {
7575
private banditLogger?: IBanditLogger;
7676
private isGracefulFailureMode = true;
7777
private assignmentCache?: AssignmentCache;
78+
private banditAssignmentCache?: AssignmentCache;
7879
private requestPoller?: IPoller;
7980
private readonly evaluator = new Evaluator();
8081
private readonly banditEvaluator = new BanditEvaluator();
@@ -651,6 +652,25 @@ export default class EppoClient {
651652
}
652653

653654
private logBanditAction(banditEvent: IBanditEvent): void {
655+
const subjectKey = banditEvent.subject;
656+
const flagKey = banditEvent.featureFlag;
657+
const banditKey = banditEvent.bandit;
658+
const actionKey = banditEvent.action ?? '__eppo_no_action';
659+
660+
// What our bandit assignment cache cares about for avoiding logging duplicate bandit assignments,
661+
// if one is active. Like the flag assignment cache, entries are only stored for a given flag
662+
// and subject.
663+
const banditAssignmentCacheProperties = {
664+
flagKey,
665+
subjectKey,
666+
banditKey,
667+
actionKey,
668+
};
669+
670+
if (this.banditAssignmentCache?.has(banditAssignmentCacheProperties)) {
671+
return;
672+
}
673+
654674
if (!this.banditLogger) {
655675
// No bandit logger set; enqueue the event in case a logger is later set
656676
if (this.queuedBanditEvents.length < MAX_EVENT_QUEUE_SIZE) {
@@ -661,6 +681,7 @@ export default class EppoClient {
661681
// If here, we have a logger
662682
try {
663683
this.banditLogger.logBanditAction(banditEvent);
684+
this.banditAssignmentCache?.set(banditAssignmentCacheProperties);
664685
} catch (err) {
665686
logger.warn('Error encountered logging bandit action', err);
666687
}
@@ -904,6 +925,22 @@ export default class EppoClient {
904925
this.assignmentCache = cache;
905926
}
906927

928+
public disableBanditAssignmentCache() {
929+
this.banditAssignmentCache = undefined;
930+
}
931+
932+
public useNonExpiringInMemoryBanditAssignmentCache() {
933+
this.banditAssignmentCache = new NonExpiringInMemoryAssignmentCache();
934+
}
935+
936+
public useLRUInMemoryBanditAssignmentCache(maxSize: number) {
937+
this.banditAssignmentCache = new LRUInMemoryAssignmentCache(maxSize);
938+
}
939+
940+
public useCustomBanditAssignmentCache(cache: AssignmentCache) {
941+
this.banditAssignmentCache = cache;
942+
}
943+
907944
public setIsGracefulFailureMode(gracefulFailureMode: boolean) {
908945
this.isGracefulFailureMode = gracefulFailureMode;
909946
}

0 commit comments

Comments
 (0)