Skip to content

Commit a3ab61b

Browse files
authored
feat: add ability to define a custom cache key invalidation version supplier (#290)
1 parent 78bc264 commit a3ab61b

File tree

12 files changed

+137
-21
lines changed

12 files changed

+137
-21
lines changed

packages/entity-cache-adapter-redis/src/GenericRedisCacher.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,39 @@ export enum RedisCacheInvalidationStrategy {
4444
* This strategy generates cache keys for both old and potential future new versions.
4545
*/
4646
SURROUNDING_CACHE_KEY_VERSIONS = 'surrounding-cache-key-versions',
47+
48+
/**
49+
* Invalidate cache keys based on user-specified function from the current cacheKeyVersion to a list of cache key
50+
* versions to invalidate.
51+
*/
52+
CUSTOM = 'custom',
4753
}
4854

55+
export type GenericRedisCacheInvalidationConfig =
56+
| {
57+
/**
58+
* Invalidation strategy for the cache.
59+
*/
60+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION;
61+
}
62+
| {
63+
/**
64+
* Invalidation strategy for the cache.
65+
*/
66+
invalidationStrategy: RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS;
67+
}
68+
| {
69+
/**
70+
* Invalidation strategy for the cache.
71+
*/
72+
invalidationStrategy: RedisCacheInvalidationStrategy.CUSTOM;
73+
74+
/**
75+
* Function that takes the current cache key version and returns the cache key versions to invalidate.
76+
*/
77+
cacheKeyVersionsToInvalidateFn: (cacheKeyVersion: number) => readonly number[];
78+
};
79+
4980
export interface GenericRedisCacheContext {
5081
/**
5182
* Instance of ioredis.Redis
@@ -77,9 +108,9 @@ export interface GenericRedisCacheContext {
77108
ttlSecondsNegative: number;
78109

79110
/**
80-
* Invalidation strategy for the cache.
111+
* Configuration for cache invalidation strategy.
81112
*/
82-
invalidationStrategy: RedisCacheInvalidationStrategy;
113+
invalidationConfig: GenericRedisCacheInvalidationConfig;
83114
}
84115

85116
export default class GenericRedisCacher<
@@ -207,7 +238,7 @@ export default class GenericRedisCacher<
207238
TSerializedLoadValue,
208239
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
209240
>(key: TLoadKey, value: TLoadValue): readonly string[] {
210-
switch (this.context.invalidationStrategy) {
241+
switch (this.context.invalidationConfig.invalidationStrategy) {
211242
case RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION:
212243
return [
213244
this.makeCacheKeyForCacheKeyVersion(key, value, this.entityConfiguration.cacheKeyVersion),
@@ -218,6 +249,12 @@ export default class GenericRedisCacher<
218249
).map((cacheKeyVersion) =>
219250
this.makeCacheKeyForCacheKeyVersion(key, value, cacheKeyVersion),
220251
);
252+
case RedisCacheInvalidationStrategy.CUSTOM:
253+
return this.context.invalidationConfig
254+
.cacheKeyVersionsToInvalidateFn(this.entityConfiguration.cacheKeyVersion)
255+
.map((cacheKeyVersion) =>
256+
this.makeCacheKeyForCacheKeyVersion(key, value, cacheKeyVersion),
257+
);
221258
}
222259
}
223260
}

packages/entity-cache-adapter-redis/src/__integration-tests__/BatchedRedisCacheAdapter-integration-test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ describe(GenericRedisCacher, () => {
8585
cacheKeyPrefix: 'test-',
8686
ttlSecondsPositive: 86400, // 1 day
8787
ttlSecondsNegative: 600, // 10 minutes
88-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
88+
invalidationConfig: {
89+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
90+
},
8991
};
9092
});
9193

packages/entity-cache-adapter-redis/src/__integration-tests__/GenericRedisCacher-full-integration-test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ describe(GenericRedisCacher, () => {
3636
cacheKeyPrefix: 'test-',
3737
ttlSecondsPositive: 86400, // 1 day
3838
ttlSecondsNegative: 600, // 10 minutes
39-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
39+
invalidationConfig: {
40+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
41+
},
4042
};
4143
});
4244

