add human duration ttls
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -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
|
||||
Vendored
+5
-3
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user