Merge pull request #5551 from backstage/iameap/cache
Add a storage-agnostic cache manager to backend-common
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
---
|
||||
'@backstage/backend-common': minor
|
||||
---
|
||||
|
||||
Introducing: a standard API for App Integrators to configure cache stores and Plugin Developers to interact with them.
|
||||
|
||||
Two cache stores are currently supported.
|
||||
|
||||
- `memory`, which is a very simple in-memory key/value store, intended for local development.
|
||||
- `memcache`, which can be used to connect to a memcache host.
|
||||
|
||||
Configuring and working with cache stores is very similar to the process for database connections.
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
cache:
|
||||
store: memcache
|
||||
connection: user:pass@cache.example.com:11211
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { CacheManager } from '@backstage/backend-common';
|
||||
|
||||
// Instantiating a cache client for a plugin.
|
||||
const cacheManager = CacheManager.fromConfig(config);
|
||||
const somePluginCache = cacheManager.forPlugin('somePlugin');
|
||||
const cacheClient = somePluginCache.getClient();
|
||||
|
||||
// Using the cache client:
|
||||
const cachedValue = await cacheClient.get('someKey');
|
||||
if (cachedValue) {
|
||||
return cachedValue;
|
||||
} else {
|
||||
const someValue = await someExpensiveProcess();
|
||||
await cacheClient.set('someKey', someValue);
|
||||
}
|
||||
await cacheClient.delete('someKey');
|
||||
```
|
||||
|
||||
Cache clients deal with TTLs in milliseconds. A TTL can be provided as a defaultTtl when getting a client, or may be passed when setting specific objects. If no TTL is provided, data will be persisted indefinitely.
|
||||
|
||||
```typescript
|
||||
// Getting a client with a default TTL
|
||||
const cacheClient = somePluginCache.getClient({
|
||||
defaultTtl: 3600000,
|
||||
});
|
||||
|
||||
// Setting a TTL on a per-object basis.
|
||||
cacheClient.set('someKey', data, { ttl: 3600000 });
|
||||
```
|
||||
|
||||
Configuring a cache store is optional. Even when no cache store is configured, the cache manager will dutifully pass plugins a manager that resolves a cache client that does not actually write or read any data.
|
||||
@@ -167,6 +167,7 @@ mailto
|
||||
maintainership
|
||||
makefile
|
||||
md
|
||||
memcache
|
||||
microsite
|
||||
middleware
|
||||
minikube
|
||||
|
||||
@@ -16,6 +16,7 @@ import { GithubCredentialsProvider } from '@backstage/integration';
|
||||
import { GitHubIntegration } from '@backstage/integration';
|
||||
import { GitLabIntegration } from '@backstage/integration';
|
||||
import * as http from 'http';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import { Knex } from 'knex';
|
||||
import { Logger } from 'winston';
|
||||
import { MergeResult } from 'isomorphic-git';
|
||||
@@ -62,6 +63,19 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface CacheClient {
|
||||
delete(key: string): Promise<void>;
|
||||
get(key: string): Promise<JsonValue | undefined>;
|
||||
set(key: string, value: JsonValue, options?: CacheSetOptions): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class CacheManager {
|
||||
forPlugin(pluginId: string): PluginCacheManager;
|
||||
static fromConfig(config: Config): CacheManager;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const coloredFormat: winston.Logform.Format;
|
||||
|
||||
@@ -238,6 +252,11 @@ export function loadBackendConfig(options: Options): Promise<Config>;
|
||||
// @public
|
||||
export function notFoundHandler(): RequestHandler;
|
||||
|
||||
// @public
|
||||
export type PluginCacheManager = {
|
||||
getClient: (options?: ClientOptions) => CacheClient;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface PluginDatabaseManager {
|
||||
getClient(): Promise<Knex>;
|
||||
|
||||
Vendored
+14
@@ -68,6 +68,20 @@ export interface Config {
|
||||
connection: string | object;
|
||||
};
|
||||
|
||||
/** Cache connection configuration, select cache type using the `store` field */
|
||||
cache?:
|
||||
| {
|
||||
store: 'memory';
|
||||
}
|
||||
| {
|
||||
store: 'memcache';
|
||||
/**
|
||||
* A memcache connection string in the form `user:pass@host:port`.
|
||||
* @secret
|
||||
*/
|
||||
connection: string;
|
||||
};
|
||||
|
||||
cors?: {
|
||||
origin?: string | string[];
|
||||
methods?: string | string[];
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
"git-url-parse": "^11.4.4",
|
||||
"helmet": "^4.0.0",
|
||||
"isomorphic-git": "^1.8.0",
|
||||
"keyv": "^4.0.3",
|
||||
"keyv-memcache": "^1.2.5",
|
||||
"knex": "^0.95.1",
|
||||
"lodash": "^4.17.15",
|
||||
"logform": "^2.1.1",
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DefaultCacheClient } from './CacheClient';
|
||||
import Keyv from 'keyv';
|
||||
|
||||
describe('CacheClient', () => {
|
||||
let client: Keyv;
|
||||
const b64 = (k: string) => Buffer.from(k).toString('base64');
|
||||
|
||||
beforeEach(() => {
|
||||
client = new Keyv();
|
||||
client.get = jest.fn();
|
||||
client.set = jest.fn();
|
||||
client.delete = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('CacheClient.get', () => {
|
||||
it('calls client with normalized key', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'somekey';
|
||||
|
||||
await sut.get(key);
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith(b64(key));
|
||||
});
|
||||
|
||||
it('calls client with normalized key (very long key)', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'x'.repeat(251);
|
||||
|
||||
await sut.get(key);
|
||||
|
||||
const spy = client.get as jest.Mock;
|
||||
const actualKey = spy.mock.calls[0][0];
|
||||
expect(actualKey).not.toEqual(b64(key));
|
||||
expect(actualKey.length).toBeLessThan(250);
|
||||
});
|
||||
|
||||
it('rejects on underlying error', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedError = new Error('Some runtime error');
|
||||
client.get = jest.fn().mockRejectedValue(expectedError);
|
||||
|
||||
return expect(sut.get('someKey')).rejects.toEqual(expectedError);
|
||||
});
|
||||
|
||||
it('resolves what underlying client resolves', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedValue = 'some value';
|
||||
client.get = jest.fn().mockResolvedValue(expectedValue);
|
||||
|
||||
const actualValue = await sut.get('someKey');
|
||||
|
||||
return expect(actualValue).toEqual(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheClient.set', () => {
|
||||
it('calls client with normalized key', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'somekey';
|
||||
|
||||
await sut.set(key, {});
|
||||
|
||||
const spy = client.set as jest.Mock;
|
||||
const actualKey = spy.mock.calls[0][0];
|
||||
expect(actualKey).toEqual(b64(key));
|
||||
});
|
||||
|
||||
it('passes ttl to client when given', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedTtl = 3600;
|
||||
|
||||
await sut.set('someKey', {}, { ttl: expectedTtl });
|
||||
|
||||
const spy = client.set as jest.Mock;
|
||||
const actualTtl = spy.mock.calls[0][2];
|
||||
expect(actualTtl).toEqual(expectedTtl);
|
||||
});
|
||||
|
||||
it('rejects on underlying error if configured', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedError = new Error('Some runtime error');
|
||||
client.set = jest.fn().mockRejectedValue(expectedError);
|
||||
|
||||
return expect(sut.set('someKey', {})).rejects.toEqual(expectedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheClient.delete', () => {
|
||||
it('calls client with normalized key', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'somekey';
|
||||
|
||||
await sut.delete(key);
|
||||
|
||||
const spy = client.delete as jest.Mock;
|
||||
const actualKey = spy.mock.calls[0][0];
|
||||
expect(actualKey).toEqual(b64(key));
|
||||
});
|
||||
|
||||
it('rejects on underlying error if configured', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedError = new Error('Some runtime error');
|
||||
client.delete = jest.fn().mockRejectedValue(expectedError);
|
||||
|
||||
return expect(sut.delete('someKey')).rejects.toEqual(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import { createHash } from 'crypto';
|
||||
import Keyv from 'keyv';
|
||||
|
||||
type CacheClientArgs = {
|
||||
client: Keyv;
|
||||
};
|
||||
|
||||
type CacheSetOptions = {
|
||||
/**
|
||||
* Optional TTL in milliseconds. Defaults to the TTL provided when the client
|
||||
* was set up (or no TTL if none are provided).
|
||||
*/
|
||||
ttl?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A pre-configured, storage agnostic cache client suitable for use by
|
||||
* Backstage plugins.
|
||||
*/
|
||||
export interface CacheClient {
|
||||
/**
|
||||
* Reads data from a cache store for the given key. If no data was found,
|
||||
* returns undefined.
|
||||
*/
|
||||
get(key: string): Promise<JsonValue | undefined>;
|
||||
|
||||
/**
|
||||
* Writes the given data to a cache store, associated with the given key. An
|
||||
* optional TTL may also be provided, otherwise it defaults to the TTL that
|
||||
* was provided when the client was instantiated.
|
||||
*/
|
||||
set(key: string, value: JsonValue, options?: CacheSetOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Removes the given key from the cache store.
|
||||
*/
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic, concrete implementation of the CacheClient, suitable for almost
|
||||
* all uses in Backstage.
|
||||
*/
|
||||
export class DefaultCacheClient implements CacheClient {
|
||||
private readonly client: Keyv;
|
||||
|
||||
constructor({ client }: CacheClientArgs) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async get(key: string): Promise<JsonValue | undefined> {
|
||||
const k = this.getNormalizedKey(key);
|
||||
return await this.client.get(k);
|
||||
}
|
||||
|
||||
async set(
|
||||
key: string,
|
||||
value: JsonValue,
|
||||
opts: CacheSetOptions = {},
|
||||
): Promise<void> {
|
||||
const k = this.getNormalizedKey(key);
|
||||
await this.client.set(k, value, opts.ttl);
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
const k = this.getNormalizedKey(key);
|
||||
await this.client.delete(k);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures keys are well-formed for any/all cache stores.
|
||||
*/
|
||||
private getNormalizedKey(candidateKey: string): string {
|
||||
// Remove potentially invalid characters.
|
||||
const wellFormedKey = Buffer.from(candidateKey).toString('base64');
|
||||
|
||||
// Memcache in particular doesn't do well with keys > 250 bytes.
|
||||
if (wellFormedKey.length < 250) {
|
||||
return wellFormedKey;
|
||||
}
|
||||
|
||||
return createHash('md5').update(candidateKey).digest('base64');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import Keyv from 'keyv';
|
||||
/* @ts-expect-error */
|
||||
import KeyvMemcache from 'keyv-memcache';
|
||||
import { DefaultCacheClient } from './CacheClient';
|
||||
import { CacheManager } from './CacheManager';
|
||||
import { NoStore } from './NoStore';
|
||||
|
||||
jest.createMockFromModule('keyv');
|
||||
jest.mock('keyv');
|
||||
jest.createMockFromModule('keyv-memcache');
|
||||
jest.mock('keyv-memcache');
|
||||
jest.mock('./CacheClient', () => {
|
||||
return {
|
||||
DefaultCacheClient: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('CacheManager', () => {
|
||||
const defaultConfigOptions = {
|
||||
backend: {
|
||||
cache: {
|
||||
store: 'memory',
|
||||
},
|
||||
},
|
||||
};
|
||||
const defaultConfig = () => new ConfigReader(defaultConfigOptions);
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('CacheManager.fromConfig', () => {
|
||||
it('accesses the backend.cache key', () => {
|
||||
const getOptionalString = jest.fn();
|
||||
const config = defaultConfig();
|
||||
config.getOptionalString = getOptionalString;
|
||||
|
||||
CacheManager.fromConfig(config);
|
||||
|
||||
expect(getOptionalString.mock.calls[0][0]).toEqual('backend.cache.store');
|
||||
expect(getOptionalString.mock.calls[1][0]).toEqual(
|
||||
'backend.cache.connection',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not require the backend.cache key', () => {
|
||||
const config = new ConfigReader({ backend: {} });
|
||||
expect(() => {
|
||||
CacheManager.fromConfig(config);
|
||||
}).not.toThrowError();
|
||||
});
|
||||
|
||||
it('throws on unknown cache store', () => {
|
||||
const config = new ConfigReader({
|
||||
backend: { cache: { store: 'notreal' } },
|
||||
});
|
||||
expect(() => {
|
||||
CacheManager.fromConfig(config);
|
||||
}).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheManager.forPlugin', () => {
|
||||
const manager = CacheManager.fromConfig(defaultConfig());
|
||||
|
||||
it('connects to a cache store scoped to the plugin', async () => {
|
||||
const pluginId = 'test1';
|
||||
manager.forPlugin(pluginId).getClient();
|
||||
|
||||
const client = DefaultCacheClient as jest.Mock;
|
||||
expect(client).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('provides different plugins different cache clients', async () => {
|
||||
const plugin1Id = 'test1';
|
||||
const plugin2Id = 'test2';
|
||||
const expectedTtl = 3600;
|
||||
manager.forPlugin(plugin1Id).getClient({ defaultTtl: expectedTtl });
|
||||
manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl });
|
||||
|
||||
const client = DefaultCacheClient as jest.Mock;
|
||||
const cache = (Keyv as unknown) as jest.Mock;
|
||||
expect(cache).toHaveBeenCalledTimes(2);
|
||||
expect(client).toHaveBeenCalledTimes(2);
|
||||
|
||||
const plugin1CallArgs = cache.mock.calls[0];
|
||||
const plugin2CallArgs = cache.mock.calls[1];
|
||||
expect(plugin1CallArgs[0].namespace).not.toEqual(
|
||||
plugin2CallArgs[0].namespace,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheManager.forPlugin stores', () => {
|
||||
it('returns none client when no cache is configured', () => {
|
||||
const manager = CacheManager.fromConfig(
|
||||
new ConfigReader({ backend: {} }),
|
||||
);
|
||||
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].store).toBeInstanceOf(NoStore);
|
||||
});
|
||||
|
||||
it('returns memory client when explicitly configured', () => {
|
||||
const manager = CacheManager.fromConfig(defaultConfig());
|
||||
const expectedTtl = 3600;
|
||||
const expectedNamespace = 'test-plugin';
|
||||
manager
|
||||
.forPlugin(expectedNamespace)
|
||||
.getClient({ defaultTtl: expectedTtl });
|
||||
|
||||
const cache = (Keyv as unknown) as jest.Mock;
|
||||
const mockCalls = cache.mock.calls.splice(-1);
|
||||
const callArgs = mockCalls[0];
|
||||
expect(callArgs[0]).toMatchObject({
|
||||
ttl: expectedTtl,
|
||||
namespace: expectedNamespace,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a memcache client when configured', () => {
|
||||
const expectedHost = '127.0.0.1:11211';
|
||||
const manager = CacheManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
cache: {
|
||||
store: 'memcache',
|
||||
connection: expectedHost,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const expectedTtl = 3600;
|
||||
manager.forPlugin('test').getClient({ defaultTtl: expectedTtl });
|
||||
|
||||
const cache = (Keyv as unknown) as jest.Mock;
|
||||
const mockCacheCalls = cache.mock.calls.splice(-1);
|
||||
expect(mockCacheCalls[0][0]).toMatchObject({
|
||||
ttl: expectedTtl,
|
||||
});
|
||||
expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvMemcache);
|
||||
const memcache = KeyvMemcache as jest.Mock;
|
||||
const mockMemcacheCalls = memcache.mock.calls.splice(-1);
|
||||
expect(mockMemcacheCalls[0][0]).toEqual(expectedHost);
|
||||
});
|
||||
});
|
||||
});
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import Keyv from 'keyv';
|
||||
// @ts-expect-error
|
||||
import KeyvMemcache from 'keyv-memcache';
|
||||
import { DefaultCacheClient, CacheClient } from './CacheClient';
|
||||
import { NoStore } from './NoStore';
|
||||
import { PluginCacheManager } from './types';
|
||||
|
||||
/**
|
||||
* Implements a Cache Manager which will automatically create new cache clients
|
||||
* for plugins when requested. All requested cache clients are created with the
|
||||
* connection details provided.
|
||||
*/
|
||||
export class CacheManager {
|
||||
/**
|
||||
* Keys represent supported `backend.cache.store` values, mapped to factories
|
||||
* that return Keyv instances appropriate to the store.
|
||||
*/
|
||||
private readonly storeFactories = {
|
||||
memcache: this.getMemcacheClient,
|
||||
memory: this.getMemoryClient,
|
||||
none: this.getNoneClient,
|
||||
};
|
||||
|
||||
private readonly store: keyof CacheManager['storeFactories'];
|
||||
private readonly connection: string;
|
||||
|
||||
/**
|
||||
* Creates a new CacheManager instance by reading from the `backend` config
|
||||
* section, specifically the `.cache` key.
|
||||
*
|
||||
* @param config The loaded application configuration.
|
||||
*/
|
||||
static fromConfig(config: Config): 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);
|
||||
}
|
||||
|
||||
private constructor(store: string, connectionString: string) {
|
||||
if (!this.storeFactories.hasOwnProperty(store)) {
|
||||
throw new Error(`Unknown cache store: ${store}`);
|
||||
}
|
||||
this.store = store as keyof CacheManager['storeFactories'];
|
||||
this.connection = connectionString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a CacheManagerInstance for consumption by plugins.
|
||||
*
|
||||
* @param pluginId The plugin that the cache manager should be created for. Plugin names should be unique.
|
||||
*/
|
||||
forPlugin(pluginId: string): PluginCacheManager {
|
||||
return {
|
||||
getClient: (opts = {}): CacheClient => {
|
||||
const concreteClient = this.getClientWithTtl(pluginId, opts.defaultTtl);
|
||||
return new DefaultCacheClient({
|
||||
client: concreteClient,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getClientWithTtl(pluginId: string, ttl: number | undefined): Keyv {
|
||||
return this.storeFactories[this.store].call(this, pluginId, ttl);
|
||||
}
|
||||
|
||||
private getMemcacheClient(
|
||||
pluginId: string,
|
||||
defaultTtl: number | undefined,
|
||||
): Keyv {
|
||||
return new Keyv({
|
||||
namespace: pluginId,
|
||||
ttl: defaultTtl,
|
||||
store: new KeyvMemcache(this.connection),
|
||||
});
|
||||
}
|
||||
|
||||
private getMemoryClient(
|
||||
pluginId: string,
|
||||
defaultTtl: number | undefined,
|
||||
): Keyv {
|
||||
return new Keyv({
|
||||
namespace: pluginId,
|
||||
ttl: defaultTtl,
|
||||
});
|
||||
}
|
||||
|
||||
private getNoneClient(pluginId: string): Keyv {
|
||||
return new Keyv({
|
||||
namespace: pluginId,
|
||||
store: new NoStore(),
|
||||
});
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Storage class compatible with Keyv which always results in a no-op. This is
|
||||
* used when no cache store is configured in a Backstage backend instance.
|
||||
*/
|
||||
export class NoStore extends Map<string, any> {
|
||||
clear(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
delete(_key: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
get(_key: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
has(_key: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
set(_key: string, _value: any): this {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type { CacheClient } from './CacheClient';
|
||||
export { CacheManager } from './CacheManager';
|
||||
export type { PluginCacheManager } from './types';
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CacheClient } from './CacheClient';
|
||||
|
||||
type ClientOptions = {
|
||||
/**
|
||||
* An optional default TTL (in milliseconds) 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* The PluginCacheManager manages access to cache stores that Plugins get.
|
||||
*/
|
||||
export type PluginCacheManager = {
|
||||
/**
|
||||
* getClient provides backend plugins cache connections for itself.
|
||||
*
|
||||
* The purpose of this method is to allow plugins to get isolated data
|
||||
* stores so that plugins are discouraged from cache-level integration
|
||||
* and/or cache key collisions.
|
||||
*/
|
||||
getClient: (options?: ClientOptions) => CacheClient;
|
||||
};
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './cache';
|
||||
export * from './config';
|
||||
export * from './database';
|
||||
export * from './discovery';
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
import Router from 'express-promise-router';
|
||||
import {
|
||||
CacheManager,
|
||||
createServiceBuilder,
|
||||
getRootLogger,
|
||||
loadBackendConfig,
|
||||
@@ -59,11 +60,13 @@ function makeCreateEnv(config: Config) {
|
||||
root.info(`Created UrlReader ${reader}`);
|
||||
|
||||
const databaseManager = SingleConnectionDatabaseManager.fromConfig(config);
|
||||
const cacheManager = CacheManager.fromConfig(config);
|
||||
|
||||
return (plugin: string): PluginEnvironment => {
|
||||
const logger = root.child({ type: 'plugin', plugin });
|
||||
const database = databaseManager.forPlugin(plugin);
|
||||
return { logger, database, config, reader, discovery };
|
||||
const cache = cacheManager.forPlugin(plugin);
|
||||
return { logger, cache, database, config, reader, discovery };
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { Logger } from 'winston';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
PluginCacheManager,
|
||||
PluginDatabaseManager,
|
||||
PluginEndpointDiscovery,
|
||||
UrlReader,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
|
||||
export type PluginEnvironment = {
|
||||
logger: Logger;
|
||||
cache: PluginCacheManager;
|
||||
database: PluginDatabaseManager;
|
||||
config: Config;
|
||||
reader: UrlReader;
|
||||
|
||||
@@ -14428,9 +14428,9 @@ graphql-extensions@^0.12.8:
|
||||
apollo-server-env "^3.0.0"
|
||||
apollo-server-types "^0.6.3"
|
||||
|
||||
graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.8.2:
|
||||
graphql-language-service-interface@^2.8.2:
|
||||
version "2.8.2"
|
||||
resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b"
|
||||
resolved "https://registry.yarnpkg.com/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b"
|
||||
integrity sha512-otbOQmhgkAJU1QJgQkMztNku6SbJLu/uodoFOYOOtJsizTjrMs93vkYaHCcYnLA3oi1Goj27XcHjMnRCYQOZXQ==
|
||||
dependencies:
|
||||
graphql-language-service-parser "^1.9.0"
|
||||
@@ -14438,9 +14438,9 @@ graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.
|
||||
graphql-language-service-utils "^2.5.1"
|
||||
vscode-languageserver-types "^3.15.1"
|
||||
|
||||
graphql-language-service-parser@1.9.0, graphql-language-service-parser@^1.9.0:
|
||||
graphql-language-service-parser@^1.9.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724"
|
||||
resolved "https://registry.yarnpkg.com/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724"
|
||||
integrity sha512-B5xPZLbBmIp0kHvpY1Z35I5DtPoDK9wGxQVRDIzcBaiIvAmlTrDvjo3bu7vKREdjFbYKvWNgrEWENuprMbF17Q==
|
||||
dependencies:
|
||||
graphql-language-service-types "^1.8.0"
|
||||
@@ -16870,7 +16870,7 @@ json-buffer@3.0.0:
|
||||
resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
||||
integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
|
||||
|
||||
json-buffer@3.0.1:
|
||||
json-buffer@3.0.1, json-buffer@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
|
||||
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
|
||||
@@ -17222,6 +17222,14 @@ keytar@^5.4.0:
|
||||
nan "2.14.1"
|
||||
prebuild-install "5.3.3"
|
||||
|
||||
keyv-memcache@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/keyv-memcache/-/keyv-memcache-1.2.5.tgz#9097af5c617dc740e7300ebfd4a2efb8917429e3"
|
||||
integrity sha512-iG+GHlhXyV83gmlCtMFIqNYVvv0i0towAy42NvziUR7D+k9jp81fAq0lu66H4gooOnW+ojkdigRvRpL40j1f7w==
|
||||
dependencies:
|
||||
json-buffer "^3.0.1"
|
||||
memjs "^1.3.0"
|
||||
|
||||
keyv@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
|
||||
@@ -17236,6 +17244,13 @@ keyv@^4.0.0:
|
||||
dependencies:
|
||||
json-buffer "3.0.1"
|
||||
|
||||
keyv@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254"
|
||||
integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==
|
||||
dependencies:
|
||||
json-buffer "3.0.1"
|
||||
|
||||
killable@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
|
||||
@@ -18309,6 +18324,11 @@ media-typer@0.3.0:
|
||||
resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||
|
||||
memjs@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/memjs/-/memjs-1.3.0.tgz#b7959b4ff3770e4c785463fd147f1e4fafd47a24"
|
||||
integrity sha512-y/V9a0auepA9Lgyr4QieK6K2FczjHucEdTpSS+hHVNmVEkYxruXhkHu8n6DSRQ4HXHEE3cc6Sf9f88WCJXGXsQ==
|
||||
|
||||
memoize-one@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
|
||||
@@ -25699,11 +25719,16 @@ typescript-json-schema@^0.49.0:
|
||||
typescript "^4.1.3"
|
||||
yargs "^16.2.0"
|
||||
|
||||
typescript@^4.0.3, typescript@^4.1.3, typescript@~4.1.3:
|
||||
typescript@^4.0.3, typescript@^4.1.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
|
||||
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
|
||||
|
||||
typescript@~4.1.3:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72"
|
||||
integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==
|
||||
|
||||
ua-parser-js@^0.7.18:
|
||||
version "0.7.28"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
|
||||
|
||||
Reference in New Issue
Block a user