feat(cache): allow a defaultTTL to be set through the app config

Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Phil Kuang
2023-10-26 17:09:12 -04:00
parent fa5382eaca
commit 7f04128bbc
4 changed files with 90 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Allow a default cache TTL to be set through the app config
+6
View File
@@ -149,6 +149,8 @@ export interface Config {
cache?:
| {
store: 'memory';
/** An optional default TTL (in milliseconds). */
defaultTtl?: number;
}
| {
store: 'redis';
@@ -157,6 +159,8 @@ export interface Config {
* @visibility secret
*/
connection: string;
/** An optional default TTL (in milliseconds). */
defaultTtl?: number;
}
| {
store: 'memcache';
@@ -165,6 +169,8 @@ export interface Config {
* @visibility secret
*/
connection: string;
/** An optional default TTL (in milliseconds). */
defaultTtl?: number;
};
cors?: {
+73
View File
@@ -33,11 +33,13 @@ jest.mock('./CacheClient', () => {
};
});
const globalDefaultTtl = 1234;
describe('CacheManager', () => {
const defaultConfigOptions = {
backend: {
cache: {
store: 'memory',
defaultTtl: globalDefaultTtl,
},
},
};
@@ -49,9 +51,11 @@ describe('CacheManager', () => {
it('accesses the backend.cache key', () => {
const getOptionalString = jest.fn();
const getOptionalBoolean = jest.fn();
const getOptionalNumber = jest.fn();
const config = defaultConfig();
config.getOptionalString = getOptionalString;
config.getOptionalBoolean = getOptionalBoolean;
config.getOptionalNumber = getOptionalNumber;
CacheManager.fromConfig(config);
@@ -62,6 +66,9 @@ describe('CacheManager', () => {
expect(getOptionalBoolean.mock.calls[0][0]).toEqual(
'backend.cache.useRedisSets',
);
expect(getOptionalNumber.mock.calls[0][0]).toEqual(
'backend.cache.defaultTtl',
);
});
it('does not require the backend.cache key', () => {
@@ -159,6 +166,20 @@ describe('CacheManager', () => {
});
});
it('returns memory client with a global defaultTtl when explicitly configured', () => {
const manager = CacheManager.fromConfig(defaultConfig());
const expectedNamespace = 'test-plugin';
manager.forPlugin(expectedNamespace).getClient();
const cache = Keyv as unknown as jest.Mock;
const mockCalls = cache.mock.calls.splice(-1);
const callArgs = mockCalls[0];
expect(callArgs[0]).toMatchObject({
ttl: globalDefaultTtl,
namespace: expectedNamespace,
});
});
it('shares memory across multiple instances of the memory client', () => {
const manager = CacheManager.fromConfig(defaultConfig());
const plugin = 'test-plugin';
@@ -201,6 +222,32 @@ describe('CacheManager', () => {
expect(mockMemcacheCalls[0][0]).toEqual(expectedHost);
});
it('returns a memcache client with a global defaultTtl when configured', () => {
const expectedHost = '127.0.0.1:11211';
const manager = CacheManager.fromConfig(
new ConfigReader({
backend: {
cache: {
store: 'memcache',
connection: expectedHost,
defaultTtl: globalDefaultTtl,
},
},
}),
);
manager.forPlugin('test').getClient();
const cache = Keyv as unknown as jest.Mock;
const mockCacheCalls = cache.mock.calls.splice(-1);
expect(mockCacheCalls[0][0]).toMatchObject({
ttl: globalDefaultTtl,
});
expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvMemcache);
const memcache = KeyvMemcache as unknown as jest.Mock;
const mockMemcacheCalls = memcache.mock.calls.splice(-1);
expect(mockMemcacheCalls[0][0]).toEqual(expectedHost);
});
it('returns a Redis client when configured', () => {
const redisConnection = 'redis://127.0.0.1:6379';
const manager = CacheManager.fromConfig(
@@ -227,6 +274,32 @@ describe('CacheManager', () => {
expect(mockRedisCalls[0][0]).toEqual(redisConnection);
});
it('returns a Redis client with a global defaultTtl when configured', () => {
const redisConnection = 'redis://127.0.0.1:6379';
const manager = CacheManager.fromConfig(
new ConfigReader({
backend: {
cache: {
store: 'redis',
connection: redisConnection,
defaultTtl: globalDefaultTtl,
},
},
}),
);
manager.forPlugin('test').getClient();
const cache = Keyv as unknown as jest.Mock;
const mockCacheCalls = cache.mock.calls.splice(-1);
expect(mockCacheCalls[0][0]).toMatchObject({
ttl: globalDefaultTtl,
});
expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvRedis);
const redis = KeyvRedis as unknown as jest.Mock;
const mockRedisCalls = redis.mock.calls.splice(-1);
expect(mockRedisCalls[0][0]).toEqual(redisConnection);
});
it('returns a Redis client when configured with useRedisSets flag', () => {
const redisConnection = 'redis://127.0.0.1:6379';
const useRedisSets = false;
+6 -1
View File
@@ -57,6 +57,7 @@ export class CacheManager {
private readonly connection: string;
private readonly useRedisSets: boolean;
private readonly errorHandler: CacheManagerOptions['onError'];
private readonly defaultTtl?: number;
/**
* Creates a new {@link CacheManager} instance by reading from the `backend`
@@ -71,6 +72,7 @@ export class CacheManager {
// If no `backend.cache` config is provided, instantiate the CacheManager
// with an in-memory cache client.
const store = config.getOptionalString('backend.cache.store') || 'memory';
const defaultTtl = config.getOptionalNumber('backend.cache.defaultTtl');
const connectionString =
config.getOptionalString('backend.cache.connection') || '';
const useRedisSets =
@@ -84,6 +86,7 @@ export class CacheManager {
useRedisSets,
logger,
options.onError,
defaultTtl,
);
}
@@ -93,6 +96,7 @@ export class CacheManager {
useRedisSets: boolean,
logger: LoggerService,
errorHandler: CacheManagerOptions['onError'],
defaultTtl?: number,
) {
if (!this.storeFactories.hasOwnProperty(store)) {
throw new Error(`Unknown cache store: ${store}`);
@@ -102,6 +106,7 @@ export class CacheManager {
this.connection = connectionString;
this.useRedisSets = useRedisSets;
this.errorHandler = errorHandler;
this.defaultTtl = defaultTtl;
}
/**
@@ -116,7 +121,7 @@ export class CacheManager {
const clientFactory = (options: CacheServiceOptions) => {
const concreteClient = this.getClientWithTtl(
pluginId,
options.defaultTtl,
options.defaultTtl ?? this.defaultTtl,
);
// Always provide an error handler to avoid stopping the process.