packages/entity-cache-adapter-redis/src/__integration-tests__/GenericRedisCacher-integration-test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ describe(GenericRedisCacher, () => {
3030
cacheKeyPrefix: 'test-',
3131
ttlSecondsPositive: 86400, // 1 day
3232
ttlSecondsNegative: 600, // 10 minutes
33-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
33+
invalidationConfig: {
34+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
35+
},
3436
};
3537
});
3638

packages/entity-cache-adapter-redis/src/__integration-tests__/errors-test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ describe(GenericRedisCacher, () => {
2828
cacheKeyPrefix: 'test-',
2929
ttlSecondsPositive: 86400, // 1 day
3030
ttlSecondsNegative: 600, // 10 minutes
31-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
31+
invalidationConfig: {
32+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
33+
},
3234
};
3335
});
3436

packages/entity-cache-adapter-redis/src/__tests__/GenericRedisCacher-test.ts

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ describe(GenericRedisCacher, () => {
4444
cacheKeyPrefix: 'hello-',
4545
ttlSecondsPositive: 1,
4646
ttlSecondsNegative: 2,
47-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
47+
invalidationConfig: {
48+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
49+
},
4850
},
4951
entityConfiguration,
5052
);
@@ -84,7 +86,9 @@ describe(GenericRedisCacher, () => {
8486
cacheKeyPrefix: 'hello-',
8587
ttlSecondsPositive: 1,
8688
ttlSecondsNegative: 2,
87-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
89+
invalidationConfig: {
90+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
91+
},
8892
},
8993
entityConfiguration,
9094
);
@@ -117,7 +121,9 @@ describe(GenericRedisCacher, () => {
117121
cacheKeyPrefix: 'hello-',
118122
ttlSecondsPositive: 1,
119123
ttlSecondsNegative: 2,
120-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
124+
invalidationConfig: {
125+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
126+
},
121127
},
122128
entityConfiguration,
123129
);
@@ -160,7 +166,9 @@ describe(GenericRedisCacher, () => {
160166
cacheKeyPrefix: 'hello-',
161167
ttlSecondsPositive: 1,
162168
ttlSecondsNegative: 2,
163-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
169+
invalidationConfig: {
170+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
171+
},
164172
},
165173
entityConfiguration,
166174
);
@@ -192,7 +200,9 @@ describe(GenericRedisCacher, () => {
192200
cacheKeyPrefix: 'hello-',
193201
ttlSecondsPositive: 1,
194202
ttlSecondsNegative: 2,
195-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
203+
invalidationConfig: {
204+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
205+
},
196206
},
197207
entityConfiguration,
198208
);
@@ -201,6 +211,7 @@ describe(GenericRedisCacher, () => {
201211
new SingleFieldValueHolder('wat'),
202212
);
203213
expect(cacheKeys).toHaveLength(1);
214+
expect(cacheKeys[0]).toBe('hello-:single:blah:v2.2:id:wat');
204215

205216
await genericCacher.invalidateManyAsync(cacheKeys);
206217
verify(mockRedisClient.del(...cacheKeys)).once();
@@ -217,7 +228,9 @@ describe(GenericRedisCacher, () => {
217228
cacheKeyPrefix: 'hello-',
218229
ttlSecondsPositive: 1,
219230
ttlSecondsNegative: 2,
220-
invalidationStrategy: RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS,
231+
invalidationConfig: {
232+
invalidationStrategy: RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS,
233+
},
221234
},
222235
entityConfiguration,
223236
);
@@ -226,6 +239,48 @@ describe(GenericRedisCacher, () => {
226239
new SingleFieldValueHolder('wat'),
227240
);
228241
expect(cacheKeys).toHaveLength(3);
242+
expect(cacheKeys[0]).toBe('hello-:single:blah:v2.1:id:wat');
243+
expect(cacheKeys[1]).toBe('hello-:single:blah:v2.2:id:wat');
244+
expect(cacheKeys[2]).toBe('hello-:single:blah:v2.3:id:wat');
245+
246+
await genericCacher.invalidateManyAsync(cacheKeys);
247+
verify(mockRedisClient.del(...cacheKeys)).once();
248+
});
249+
250+
it('invalidates correctly with RedisCacheInvalidationStrategy.CUSTOM', async () => {
251+
const mockRedisClient = mock<Redis>();
252+
when(mockRedisClient.del()).thenResolve(1);
253+
254+
const genericCacher = new GenericRedisCacher(
255+
{
256+
redisClient: instance(mockRedisClient),
257+
makeKeyFn: (...parts) => parts.join(':'),
258+
cacheKeyPrefix: 'hello-',
259+
ttlSecondsPositive: 1,
260+
ttlSecondsNegative: 2,
261+
invalidationConfig: {
262+
invalidationStrategy: RedisCacheInvalidationStrategy.CUSTOM,
263+
cacheKeyVersionsToInvalidateFn(cacheKeyVersion) {
264+
return [
265+
cacheKeyVersion,
266+
cacheKeyVersion + 1,
267+
cacheKeyVersion + 2,
268+
cacheKeyVersion + 3,
269+
];
270+
},
271+
},
272+
},
273+
entityConfiguration,
274+
);
275+
const cacheKeys = genericCacher['makeCacheKeysForInvalidation'](
276+
new SingleFieldHolder('id'),
277+
new SingleFieldValueHolder('wat'),
278+
);
279+
expect(cacheKeys).toHaveLength(4);
280+
expect(cacheKeys[0]).toBe('hello-:single:blah:v2.2:id:wat');
281+
expect(cacheKeys[1]).toBe('hello-:single:blah:v2.3:id:wat');
282+
expect(cacheKeys[2]).toBe('hello-:single:blah:v2.4:id:wat');
283+
expect(cacheKeys[3]).toBe('hello-:single:blah:v2.5:id:wat');
229284

230285
await genericCacher.invalidateManyAsync(cacheKeys);
231286
verify(mockRedisClient.del(...cacheKeys)).once();
@@ -239,7 +294,9 @@ describe(GenericRedisCacher, () => {
239294
cacheKeyPrefix: 'hello-',
240295
ttlSecondsPositive: 1,
241296
ttlSecondsNegative: 2,
242-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
297+
invalidationConfig: {
298+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
299+
},
243300
},
244301
entityConfiguration,
245302
);

packages/entity-full-integration-tests/src/__integration-tests__/EntityCacheInconsistency-test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ describe('Entity cache inconsistency', () => {
124124
cacheKeyPrefix: 'test-',
125125
ttlSecondsPositive: 86400, // 1 day
126126
ttlSecondsNegative: 600, // 10 minutes
127-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
127+
invalidationConfig: {
128+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
129+
},
128130
};
129131
});
130132

packages/entity-full-integration-tests/src/__integration-tests__/EntityCachePushSafety-test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ describe('Lack of entity cache push safety with RedisCacheInvalidationStrategy.C
124124
cacheKeyPrefix: 'test-',
125125
ttlSecondsPositive: 86400, // 1 day
126126
ttlSecondsNegative: 600, // 10 minutes
127-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
127+
invalidationConfig: {
128+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
129+
},
128130
};
129131
});
130132

@@ -214,7 +216,9 @@ describe('Entity cache push safety with RedisCacheInvalidationStrategy.SURROUNDI
214216
cacheKeyPrefix: 'test-',
215217
ttlSecondsPositive: 86400, // 1 day
216218
ttlSecondsNegative: 600, // 10 minutes
217-
invalidationStrategy: RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS,
219+
invalidationConfig: {
220+
invalidationStrategy: RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS,
221+
},
218222
};
219223
});
220224

packages/entity-full-integration-tests/src/__integration-tests__/EntityEdgesIntegration-test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
6262
cacheKeyPrefix: 'test-',
6363
ttlSecondsPositive: 86400, // 1 day
6464
ttlSecondsNegative: 600, // 10 minutes
65-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
65+
invalidationConfig: {
66+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
67+
},
6668
};
6769
});
6870

packages/entity-full-integration-tests/src/__integration-tests__/EntityIntegrity-test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ describe('Entity integrity', () => {
113113
cacheKeyPrefix: 'test-',
114114
ttlSecondsPositive: 86400, // 1 day
115115
ttlSecondsNegative: 600, // 10 minutes
116-
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
116+
invalidationConfig: {
117+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
118+
},
117119
};
118120
});
119121

0 commit comments

Comments
 (0)