Skip to content

Commit 80c9f95

Browse files
refactor: bruke lru-cache for hurtigbuffer
1 parent b44ea9c commit 80c9f95

File tree

11 files changed

+239
-616
lines changed

11 files changed

+239
-616
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ jobs:
3232
- name: Build
3333
run: yarn build
3434

35+
- name: Test (API, Unit)
36+
run: yarn workspace ordbokapi run test
37+
3538
# - name: Test (E2E)
3639
# # Make sure to output results to a file so we can parse them later
3740
# run: yarn test:e2e --ci --reporters=default --reporters=jest-junit

packages/api/jest.setup.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
global.performance = {
2+
...global.performance,
3+
now: () => Date.now(),
4+
};

packages/api/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"class-validator": "^0.14.1",
3737
"fastify": "^5.2.1",
3838
"graphql": "^16.10.0",
39+
"lru-cache": "^11.0.2",
3940
"memjs": "^1.3.2",
4041
"ordbokapi-common": "workspace:^",
4142
"reflect-metadata": "^0.2.2",
@@ -50,6 +51,7 @@
5051
"@nestjs/testing": "^11.0.5",
5152
"@types/express": "^5.0.0",
5253
"@types/jest": "^29.5.14",
54+
"@types/lru-cache": "^7.10.10",
5355
"@types/memjs": "^1.3.3",
5456
"@types/node": "^22.10.10",
5557
"@types/request-ip": "^0.0.41",
@@ -86,6 +88,9 @@
8688
"collectCoverageFrom": [
8789
"**/*.(t|j)s"
8890
],
91+
"setupFiles": [
92+
"<rootDir>/../jest.setup.ts"
93+
],
8994
"coverageDirectory": "../coverage",
9095
"testEnvironment": "node"
9196
}

packages/api/src/dictionary/dictionary.module.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,7 @@ import {
1010
CacheWrapperService,
1111
UibCacheService,
1212
} from './providers';
13-
import {
14-
BuildInfoProvider,
15-
// MemcachedProvider,
16-
InMemoryCacheProvider,
17-
// MemcachedCacheProvider,
18-
CacheSerializationProvider,
19-
} from '../providers';
13+
import { BuildInfoProvider, InMemoryCacheProvider } from '../providers';
2014
import * as resolvers from './resolvers';
2115

