This repository has been archived by the owner on Feb 19, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(purgecache): consolidate logic for purgeCache to accomodate urls…
… or entire cache
- Loading branch information
Showing
6 changed files
with
229 additions
and
131 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
} | ||
); | ||
}); |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters