add human duration ttls

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-08-23 16:29:00 +02:00
parent 93f4a773d4
commit 66dbf0afff
8 changed files with 146 additions and 14 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/backend-plugin-api': patch
'@backstage/backend-defaults': patch
'@backstage/backend-common': patch
---
Allow the cache service to accept the human duration format for TTL
+5 -3
View File
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
app: {
baseUrl: string; // defined in core, but repeated here without doc
@@ -180,7 +182,7 @@ export interface Config {
| {
store: 'memory';
/** An optional default TTL (in milliseconds). */
defaultTtl?: number;
defaultTtl?: number | HumanDuration;
}
| {
store: 'redis';
@@ -190,7 +192,7 @@ export interface Config {
*/
connection: string;
/** An optional default TTL (in milliseconds). */
defaultTtl?: number;
defaultTtl?: number | HumanDuration;
/**
* Whether or not [useRedisSets](https://github.com/jaredwray/keyv/tree/main/packages/redis#useredissets) should be configured to this redis cache.
* Defaults to true if unspecified.
@@ -205,7 +207,7 @@ export interface Config {
*/
connection: string;
/** An optional default TTL (in milliseconds). */
defaultTtl?: number;
defaultTtl?: number | HumanDuration;
};
cors?: {
@@ -22,6 +22,7 @@ import {
import { JsonValue } from '@backstage/types';
import { createHash } from 'crypto';
import Keyv from 'keyv';
import { ttlToMilliseconds } from './types';
export type CacheClientFactory = (options: CacheServiceOptions) => Keyv;
@@ -58,7 +59,9 @@ export class DefaultCacheClient implements CacheService {
opts: CacheServiceSetOptions = {},
): Promise<void> {
const k = this.getNormalizedKey(key);
await this.#client.set(k, value, opts.ttl);
const ttl =
opts.ttl !== undefined ? ttlToMilliseconds(opts.ttl) : undefined;
await this.#client.set(k, value, ttl);
}
async delete(key: string): Promise<void> {
@@ -93,4 +93,96 @@ describe('CacheManager integration', () => {
await expect(plugin2b.get('a')).resolves.toBe('plugin2b');
},
);
it.each(caches.eachSupportedId())(
'supports both milliseconds and human durations throughout, %p',
async cacheId => {
const { store, connection } = await caches.init(cacheId);
for (const defaultTtl of [200, { milliseconds: 200 }]) {
const manager = CacheManager.fromConfig(
mockServices.rootConfig({
data: {
backend: {
cache: {
store,
connection,
defaultTtl,
},
},
},
}),
).forPlugin('p');
const defaultClient = manager.getClient();
const numberOverrideClient = manager.getClient({ defaultTtl: 400 });
const durationOverrideClient = manager.getClient({
defaultTtl: { milliseconds: 400 },
});
await defaultClient.set('a', 'x');
await defaultClient.set('b', 'x');
await numberOverrideClient.set('c', 'x');
await durationOverrideClient.set('d', 'x');
await defaultClient.set('e', 'x', { ttl: 400 });
await defaultClient.set('f', 'x', { ttl: { milliseconds: 400 } });
await expect(defaultClient.get('a')).resolves.toBe('x');
await expect(defaultClient.get('b')).resolves.toBe('x');
await expect(defaultClient.get('c')).resolves.toBe('x');
await expect(defaultClient.get('d')).resolves.toBe('x');
await expect(defaultClient.get('e')).resolves.toBe('x');
await expect(defaultClient.get('f')).resolves.toBe('x');
await new Promise(resolve => setTimeout(resolve, 50 + 200));
await expect(defaultClient.get('a')).resolves.toBeUndefined();
await expect(defaultClient.get('b')).resolves.toBeUndefined();
await expect(defaultClient.get('c')).resolves.toBe('x');
await expect(defaultClient.get('d')).resolves.toBe('x');
await expect(defaultClient.get('e')).resolves.toBe('x');
await expect(defaultClient.get('f')).resolves.toBe('x');
await new Promise(resolve => setTimeout(resolve, 200));
await expect(defaultClient.get('a')).resolves.toBeUndefined();
await expect(defaultClient.get('b')).resolves.toBeUndefined();
await expect(defaultClient.get('c')).resolves.toBeUndefined();
await expect(defaultClient.get('d')).resolves.toBeUndefined();
await expect(defaultClient.get('e')).resolves.toBeUndefined();
await expect(defaultClient.get('f')).resolves.toBeUndefined();
}
},
);
it('rejects invalid defaultTtl', () => {
expect(() =>
CacheManager.fromConfig(
mockServices.rootConfig({
data: {
backend: {
cache: {
store: 'memory',
},
},
},
}),
),
).not.toThrow();
expect(() =>
CacheManager.fromConfig(
mockServices.rootConfig({
data: {
backend: {
cache: {
store: 'memory',
defaultTtl: 'hello',
},
},
},
}),
),
).toThrow(/Invalid configuration backend.cache.defaultTtl/);
});
});
@@ -21,7 +21,12 @@ import {
import { Config } from '@backstage/config';
import Keyv from 'keyv';
import { DefaultCacheClient } from './CacheClient';
import { CacheManagerOptions, PluginCacheManager } from './types';
import {
CacheManagerOptions,
PluginCacheManager,
ttlToMilliseconds,
} from './types';
import { durationToMilliseconds } from '@backstage/types';
type StoreFactory = (pluginId: string, defaultTtl: number | undefined) => Keyv;
@@ -63,7 +68,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 defaultTtlConfig = config.getOptional('backend.cache.defaultTtl');
const connectionString =
config.getOptionalString('backend.cache.connection') || '';
const useRedisSets =
@@ -71,6 +76,23 @@ export class CacheManager {
const logger = options.logger?.child({
type: 'cacheManager',
});
let defaultTtl: number | undefined;
if (defaultTtlConfig !== undefined && defaultTtlConfig !== null) {
if (typeof defaultTtlConfig === 'number') {
defaultTtl = defaultTtlConfig;
} else if (
typeof defaultTtlConfig === 'object' &&
!Array.isArray(defaultTtlConfig)
) {
defaultTtl = durationToMilliseconds(defaultTtlConfig);
} else {
throw new Error(
`Invalid configuration backend.cache.defaultTtl: ${defaultTtlConfig}, expected milliseconds number or HumanDuration object`,
);
}
}
return new CacheManager(
store,
connectionString,
@@ -111,9 +133,10 @@ export class CacheManager {
return {
getClient: (defaultOptions = {}) => {
const clientFactory = (options: CacheServiceOptions) => {
const ttl = options.defaultTtl ?? this.defaultTtl;
return this.getClientWithTtl(
pluginId,
options.defaultTtl ?? this.defaultTtl,
ttl !== undefined ? ttlToMilliseconds(ttl) : undefined,
);
};
@@ -19,6 +19,7 @@ import {
CacheService,
CacheServiceOptions,
} from '@backstage/backend-plugin-api';
import { HumanDuration, durationToMilliseconds } from '@backstage/types';
/**
* Options given when constructing a {@link CacheManager}.
@@ -44,3 +45,7 @@ export type CacheManagerOptions = {
export interface PluginCacheManager {
getClient(options?: CacheServiceOptions): CacheService;
}
export function ttlToMilliseconds(ttl: number | HumanDuration): number {
return typeof ttl === 'number' ? ttl : durationToMilliseconds(ttl);
}
+2 -2
View File
@@ -162,12 +162,12 @@ export interface CacheService {
// @public
export type CacheServiceOptions = {
defaultTtl?: number;
defaultTtl?: number | HumanDuration;
};
// @public
export type CacheServiceSetOptions = {
ttl?: number;
ttl?: number | HumanDuration;
};
// @public
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonValue } from '@backstage/types';
import { HumanDuration, JsonValue } from '@backstage/types';
/**
* Options passed to {@link CacheService.set}.
@@ -23,10 +23,10 @@ import { JsonValue } from '@backstage/types';
*/
export type CacheServiceSetOptions = {
/**
* Optional TTL in milliseconds. Defaults to the TTL provided when the client
* Optional TTL (in milliseconds if given as a number). Defaults to the TTL provided when the client
* was set up (or no TTL if none are provided).
*/
ttl?: number;
ttl?: number | HumanDuration;
};
/**
@@ -36,11 +36,11 @@ export type CacheServiceSetOptions = {
*/
export type CacheServiceOptions = {
/**
* An optional default TTL (in milliseconds) to be set when getting a client
* An optional default TTL (in milliseconds if given as a number) to be set when getting a client
* instance. If not provided, data will persist indefinitely by default (or
* can be configured per entry at set-time).
*/
defaultTtl?: number;
defaultTtl?: number | HumanDuration;
};
/**