2216
@Module({
@@ -30,14 +24,6 @@ import * as resolvers from './resolvers';
3024
OrdboekeneApiService,
3125
UibCacheService,
3226
])
33-
// .addIf(process.env.MEMCACHEDCLOUD_SERVERS, MemcachedProvider)
34-
.add(CacheSerializationProvider)
35-
// .add({
36-
// provide: 'ICacheProvider',
37-
// useClass: process.env.MEMCACHEDCLOUD_SERVERS
38-
// ? MemcachedCacheProvider
39-
// : InMemoryCacheProvider,
40-
// })
4127
.add({
4228
provide: 'ICacheProvider',
4329
useClass: InMemoryCacheProvider,

packages/api/src/providers/compression.provider.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { Logger } from '@nestjs/common';
2+
import { InMemoryCacheProvider } from './in-memory-cache.provider';
3+
import { TTLBucket } from './i-cache-provider';
4+
5+
describe('InMemoryCacheProvider', () => {
6+
let cacheProvider: InMemoryCacheProvider;
7+
// Used to simulate the current time.
8+
const startTime = new Date(2025, 0, 1, 0, 0, 0).getTime();
9+
10+
beforeAll(() => {
11+
// Silence the logger by overriding its methods.
12+
jest.spyOn(Logger.prototype, 'verbose').mockImplementation(() => {});
13+
jest.spyOn(Logger.prototype, 'debug').mockImplementation(() => {});
14+
jest.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
15+
});
16+
17+
beforeEach(() => {
18+
jest.useFakeTimers();
19+
jest.setSystemTime(startTime);
20+
cacheProvider = new InMemoryCacheProvider();
21+
});
22+
23+
afterEach(() => {
24+
jest.useRealTimers();
25+
});
26+
27+
test('should return null when a key is not found', async () => {
28+
const result = await cacheProvider.get('nonExistentKey');
29+
expect(result).toBeNull();
30+
});
31+
32+
describe('Non-expiring keys (TTLBucket.Never)', () => {
33+
test('should cache and retrieve a non-expiring key', async () => {
34+
cacheProvider.set('nonExpiringKey', 'nonExpiringValue', TTLBucket.Never);
35+
let result = await cacheProvider.get('nonExpiringKey');
36+
expect(result).toEqual('nonExpiringValue');
37+
38+
// Advance time by 10 minutes - the key should still be retrievable.
39+
await jest.advanceTimersByTimeAsync(10 * 60 * 1000);
40+
result = await cacheProvider.get('nonExpiringKey');
41+
expect(result).toEqual('nonExpiringValue');
42+
});
43+
});
44+
45+
describe('TTL keys with TTLBucket.Short', () => {
46+
// For TTLBucket.Short, this implementation sets:
47+
// min TTL = 1 * 60 seconds (60 sec) and max TTL = 60 * 60 seconds (3600 sec).
48+
test('should cache and retrieve a TTL key and extend its TTL on get', async () => {
49+
cacheProvider.set('ttlKey', 'ttlValue', TTLBucket.Short);
50+
51+
// Immediately get - the entry is found.
52+
let result = await cacheProvider.get('ttlKey');
53+
expect(result).toEqual('ttlValue');
54+
55+
// Advance time by 30 seconds, well within the initial 60-sec TTL.
56+
await jest.advanceTimersByTimeAsync(30 * 1000);
57+
result = await cacheProvider.get('ttlKey');
58+
expect(result).toEqual('ttlValue');
59+
// Note: Each get extends the TTL to "now + 60 sec" (but never past the
60+
// absolute expiry of t0 + 3600 sec).
61+
62+
// Advance time to nearly the maximum allowed expiry.
63+
// (Imagine we are nearly at startTime + 3600 seconds.)
64+
jest.setSystemTime(startTime + 3599 * 1000);
65+
result = await cacheProvider.get('ttlKey');
66+
expect(result).toEqual('ttlValue');
67+
68+
// Now set the time beyond the maximum TTL boundary.
69+
jest.setSystemTime(startTime + 3601 * 1000);
70+
result = await cacheProvider.get('ttlKey');
71+
expect(result).toBeNull();
72+
});
73+
74+
test('should return null if a TTL key expires in the LRUCache (without a get extension)', async () => {
75+
cacheProvider.set('ttlNoAccess', 'valueNoAccess', TTLBucket.Short);
76+
// If we do not call get within the initial 60 seconds (min TTL), the key
77+
// should be removed by lru-cache.
78+
jest.setSystemTime(startTime + 61 * 1000);
79+
await jest.advanceTimersByTimeAsync(0);
80+
await new Promise((resolve) =>
81+
jest.requireActual('timers').setImmediate(resolve),
82+
);
83+
const result = await cacheProvider.get('ttlNoAccess');
84+
expect(result).toBeNull();
85+
});
86+
});
87+
88+
describe('TTL keys with TTLBucket.Long', () => {
89+
// For TTLBucket.Long, our implementation sets:
90+
// min TTL = 30 * 60 seconds (1800 sec) and max TTL = 4 * 60 * 60 seconds (14400 sec).
91+
test('should cache and retrieve a Long TTL key and extend its TTL on get', async () => {
92+
cacheProvider.set('longTTLKey', 'longTTLValue', TTLBucket.Long);
93+
94+
// Immediately get the value.
95+
let result = await cacheProvider.get('longTTLKey');
96+
expect(result).toEqual('longTTLValue');
97+
98+
// Advance a short amount of time (e.g. 10 seconds) and the key should
99+
// still exist.
100+
await jest.advanceTimersByTimeAsync(10 * 1000);
101+
result = await cacheProvider.get('longTTLKey');
102+
expect(result).toEqual('longTTLValue');
103+
104+
// Advance time to just before the maximum expiry (t0 + 14400 sec - 1 sec):
105+
jest.setSystemTime(startTime + (14400 - 1) * 1000);
106+
result = await cacheProvider.get('longTTLKey');
107+
expect(result).toEqual('longTTLValue');
108+
109+
// Advance beyond the maximum TTL.
110+
jest.setSystemTime(startTime + 14400 * 1000 + 1);
111+
result = await cacheProvider.get('longTTLKey');
112+
expect(result).toBeNull();
113+
});
114+
});
115+
116+
describe('Delete operation', () => {
117+
test('should delete a TTL key from the cache', async () => {
118+
cacheProvider.set('keyToDelete', 'deleteValue', TTLBucket.Short);
119+
let result = await cacheProvider.get('keyToDelete');
120+
expect(result).toEqual('deleteValue');
121+
122+
// Delete the key.
123+
cacheProvider.delete('keyToDelete');
124+
result = await cacheProvider.get('keyToDelete');
125+
expect(result).toBeNull();
126+
});
127+
128+
test('should delete a non-expiring key from the cache', async () => {
129+
cacheProvider.set('nonExpDelete', 'nonExpValue', TTLBucket.Never);
130+
let result = await cacheProvider.get('nonExpDelete');
131+
expect(result).toEqual('nonExpValue');
132+
133+
// Delete the non-expiring key.
134+
cacheProvider.delete('nonExpDelete');
135+
result = await cacheProvider.get('nonExpDelete');
136+
expect(result).toBeNull();
137+
});
138+
});
139+
140+
describe('Overwriting existing keys', () => {
141+
test('should override an existing key when set is called again', async () => {
142+
// First, set a key with a TTL.
143+
cacheProvider.set('duplicateKey', 'initialValue', TTLBucket.Short);
144+
let result = await cacheProvider.get('duplicateKey');
145+
expect(result).toEqual('initialValue');
146+
147+
// Now, override it with a new value (using a different TTL bucket,
148+
// e.g. non-expiring).
149+
cacheProvider.set('duplicateKey', 'newValue', TTLBucket.Never);
150+
result = await cacheProvider.get('duplicateKey');
151+
expect(result).toEqual('newValue');
152+
});
153+
});
154+
});

0 commit comments

Comments
 (0)