Move error handler to CacheManager from Client.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-05-19 10:49:54 +02:00
parent c7dad92187
commit f2d9f5ecb9
4 changed files with 103 additions and 17 deletions
+51 -3
View File
@@ -88,13 +88,12 @@ describe('CacheManager', () => {
it('attaches error handler to client', () => {
const pluginId = 'error-test';
const handler = jest.fn();
manager.forPlugin(pluginId).getClient({ onError: handler });
manager.forPlugin(pluginId).getClient();
const client = DefaultCacheClient as jest.Mock;
const mockCalls = client.mock.calls.splice(-1);
const realClient = mockCalls[0][0].client as Keyv;
expect(realClient.on).toHaveBeenCalledWith('error', handler);
expect(realClient.on).toHaveBeenCalledWith('error', expect.any(Function));
});
it('provides different plugins different cache clients', async () => {
@@ -174,4 +173,53 @@ describe('CacheManager', () => {
expect(mockMemcacheCalls[0][0]).toEqual(expectedHost);
});
});
describe('connection errors', () => {
it('uses provided logger', () => {
// Set up and inject mock logger.
const mockLogger = { child: jest.fn(), error: jest.fn() };
mockLogger.child.mockImplementation(() => mockLogger as any);
const manager = CacheManager.fromConfig(defaultConfig(), {
logger: mockLogger as any,
});
// Set up a cache client using the configured manager.
manager.forPlugin('error-logger-test').getClient();
// Retrieve the error handler attached to the cache client.
const client = DefaultCacheClient as jest.Mock;
const mockCalls = client.mock.calls.splice(-1);
const realClient = mockCalls[0][0].client as Keyv;
const realOnError = realClient.on as jest.Mock;
const realHandler = realOnError.mock.calls.splice(-1)[0][1];
// Invoke the actual error handler.
const expectedError = new Error('some error');
realHandler(expectedError);
expect(mockLogger.error).toHaveBeenCalledWith(expectedError);
});
it('calls provided handler', () => {
// Set up and inject mock logger.
const mockHandler = jest.fn();
const manager = CacheManager.fromConfig(defaultConfig(), {
onError: mockHandler,
});
// Set up a cache client using the configured manager.
manager.forPlugin('error-handler-test').getClient();
// Retrieve the error handler attached to the cache client.
const client = DefaultCacheClient as jest.Mock;
const mockCalls = client.mock.calls.splice(-1);
const realClient = mockCalls[0][0].client as Keyv;
const realOnError = realClient.on as jest.Mock;
const realHandler = realOnError.mock.calls.splice(-1)[0][1];
// Invoke the actual error handler.
const expectedError = new Error('some error');
realHandler(expectedError);
expect(mockHandler).toHaveBeenCalledWith(expectedError);
});
});
});
+35 -8
View File
@@ -18,9 +18,15 @@ import { Config } from '@backstage/config';
import Keyv from 'keyv';
// @ts-expect-error
import KeyvMemcache from 'keyv-memcache';
import { Logger } from 'winston';
import { getRootLogger } from '../logging';
import { DefaultCacheClient, CacheClient } from './CacheClient';
import { NoStore } from './NoStore';
import { PluginCacheManager } from './types';
import {
CacheManagerOptions,
OptionalOnError,
PluginCacheManager,
} from './types';
/**
* Implements a Cache Manager which will automatically create new cache clients
@@ -38,8 +44,10 @@ export class CacheManager {
none: this.getNoneClient,
};
private readonly logger: Logger;
private readonly store: keyof CacheManager['storeFactories'];
private readonly connection: string;
private readonly errorHandler: OptionalOnError;
/**
* Creates a new CacheManager instance by reading from the `backend` config
@@ -47,21 +55,34 @@ export class CacheManager {
*
* @param config The loaded application configuration.
*/
static fromConfig(config: Config): CacheManager {
static fromConfig(
config: Config,
options: CacheManagerOptions = {},
): CacheManager {
// If no `backend.cache` config is provided, instantiate the CacheManager
// with a "NoStore" cache client.
const store = config.getOptionalString('backend.cache.store') || 'none';
const connectionString =
config.getOptionalString('backend.cache.connection') || '';
return new CacheManager(store, connectionString);
const logger = (options.logger || getRootLogger()).child({
type: 'cacheManager',
});
return new CacheManager(store, connectionString, logger, options.onError);
}
private constructor(store: string, connectionString: string) {
private constructor(
store: string,
connectionString: string,
logger: Logger,
errorHandler: OptionalOnError,
) {
if (!this.storeFactories.hasOwnProperty(store)) {
throw new Error(`Unknown cache store: ${store}`);
}
this.logger = logger;
this.store = store as keyof CacheManager['storeFactories'];
this.connection = connectionString;
this.errorHandler = errorHandler;
}
/**
@@ -74,10 +95,16 @@ export class CacheManager {
getClient: (opts = {}): CacheClient => {
const concreteClient = this.getClientWithTtl(pluginId, opts.defaultTtl);
// Attach error handler if provided.
if (typeof opts?.onError === 'function') {
concreteClient.on('error', opts.onError);
}
// Always provide an error handler to avoid killing the process.
concreteClient.on('error', (err: Error) => {
// In all cases, just log the error.
this.logger.error(err);
// Invoke any custom error handler if provided.
if (typeof this.errorHandler === 'function') {
this.errorHandler(err);
}
});
return new DefaultCacheClient({
client: concreteClient,
+11 -1
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { Logger } from 'winston';
import { CacheClient } from './CacheClient';
type ClientOptions = {
@@ -23,12 +24,21 @@ type ClientOptions = {
* can be configured per entry at set-time).
*/
defaultTtl?: number;
};
export type OptionalOnError = ((err: Error) => void) | undefined;
export type CacheManagerOptions = {
/**
* An optional logger for use by the PluginCacheManager.
*/
logger?: Logger;
/**
* An optional handler for connection errors emitted from the underlying data
* store.
*/
onError?: (err: Error) => void;
onError?: OptionalOnError;
};
/**