-
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
Merged
Merged
Changes from 10 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
d0093fb
Added Expiring LRU Cache implementation
maya-the-mango ffc0c59
Added Expiring LRU Cache implementation
maya-the-mango 90429c1
Reorganized caches into their own files
maya-the-mango e4d1d26
fixed older unit tests
maya-the-mango 001ae6e
renaming classes and file organizing
maya-the-mango c386303
Fixed cache eviction for TLRU cache entries that were overwritten. Ch…
maya-the-mango e5c85cc
minor code cleanliness improvements
maya-the-mango 4b02b3e
memory optimization of tlru cache
maya-the-mango 4bdfa7f
use better name for bandit cache method in EppoClient
maya-the-mango d4a14bd
fixed tests
maya-the-mango 1cf7391
added more test for tlru cache, changed get, set, entries, keys, valu…
maya-the-mango 64c6729
fixed get() behaviour of tlru cache
maya-the-mango 1d1f7ee
sdk version bump
maya-the-mango 7336c3e
Merge branch 'main' into selki/FF3396-expiring-bandit-cache
maya-the-mango File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { TLRUCache } from './tlru-cache'; | ||
|
||
describe('TLRU Cache', () => { | ||
maya-the-mango marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let cache: TLRUCache; | ||
const expectedCacheTimeoutMs = 10; | ||
|
||
beforeEach(async () => { | ||
cache = new TLRUCache(2, expectedCacheTimeoutMs); | ||
}); | ||
|
||
afterEach(async () => { | ||
jest.restoreAllMocks(); | ||
jest.clearAllTimers(); | ||
}); | ||
|
||
it('should evict cache after expiration', () => { | ||
jest.useFakeTimers(); | ||
|
||
cache.set('a', 'apple'); | ||
jest.advanceTimersByTime(expectedCacheTimeoutMs); | ||
|
||
expect(cache.get('a')).toBeUndefined(); | ||
}); | ||
|
||
it('should evict all expired entries', () => { | ||
jest.useFakeTimers(); | ||
|
||
cache.set('a', 'avocado'); | ||
jest.advanceTimersByTime(expectedCacheTimeoutMs); | ||
cache.set('b', 'banana'); | ||
jest.advanceTimersByTime(expectedCacheTimeoutMs); | ||
|
||
expect(cache.get('b')).toBeUndefined(); | ||
expect(cache.get('a')).toBeUndefined(); | ||
}); | ||
|
||
/** | ||
* This test assumes implementation which is not ideal, but that's | ||
* the only way I know of how to go around timers in jest | ||
**/ | ||
it('should overwrite existing cache entry', () => { | ||
jest.useFakeTimers(); | ||
|
||
cache.set('a', 'apple'); | ||
jest.advanceTimersByTime(expectedCacheTimeoutMs - 1); | ||
cache.set('a', 'avocado'); | ||
|
||
// spin the clock by 5sec. After that time cache entry should be still valid. | ||
jest.advanceTimersByTime(expectedCacheTimeoutMs / 2); | ||
|
||
// setting assertion in a weird way because calling cache.get() | ||
// will reset eviction timer which will mess up next assertion | ||
let avocadoInCache = false; | ||
cache.forEach((value, key) => { | ||
if (key === 'a' && value === 'avocado') { | ||
avocadoInCache = true; | ||
} | ||
}); | ||
expect(avocadoInCache).toBe(true); | ||
|
||
// after another spin of 5 sec, cache entry should evict itself | ||
jest.advanceTimersByTime(expectedCacheTimeoutMs / 2); | ||
expect(cache.get('a')).toBeUndefined(); | ||
}); | ||
|
||
it('should check if a key exists', () => { | ||
cache.set('a', 'apple'); | ||
expect(cache.has('a')).toBeTruthy(); | ||
expect(cache.has('b')).toBeFalsy(); | ||
}); | ||
|
||
it('should handle the cache capacity of zero', () => { | ||
const zeroCache = new TLRUCache(0, expectedCacheTimeoutMs); | ||
zeroCache.set('a', 'apple'); | ||
expect(zeroCache.get('a')).toBeFalsy(); | ||
}); | ||
|
||
it('should handle the cache capacity of one', () => { | ||
maya-the-mango marked this conversation as resolved.
Show resolved
Hide resolved
|
||
jest.useFakeTimers(); | ||
const oneCache = new TLRUCache(1, expectedCacheTimeoutMs); | ||
oneCache.set('a', 'apple'); | ||
jest.advanceTimersByTime(expectedCacheTimeoutMs); | ||
expect(oneCache.get('a')).toBeUndefined(); | ||
|
||
oneCache.set('a', 'avocado'); | ||
expect(oneCache.get('a')).toBe('avocado'); | ||
oneCache.set('b', 'banana'); | ||
expect(oneCache.get('a')).toBeFalsy(); | ||
expect(oneCache.get('b')).toBe('banana'); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { LRUCache } from './lru-cache'; | ||
|
||
/** | ||
* Time-aware, least-recently-used cache (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 | ||
* @param {number} evictionInterval - Frequency of cache entries eviction check | ||
**/ | ||
export class TLRUCache extends LRUCache { | ||
rasendubi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private readonly cacheEntriesTTLRegistry = new Map<string, Date>(); | ||
constructor(readonly maxSize: number, readonly ttl: number) { | ||
super(maxSize); | ||
} | ||
|
||
private getCacheEntryEvictionTime(): Date { | ||
return new Date(Date.now() + this.ttl); | ||
} | ||
|
||
private clearCacheEntryEvictionTimeIfExists(key: string): void { | ||
if (this.cacheEntriesTTLRegistry.has(key)) { | ||
this.cacheEntriesTTLRegistry.delete(key); | ||
} | ||
} | ||
|
||
private setCacheEntryEvictionTime(key: string): void { | ||
this.cacheEntriesTTLRegistry.set(key, this.getCacheEntryEvictionTime()); | ||
} | ||
|
||
private resetCacheEntryEvictionTime(key: string): void { | ||
this.clearCacheEntryEvictionTimeIfExists(key); | ||
this.setCacheEntryEvictionTime(key); | ||
} | ||
|
||
private evictExpiredCacheEntries() { | ||
maya-the-mango marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const now = new Date(Date.now()); | ||
let cacheKey: string; | ||
let evictionDate: Date; | ||
|
||
// Not using this.cache.forEach so we can break the loop once | ||
maya-the-mango marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// we find the fist non-expired entry. Each entry after that | ||
// is guaranteed to also be non-expired, because they are oldest->newest | ||
maya-the-mango marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
for ([cacheKey, evictionDate] of this.cacheEntriesTTLRegistry.entries()) { | ||
if (now >= evictionDate) { | ||
this.delete(cacheKey); | ||
} else { | ||
break; | ||
} | ||
} | ||
} | ||
|
||
delete(key: string): boolean { | ||
this.clearCacheEntryEvictionTimeIfExists(key); | ||
return super.delete(key); | ||
} | ||
|
||
get(key: string): string | undefined { | ||
this.evictExpiredCacheEntries(); | ||
maya-the-mango marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
const value = super.get(key); | ||
if (value !== undefined) { | ||
// Whenever we get a cache hit, we need to reset the timer | ||
// for eviction, because it is now considered most recently | ||
// accessed thus the timer should start over. Not doing that | ||
// will cause a de-sync that will stop proper eviction | ||
this.resetCacheEntryEvictionTime(key); | ||
} | ||
return value; | ||
} | ||
|
||
set(key: string, value: string): this { | ||
this.evictExpiredCacheEntries(); | ||
maya-the-mango marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
const cache = super.set(key, value); | ||
this.resetCacheEntryEvictionTime(key); | ||
return cache; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { DEFAULT_TLRU_TTL_MS } from '../constants'; | ||
|
||
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 = DEFAULT_TLRU_TTL_MS; // 10 minutes | ||
|
||
beforeAll(() => { | ||
jest.useFakeTimers(); | ||
cache = new TLRUInMemoryAssignmentCache(2); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.clearAllTimers(); | ||
}); | ||
|
||
it(`assignment cache's timeout should default to 10 minutes `, () => { | ||
const cacheEntry = { subjectKey: 'a', flagKey: 'b', banditKey: 'c', actionKey: 'd' }; | ||
cache.set(cacheEntry); | ||
jest.advanceTimersByTime(defaultTimout); | ||
expect(cache.has(cacheEntry)).toBeFalsy(); | ||
}); | ||
|
||
it(`assignment cache's timeout value is used on construction`, () => { | ||
const expectedTimout = 88; | ||
cache = new TLRUInMemoryAssignmentCache(2, expectedTimout); | ||
const cacheEntry = { subjectKey: 'a', flagKey: 'b', banditKey: 'c', actionKey: 'd' }; | ||
cache.set(cacheEntry); | ||
jest.advanceTimersByTime(expectedTimout); | ||
expect(cache.has(cacheEntry)).toBeFalsy(); | ||
}); | ||
|
||
it(`cache shouldn't be invalidated before timeout`, () => { | ||
const cacheEntry = { subjectKey: 'a', flagKey: 'b', banditKey: 'c', actionKey: 'd' }; | ||
cache.set(cacheEntry); | ||
|
||
expect(cache.has(cacheEntry)).toBeTruthy(); | ||
|
||
jest.advanceTimersByTime(defaultTimout); | ||
expect(cache.has(cacheEntry)).toBeFalsy(); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { DEFAULT_TLRU_TTL_MS } from '../constants'; | ||
|
||
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 = DEFAULT_TLRU_TTL_MS) { | ||
super(new TLRUCache(maxSize, ttl)); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.