-
Notifications
You must be signed in to change notification settings - Fork 1
Selki/ff3396 expiring bandit cache #128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
d0093fb
ffc0c59
90429c1
e4d1d26
001ae6e
c386303
e5c85cc
4b02b3e
4bdfa7f
d4a14bd
1cf7391
64c6729
1d1f7ee
7336c3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { AbstractAssignmentCache } from './abstract-assignment-cache'; | ||
import { LRUCache } from './lru-cache'; | ||
|
||
/** | ||
* A cache that uses the LRU algorithm to evict the least recently used items. | ||
* | ||
* It is used to limit the size of the cache. | ||
* | ||
* The primary use case is for server-side SDKs, where the cache is shared across | ||
* multiple users. In this case, the cache size should be set to the maximum number | ||
* of users that can be active at the same time. | ||
* @param {number} maxSize - Maximum cache size | ||
*/ | ||
export class LRUInMemoryAssignmentCache extends AbstractAssignmentCache<LRUCache> { | ||
maya-the-mango marked this conversation as resolved.
Show resolved
Hide resolved
|
||
constructor(maxSize: number) { | ||
super(new LRUCache(maxSize)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { AbstractAssignmentCache } from './abstract-assignment-cache'; | ||
|
||
/** | ||
* A cache that never expires. | ||
* | ||
* The primary use case is for client-side SDKs, where the cache is only used | ||
* for a single user. | ||
*/ | ||
export class NonExpiringInMemoryAssignmentCache extends AbstractAssignmentCache< | ||
Map<string, string> | ||
> { | ||
constructor(store = new Map<string, string>()) { | ||
super(store); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { TLRUCache } from './lru-cache'; | ||
|
||
describe('TLRU Cache', () => { | ||
maya-the-mango marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let cache: TLRUCache; | ||
const expectedCacheTimeoutMs = 50; | ||
|
||
beforeEach(async () => { | ||
cache = new TLRUCache(2, expectedCacheTimeoutMs); | ||
}); | ||
|
||
afterAll(async () => { | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
it('should evict cache after timeout', async () => { | ||
jest.useFakeTimers(); | ||
jest.spyOn(global, 'setTimeout'); | ||
|
||
cache.set('a', 'apple'); | ||
jest.advanceTimersByTime(expectedCacheTimeoutMs); | ||
|
||
expect(setTimeout).toHaveBeenCalledTimes(1); | ||
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), expectedCacheTimeoutMs); | ||
expect(cache.get('a')).toBeUndefined(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { LRUCache } from './lru-cache'; | ||
|
||
/** | ||
* Time-aware, least-recently-used (TLRU), variant of LRU where entries have valid lifetime. | ||
* @param {number} maxSize - Maximum cache size | ||
* @param {number} ttl - Time in milliseconds after which cache entry will evict itself | ||
**/ | ||
export class TLRUCache extends LRUCache { | ||
rasendubi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
constructor(readonly maxSize: number, readonly ttl: number) { | ||
super(maxSize); | ||
} | ||
|
||
set(key: string, value: string): this { | ||
const cache = super.set(key, value); | ||
setTimeout(() => { | ||
this.delete(key); | ||
}, this.ttl); | ||
return cache; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { TLRUInMemoryAssignmentCache } from './tlru-in-memory-assignment-cache'; | ||
|
||
describe('ExpiringLRUInMemoryAssignmentCache', () => { | ||
maya-the-mango marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let cache: TLRUInMemoryAssignmentCache; | ||
const defaultTimout = 60_000; // 10 minutes | ||
rasendubi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
beforeAll(() => { | ||
jest.useFakeTimers(); | ||
cache = new TLRUInMemoryAssignmentCache(2); | ||
}); | ||
|
||
it(`assignment cache's timeout should default to 10 minutes `, () => { | ||
const key1 = { subjectKey: 'a', flagKey: 'b', banditKey: 'c', actionKey: 'd' }; | ||
rasendubi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
cache.set(key1); | ||
jest.advanceTimersByTime(defaultTimout); | ||
expect(cache.has(key1)).toBeFalsy(); | ||
}); | ||
|
||
it(`assignment cache's timeout value is used on construction`, () => { | ||
const expectedTimout = 88; | ||
cache = new TLRUInMemoryAssignmentCache(2, expectedTimout); | ||
const key1 = { subjectKey: 'a', flagKey: 'b', banditKey: 'c', actionKey: 'd' }; | ||
cache.set(key1); | ||
jest.advanceTimersByTime(expectedTimout); | ||
expect(cache.has(key1)).toBeFalsy(); | ||
}); | ||
|
||
it(`cache shouldn't be invalidated before timeout`, () => { | ||
const key1 = { subjectKey: 'a', flagKey: 'b', banditKey: 'c', actionKey: 'd' }; | ||
cache.set(key1); | ||
|
||
expect(cache.has(key1)).toBeTruthy(); | ||
|
||
jest.advanceTimersByTime(defaultTimout); | ||
expect(cache.has(key1)).toBeFalsy(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { AbstractAssignmentCache } from './abstract-assignment-cache'; | ||
import { TLRUCache } from './tlru-cache'; | ||
|
||
/** | ||
* Variation of LRU caching mechanism that will automatically evict items after | ||
* set time of milliseconds. | ||
* | ||
* It is used to limit the size of the cache. | ||
* | ||
* @param {number} maxSize - Maximum cache size | ||
* @param {number} ttl - Time in milliseconds after cache will expire. | ||
*/ | ||
export class TLRUInMemoryAssignmentCache extends AbstractAssignmentCache<TLRUCache> { | ||
constructor(maxSize: number, ttl = 60_000) { | ||
super(new TLRUCache(maxSize, ttl)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,11 +3,10 @@ import { logger } from '../application-logger'; | |
import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger'; | ||
import { BanditEvaluator } from '../bandit-evaluator'; | ||
import { IBanditEvent, IBanditLogger } from '../bandit-logger'; | ||
import { | ||
AssignmentCache, | ||
LRUInMemoryAssignmentCache, | ||
NonExpiringInMemoryAssignmentCache, | ||
} from '../cache/abstract-assignment-cache'; | ||
import { AssignmentCache } from '../cache/abstract-assignment-cache'; | ||
import { LRUInMemoryAssignmentCache } from '../cache/lru-in-memory-assignment-cache'; | ||
import { NonExpiringInMemoryAssignmentCache } from '../cache/non-expiring-in-memory-cache-assignment'; | ||
import { TLRUInMemoryAssignmentCache } from '../cache/tlru-in-memory-assignment-cache'; | ||
import ConfigurationRequestor from '../configuration-requestor'; | ||
import { IConfigurationStore } from '../configuration-store/configuration-store'; | ||
import { | ||
|
@@ -254,25 +253,6 @@ export default class EppoClient { | |
return this.getBooleanAssignment(flagKey, subjectKey, subjectAttributes, defaultValue); | ||
} | ||
|
||
/** | ||
* Maps a subject to a boolean variation for a given experiment. | ||
* | ||
* @param flagKey feature flag identifier | ||
* @param subjectKey an identifier of the experiment subject, for example a user ID. | ||
* @param subjectAttributes optional attributes associated with the subject, for example name and email. | ||
* @param defaultValue default value to return if the subject is not part of the experiment sample | ||
* @returns a boolean variation value if the subject is part of the experiment sample, otherwise the default value | ||
*/ | ||
public getBooleanAssignment( | ||
flagKey: string, | ||
subjectKey: string, | ||
subjectAttributes: Attributes, | ||
defaultValue: boolean, | ||
): boolean { | ||
return this.getBooleanAssignmentDetails(flagKey, subjectKey, subjectAttributes, defaultValue) | ||
.variation; | ||
} | ||
|
||
/** | ||
* Maps a subject to a boolean variation for a given experiment and provides additional details about the | ||
* variation assigned and the reason for the assignment. | ||
|
@@ -657,6 +637,25 @@ export default class EppoClient { | |
return result; | ||
} | ||
|
||
/** | ||
* Maps a subject to a boolean variation for a given experiment. | ||
* | ||
* @param flagKey feature flag identifier | ||
* @param subjectKey an identifier of the experiment subject, for example a user ID. | ||
* @param subjectAttributes optional attributes associated with the subject, for example name and email. | ||
* @param defaultValue default value to return if the subject is not part of the experiment sample | ||
* @returns a boolean variation value if the subject is part of the experiment sample, otherwise the default value | ||
*/ | ||
public getBooleanAssignment( | ||
rasendubi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
flagKey: string, | ||
subjectKey: string, | ||
subjectAttributes: Attributes, | ||
defaultValue: boolean, | ||
): boolean { | ||
return this.getBooleanAssignmentDetails(flagKey, subjectKey, subjectAttributes, defaultValue) | ||
.variation; | ||
} | ||
|
||
private ensureActionsWithContextualAttributes( | ||
actions: BanditActions, | ||
): Record<string, ContextAttributes> { | ||
|
@@ -984,7 +983,15 @@ export default class EppoClient { | |
} | ||
|
||
public useLRUInMemoryBanditAssignmentCache(maxSize: number) { | ||
this.banditAssignmentCache = new LRUInMemoryAssignmentCache(maxSize); | ||
this.banditAssignmentCache = new TLRUInMemoryAssignmentCache(maxSize); | ||
|
||
} | ||
|
||
/** | ||
* @param {number} maxSize - Maximum cache size | ||
* @param {number} timeout - TTL of cache entries | ||
*/ | ||
public useTLRUInMemoryAssignmentCache(maxSize: number, timeout?: number) { | ||
|
||
this.banditAssignmentCache = new TLRUInMemoryAssignmentCache(maxSize, timeout); | ||
} | ||
|
||
public useCustomBanditAssignmentCache(cache: AssignmentCache) { | ||
|
Uh oh!
There was an error while loading. Please reload this page.