Skip to content
This repository has been archived by the owner on Feb 19, 2024. It is now read-only.

Commit

Permalink
feat(purgecache): consolidate logic for purgeCache to accomodate urls…
Browse files Browse the repository at this point in the history
… or entire cache
  • Loading branch information
ostowe committed May 13, 2020
1 parent f0b103d commit 068ff35
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 131 deletions.
3 changes: 1 addition & 2 deletions packages/core/cli/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ const {
const app = express();

// Clearing the Redis cache.
app.get('/purge-cache', purgeCache);
app.purge('/*', purgeEndpointCache);
app.post('/purge-cache', purgeCache);

// Set view engine.
app.set('view engine', 'ejs');
Expand Down
132 changes: 118 additions & 14 deletions packages/core/server/purgeCache.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,135 @@
const get = require('lodash/fp/get');
const queryString = require('query-string');
const cacheService = require('../services/cacheService')();

/**
* Turn a fully-qualified URL into a key for cache purging.
*
* @param {string} url URL to transform
* @returns {string} Path to be used as a redis key.
*/
const createKeyFromUrl = (url) => {
if (! url) {
return false;
}

const path = url.replace(process.env.ROOT_URL, '');
const normalizedPath = [...path].reduce(
(acc, letter, idx) => {
// Normalize to include preceeding slash.
if (0 === idx && '/' !== letter) {
return `/${acc}`;
}

// Remove trailing slash to prevent mismatched keys
if (
1 !== path.length &&
path.length - 1 === idx &&
'/' === letter
) {
return acc;
}

return `${acc}${letter}`;
},
''
);

// Return exact match if path is "/".
if ('/' === normalizedPath) {
const endpoint = queryString.stringify(
{
path: normalizedPath,
context: 'site',
},
{
encode: false,
sort: false,
}
);

return `components-endpoint:${endpoint}`;
}

const endpoint = queryString.stringify(
{ path: normalizedPath },
{ encode: false }
);

return `components-endpoint:${endpoint}*`;
};

/**
* Bust the entire cache from Redis.
*
* @param {string} key Base path to use for purging keys.
* @returns {Promise}
*/
const executeStream = async (key = '') => {
const stream = await cacheService.client.scanStream({
match: key,
});

return new Promise((resolve) => {
let keysDeleted = 0;

stream.on('data', async (keys) => {
// `keys` is an array of strings representing key names
if (keys.length) {
const pipeline = cacheService.client.pipeline();
keysDeleted += keys.length;

keys.forEach((foundKey) => {
pipeline.del(foundKey);
});
pipeline.exec();
}
});

stream.on('end', () => {
const keyMessage = key ? ` for key ${key}` : '';

if (0 === keysDeleted) {
resolve(`No keys matched${keyMessage}`);
}

resolve(`matched ${keysDeleted} keys${keyMessage}`);
});
});
};

/**
* Bust the entire cache or cache for a specific set of URLs from Redis.
*
* @param {object} req Request object.
* @param {object} res Response object.
* @returns {*}
*/
const purgeCache = async (req, res) => {
const urls = get('urls', req.body) || [];

// Create a readable stream (object mode).
// This approach is better for performance.
const stream = await cacheService.client.scanStream();

stream.on('data', async (keys) => {
// `keys` is an array of strings representing key names
if (keys.length) {
const pipeline = cacheService.client.pipeline();
keys.forEach((key) => {
pipeline.del(key);
if (urls.length) {
Promise.all(
urls.map(async (url) => (
executeStream(createKeyFromUrl(url))
))
).then((values) => {
res.write('Cache purge successful: ');

values.forEach((value) => {
res.write(value);
});
pipeline.exec();
} else {
res.json('No cache data to delete.');
}
});

stream.on('end', () => res.json('Entire Redis cache deleted.'));
res.end();
});

return;
}

const response = await executeStream();
res.send(`Cache purge successful: ${response}`);
};

module.exports = purgeCache;
101 changes: 101 additions & 0 deletions packages/core/server/purgeCache.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import waitForExpect from 'wait-for-expect';
import purgeCache from './purgeCache';

jest.mock('../services/cacheService');
require('../services/cacheService')();

const mockRequest = (keys) => ({
body: {
urls: keys,
},
});

const mockResponse = () => {
const res = {};

res.body = '';
res.write = jest.fn().mockImplementation((string) => {
res.body += `${string}`;
});
res.send = jest.fn().mockReturnValue(res);
res.end = jest.fn().mockReturnValue(res.body);

return res;
};

describe('purgeCache', () => {
it(
'should display "No cache to bust" if no matching keys are found',
async () => {
const req = mockRequest(['/no-key-for-this']);
const res = mockResponse();

await purgeCache(req, res);

await waitForExpect(() => {
expect(res.body)
.toBe('Cache purge successful: No keys matched for key components-endpoint:path=/no-key-for-this*');
});
}
);

it(
'should only delete the root if a "/" url is provided',
async () => {
const req = mockRequest(['/']);
const res = mockResponse();

await purgeCache(req, res);

await waitForExpect(() => {
expect(res.body)
.toBe('Cache purge successful: matched 1 keys for key components-endpoint:path=/&context=site');
});
}
);

it(
'should delete any keys prefixed with or exactly matching the passed url',
async () => {
const req = mockRequest(['/test-page']);
const res = mockResponse();

await purgeCache(req, res);

await waitForExpect(() => {
expect(res.body)
.toBe('Cache purge successful: matched 3 keys for key components-endpoint:path=/test-page*');
});
}
);

it(
'should delete mutliple keys for a passed array of URLs',
async () => {
const req = mockRequest(['/test-article', '/test-term']);
const res = mockResponse();

await purgeCache(req, res);

await waitForExpect(() => {
expect(res.body)
.toBe('Cache purge successful: matched 2 keys for key components-endpoint:path=/test-article*matched 2 keys for key components-endpoint:path=/test-term*');
});
}
);

it(
'should purge entire cache if no URLs provided',
async () => {
const req = mockRequest();
const res = mockResponse();

await purgeCache(req, res);

await waitForExpect(() => {
expect(res.send)
.toHaveBeenCalledWith('Cache purge successful: matched 1 keys');
});
}
);
});
49 changes: 0 additions & 49 deletions packages/core/server/purgeEndpointCache.js

This file was deleted.

62 changes: 0 additions & 62 deletions packages/core/server/purgeEndpointCache.test.js

This file was deleted.

13 changes: 9 additions & 4 deletions packages/core/services/__mocks__/cacheService.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
*/
/* eslint-disable max-len */
const mockRedisDatabase = {
'components-endpoint:path=/&context=site': 'some data',
'components-endpoint:path=/test-page&context=site&extra-parameter=1': 'more data',
'components-endpoint:path=/test-page&context=site&extra-parameter=2': 'more data',
'components-endpoint:path=/test-page&context=site&extra-parameter=3': 'more data',
'components-endpoint:path=/&context=site': 'data',
'components-endpoint:path=/test-page&context=site': 'data',
'components-endpoint:path=/test-page&context=site&extra-parameter=2': 'data',
'components-endpoint:path=/test-page&context=site&extra-parameter=3': 'data',
'components-endpoint:path=/test-article&context=site': 'data',
'components-endpoint:path=/test-article/&context=site': 'data',
'components-endpoint:path=/test-term&context=site': 'data',
'components-endpoint:path=/test-term&context=site&another-param': 'data',
'components-endpoint:path=/test-test-test': 'data',
};
/* eslint-enable max-len */

Expand Down

0 comments on commit 068ff35

Please sign in to comment.