feat(backend-defaults): implement more comprehensive redis option passthrough
Signed-off-by: Mark Nachazel <markn@doordash.com>
This commit is contained in:
committed by
Mark Nachazel
parent
ae33d1635a
commit
01edf6e916
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-defaults': minor
|
||||
---
|
||||
|
||||
Allow pass through of redis client and cluster options to Cache core service
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { mockServices, TestCaches } from '@backstage/backend-test-utils';
|
||||
import KeyvRedis from '@keyv/redis';
|
||||
import KeyvRedis, { createCluster } from '@keyv/redis';
|
||||
import KeyvMemcache from '@keyv/memcache';
|
||||
import { CacheManager } from './CacheManager';
|
||||
|
||||
@@ -30,6 +30,7 @@ jest.mock('@keyv/redis', () => {
|
||||
...Actual,
|
||||
__esModule: true,
|
||||
default: jest.fn((...args: any[]) => new DefaultConstructor(...args)),
|
||||
createCluster: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('@keyv/memcache', () => {
|
||||
@@ -194,3 +195,73 @@ describe('CacheManager integration', () => {
|
||||
).toThrow(/Invalid duration 'hello' in config/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheManager store options', () => {
|
||||
it('uses default options when no store-specific config exists', () => {
|
||||
const manager = CacheManager.fromConfig(
|
||||
mockServices.rootConfig({
|
||||
data: {
|
||||
backend: {
|
||||
cache: {
|
||||
store: 'redis',
|
||||
connection: 'redis://localhost:6379',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
manager.forPlugin('p1');
|
||||
|
||||
expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', {
|
||||
keyPrefixSeparator: ':',
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults to non-clustered mode when cluster config is missing root nodes', () => {
|
||||
const manager = CacheManager.fromConfig(
|
||||
mockServices.rootConfig({
|
||||
data: {
|
||||
backend: {
|
||||
cache: {
|
||||
store: 'redis',
|
||||
connection: 'redis://localhost:6379',
|
||||
cluster: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
manager.forPlugin('p1');
|
||||
|
||||
expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', {
|
||||
keyPrefixSeparator: ':',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses cluster config when present', () => {
|
||||
const manager = CacheManager.fromConfig(
|
||||
mockServices.rootConfig({
|
||||
data: {
|
||||
backend: {
|
||||
cache: {
|
||||
store: 'redis',
|
||||
connection: 'redis://localhost:6379',
|
||||
redis: {
|
||||
cluster: {
|
||||
rootNodes: [{ url: 'redis://localhost:6379' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
manager.forPlugin('p1');
|
||||
|
||||
expect(createCluster).toHaveBeenCalledWith({
|
||||
rootNodes: [{ url: 'redis://localhost:6379' }],
|
||||
defaults: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,12 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import Keyv from 'keyv';
|
||||
import { DefaultCacheClient } from './CacheClient';
|
||||
import { CacheManagerOptions, ttlToMilliseconds } from './types';
|
||||
import {
|
||||
CacheManagerOptions,
|
||||
ttlToMilliseconds,
|
||||
CacheStoreOptions,
|
||||
RedisCacheStoreOptions,
|
||||
} from './types';
|
||||
import { durationToMilliseconds } from '@backstage/types';
|
||||
import { readDurationFromConfig } from '@backstage/config';
|
||||
|
||||
@@ -51,6 +56,7 @@ export class CacheManager {
|
||||
private readonly connection: string;
|
||||
private readonly errorHandler: CacheManagerOptions['onError'];
|
||||
private readonly defaultTtl?: number;
|
||||
private readonly storeOptions?: CacheStoreOptions;
|
||||
|
||||
/**
|
||||
* Creates a new {@link CacheManager} instance by reading from the `backend`
|
||||
@@ -89,15 +95,73 @@ export class CacheManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Read store-specific options from config
|
||||
const storeOptions = CacheManager.parseStoreOptions(store, config, logger);
|
||||
|
||||
return new CacheManager(
|
||||
store,
|
||||
connectionString,
|
||||
options.onError,
|
||||
logger,
|
||||
defaultTtl,
|
||||
storeOptions,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse store-specific options from configuration.
|
||||
*
|
||||
* @param store - The cache store type ('redis', 'memcache', or 'memory')
|
||||
* @param config - The configuration service
|
||||
* @param logger - Optional logger for warnings
|
||||
* @returns The parsed store options
|
||||
*/
|
||||
private static parseStoreOptions(
|
||||
store: string,
|
||||
config: RootConfigService,
|
||||
logger?: LoggerService,
|
||||
): CacheStoreOptions | undefined {
|
||||
const storeConfigPath = `backend.cache.${store}`;
|
||||
|
||||
if (store === 'redis' && config.has(storeConfigPath)) {
|
||||
return CacheManager.parseRedisOptions(storeConfigPath, config, logger);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Redis-specific options from configuration.
|
||||
*/
|
||||
private static parseRedisOptions(
|
||||
storeConfigPath: string,
|
||||
config: RootConfigService,
|
||||
logger?: LoggerService,
|
||||
): RedisCacheStoreOptions {
|
||||
const redisOptions: RedisCacheStoreOptions = {};
|
||||
const redisConfig = config.getConfig(storeConfigPath);
|
||||
|
||||
redisOptions.client = redisConfig.getOptional('client');
|
||||
|
||||
if (redisConfig.has('cluster')) {
|
||||
const clusterConfig = redisConfig.getConfig('cluster');
|
||||
|
||||
if (!clusterConfig.has('rootNodes')) {
|
||||
logger?.warn(
|
||||
`Redis cluster config has no 'rootNodes' key, defaulting to non-clustered mode`,
|
||||
);
|
||||
return redisOptions;
|
||||
}
|
||||
|
||||
redisOptions.cluster = {
|
||||
rootNodes: clusterConfig.get('rootNodes'),
|
||||
defaults: clusterConfig.getOptional('defaults'),
|
||||
};
|
||||
}
|
||||
|
||||
return redisOptions;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
store: string,
|
||||
@@ -105,6 +169,7 @@ export class CacheManager {
|
||||
errorHandler: CacheManagerOptions['onError'],
|
||||
logger?: LoggerService,
|
||||
defaultTtl?: number,
|
||||
storeOptions?: CacheStoreOptions,
|
||||
) {
|
||||
if (!this.storeFactories.hasOwnProperty(store)) {
|
||||
throw new Error(`Unknown cache store: ${store}`);
|
||||
@@ -114,6 +179,7 @@ export class CacheManager {
|
||||
this.connection = connectionString;
|
||||
this.errorHandler = errorHandler;
|
||||
this.defaultTtl = defaultTtl;
|
||||
this.storeOptions = storeOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,13 +206,23 @@ export class CacheManager {
|
||||
|
||||
private createRedisStoreFactory(): StoreFactory {
|
||||
const KeyvRedis = require('@keyv/redis').default;
|
||||
const stores: Record<string, typeof KeyvRedis> = {};
|
||||
const { createCluster } = require('@keyv/redis');
|
||||
const stores: Record<string, any> = {};
|
||||
|
||||
return (pluginId, defaultTtl) => {
|
||||
if (!stores[pluginId]) {
|
||||
stores[pluginId] = new KeyvRedis(this.connection, {
|
||||
keyPrefixSeparator: ':',
|
||||
});
|
||||
if (this.storeOptions?.cluster) {
|
||||
// Create a Redis cluster
|
||||
const cluster = createCluster(this.storeOptions?.cluster);
|
||||
stores[pluginId] = new KeyvRedis(cluster);
|
||||
} else {
|
||||
// Create a regular Redis connection
|
||||
const redisOptions = this.storeOptions?.client || {
|
||||
keyPrefixSeparator: ':',
|
||||
};
|
||||
stores[pluginId] = new KeyvRedis(this.connection, redisOptions);
|
||||
}
|
||||
|
||||
// Always provide an error handler to avoid stopping the process
|
||||
stores[pluginId].on('error', (err: Error) => {
|
||||
this.logger?.error('Failed to create redis cache client', err);
|
||||
|
||||
@@ -16,6 +16,24 @@
|
||||
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { HumanDuration, durationToMilliseconds } from '@backstage/types';
|
||||
import { RedisClusterOptions, RedisClientOptions } from '@keyv/redis';
|
||||
|
||||
/**
|
||||
* Options for Redis cache store.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RedisCacheStoreOptions = {
|
||||
client?: RedisClientOptions;
|
||||
cluster?: RedisClusterOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Union type of all cache store options.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type CacheStoreOptions = RedisCacheStoreOptions;
|
||||
|
||||
/**
|
||||
* Options given when constructing a {@link CacheManager}.
|
||||
|
||||
Reference in New Issue
Block a user