Implement caching via response etags for adr backend

Signed-off-by: Robert Bunning <rbunning@webstaurantstore.com>
This commit is contained in:
Robert Bunning
2022-12-23 16:26:33 -05:00
parent 42ecf54e48
commit 54baecbd1a
4 changed files with 116 additions and 18 deletions
+5 -1
View File
@@ -21,5 +21,9 @@ import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter(env.reader);
return await createRouter({
reader: env.reader,
cacheClient: env.cache.getClient(),
logger: env.logger,
});
}
+29 -1
View File
@@ -15,6 +15,7 @@
*/
import {
CacheClient,
ReadTreeResponse,
ReadTreeResponseFile,
ReadUrlResponse,
@@ -23,6 +24,7 @@ import {
import express from 'express';
import request from 'supertest';
import { createRouter } from './router';
import { Logger } from 'winston';
const listEndpointName = '/list';
const fileEndpointName = '/file';
@@ -90,13 +92,39 @@ const mockUrlReader: UrlReader = {
},
};
class MockCacheClient implements CacheClient {
private itemRegistry: { [key: string]: any };
constructor() {
this.itemRegistry = {};
}
async get(key: string) {
return this.itemRegistry[key];
}
async set(key: string, value: any) {
this.itemRegistry[key] = value;
}
async delete(key: string) {
delete this.itemRegistry[key];
}
}
describe('createRouter', () => {
let app: express.Express;
beforeEach(async () => {
jest.resetAllMocks();
const router = await createRouter(mockUrlReader);
const router = await createRouter({
reader: mockUrlReader,
cacheClient: new MockCacheClient(),
logger: {
error: (message: any) => message,
} as Logger,
});
app = express().use(router);
});
+81 -15
View File
@@ -14,12 +14,24 @@
* limitations under the License.
*/
import { UrlReader } from '@backstage/backend-common';
import { CacheClient, UrlReader } from '@backstage/backend-common';
import { NotModifiedError, stringifyError } from '@backstage/errors';
import { Logger } from 'winston';
import express from 'express';
import Router from 'express-promise-router';
export type AdrRouterOptions = {
reader: UrlReader;
cacheClient: CacheClient;
logger: Logger;
};
/** @public */
export async function createRouter(reader: UrlReader): Promise<express.Router> {
export async function createRouter(
options: AdrRouterOptions,
): Promise<express.Router> {
const { reader, cacheClient, logger } = options;
const router = Router();
router.use(express.json());
@@ -31,17 +43,46 @@ export async function createRouter(reader: UrlReader): Promise<express.Router> {
return;
}
const treeGetResponse = await reader.readTree(urlToProcess);
const files = await treeGetResponse.files();
const fileData = files.map(file => {
return {
type: 'file',
name: file.path.substring(file.path.lastIndexOf('/') + 1),
path: file.path,
};
});
const cachedTree = (await cacheClient.get(urlToProcess)) as {
data: {
type: string;
name: string;
path: string;
}[];
etag: string;
};
const cachedData = cachedTree?.data;
res.json({ data: fileData });
try {
const treeGetResponse = await reader.readTree(urlToProcess, {
etag: cachedTree?.etag,
});
const files = await treeGetResponse.files();
const data = files.map(file => {
return {
type: 'file',
name: file.path.substring(file.path.lastIndexOf('/') + 1),
path: file.path,
};
});
await cacheClient.set(urlToProcess, {
data,
etag: treeGetResponse.etag,
});
res.json({ data });
} catch (error: any) {
if (cachedData && error.name === NotModifiedError.name) {
res.json({ data: cachedData });
return;
}
const message = stringifyError(error);
logger.error(`Unable to fetch ADRs from ${urlToProcess}: ${message}`);
res.statusCode = 500;
res.json({ message });
}
});
router.get('/file', async (req, res) => {
@@ -52,10 +93,35 @@ export async function createRouter(reader: UrlReader): Promise<express.Router> {
return;
}
const fileGetResponse = await reader.readUrl(urlToProcess);
const fileBuffer = await fileGetResponse.buffer();
const cachedFileContent = (await cacheClient.get(urlToProcess)) as {
data: string;
etag: string;
};
res.json({ data: fileBuffer.toString() });
try {
const fileGetResponse = await reader.readUrl(urlToProcess, {
etag: cachedFileContent?.etag,
});
const fileBuffer = await fileGetResponse.buffer();
const data = fileBuffer.toString();
await cacheClient.set(urlToProcess, {
data,
etag: fileGetResponse.etag,
});
res.json({ data });
} catch (error) {
if (cachedFileContent && error.name === NotModifiedError.name) {
res.json({ data: cachedFileContent.data });
return;
}
const message = stringifyError(error);
logger.error(`Unable to fetch ADRs from ${urlToProcess}: ${message}`);
res.statusCode = 500;
res.json({ message });
}
});
return router;
+1 -1
View File
@@ -51,7 +51,7 @@ export class AdrClient implements AdrApi {
const data = await result.json();
if (!result.ok) {
throw new Error(data.error.message);
throw new Error(`${data.message}`);
}
return data;
}