server: fix cache eviction

Previously due to bad logic it was impossible for cache entries to ever
be evicted. Ideally this should not be done again but should just use redis
but that can be done another day.

Changelog: Fixed
This commit is contained in:
Johann150 2023-11-04 11:52:57 +01:00
parent 451c674906
commit 8d78113907
Signed by: Johann150
GPG Key ID: 9EE6577A2A06F8F1
1 changed files with 62 additions and 14 deletions

View File

@ -1,12 +1,23 @@
export class Cache<T> {
// The actual "database" that holds the cache entries, along with their
// insertion time.
// Insertion order is the same as the order of elements expiring. This is
// important because the expiration logic relies on the insertion order.
public cache: Map<string, { date: number; value: T; }>;
// The lifetime of each cache member.
//
// This must not be changed after setup because it may upset
// the expiration logic.
private lifetime: number;
// Function of which the results should be cached.
public fetcher: (key: string) => Promise<T | undefined>;
private timeoutScheduled: boolean;
constructor(lifetime: number, fetcher: Cache<T>['fetcher']) {
this.cache = new Map();
this.lifetime = lifetime;
this.fetcher = fetcher;
this.timeoutScheduled = false;
}
public set(key: string, value: T): void {
@ -14,38 +25,36 @@ export class Cache<T> {
date: Date.now(),
value,
});
// make sure the expiration timeout is in place
this.expire();
}
public get(key: string): T | undefined {
const cached = this.cache.get(key);
if (cached == null) return undefined;
// discard if past the cache lifetime
if ((Date.now() - cached.date) > this.lifetime) {
this.cache.delete(key);
return undefined;
}
return cached.value;
else return cached.value;
}
public delete(key: string): void {
this.cache.delete(key);
}
/**
* If the value is cached, it is returned. Otherwise the fetcher is
* run to get the value. If the fetcher returns undefined, it is
* returned but not cached.
*/
// If the value is cached, it is returned. Otherwise the fetcher is
// run to get the value. If the fetcher returns undefined, it is
// returned but not cached.
public async fetch(key: string): Promise<T | undefined> {
// Check if this value is cached
const cached = this.get(key);
if (cached !== undefined) {
// The value was cached, return it.
return cached;
} else {
// The value was not cached, need to call the original function
// to get its result and then cache it.
const value = await this.fetcher(key);
// don't cache undefined
// `undefined` is not cached
if (value !== undefined) {
this.set(key, value);
}
@ -53,4 +62,43 @@ export class Cache<T> {
return value;
}
}
// Handling the expiration of cached values.
// This is done using a timeout.
private expire(): void {
// If there already is a timeout scheduled, it will be appropriate
// for the first inserted element of the cache.
// If the first element of the cache was removed, it will reschedule
// to the appropriate time when it runs out.
//
// If the cache is empty, there is nothing to expire either.
if (this.timeoutScheduled) return;
// Otherwise, this must mean this is the previously scheduled timeout.
// Since it is running now, it is no longer scheduled.
this.timeoutScheduled = false;
// Check if the first element is actually due for expiration.
//
// Items may have been removed in the meantime or this may be
// the initial call for the first key inserted into the cache.
const [expiredKey, expiredValue] = this.cache.entries().next().value;
if (expiredValue.date + this.lifetime >= Date.now()) {
// This item is due for expiration, so remove it.
this.cache.delete(expiredKey);
}
// If there are no further elements in the cache, there is nothing to
// expire at a later time. The timeout will be set up again later by
// a call from `this.set`.
if (this.cache.size === 0) return;
// Check when the next key is due for removal and schedule
// an appropriate timeout.
const [nextKey, nextValue] = this.cache.entries().next().value;
setTimeout(
() => this.expire(),
nextValue.date + this.lifetime - Date.now()
);
this.timeoutScheduled = true;
}
}