feat: Add error handling for inMemoryCache

This commit is contained in:
Bouncey
2018-12-02 13:38:03 +00:00
committed by mrugesh mohapatra
parent 09cb38aa21
commit 15a9992603
3 changed files with 84 additions and 26 deletions

View File

@ -1,6 +1,7 @@
import { Observable } from 'rx'; import { Observable } from 'rx';
import debug from 'debug'; import debug from 'debug';
import { reportError } from '../middlewares/error-reporter';
import InMemoryCache from '../utils/in-memory-cache'; import InMemoryCache from '../utils/in-memory-cache';
const log = debug('fcc:boot:donate'); const log = debug('fcc:boot:donate');
@ -9,7 +10,7 @@ const fiveMinutes = 1000 * 60 * 5;
export default function(Donation) { export default function(Donation) {
let activeDonationUpdateInterval = null; let activeDonationUpdateInterval = null;
const activeDonationCountCacheTTL = fiveMinutes; const activeDonationCountCacheTTL = fiveMinutes;
const activeDonationCountCache = InMemoryCache(0); const activeDonationCountCache = InMemoryCache(0, reportError);
const activeDonationsQuery$ = () => const activeDonationsQuery$ = () =>
Donation.find$({ Donation.find$({
// eslint-disable-next-line no-undefined // eslint-disable-next-line no-undefined
@ -23,25 +24,50 @@ export default function(Donation) {
} }
process.on('exit', cleanUp); process.on('exit', cleanUp);
Donation.on('dataSourceAttached', () => { Donation.on('dataSourceAttached', () => {
Donation.find$ = Observable.fromNodeCallback(Donation.find.bind(Donation)); Donation.find$ = Observable.fromNodeCallback(Donation.find.bind(Donation));
Donation.findOne$ = Observable.fromNodeCallback( Donation.findOne$ = Observable.fromNodeCallback(
Donation.findOne.bind(Donation) Donation.findOne.bind(Donation)
); );
activeDonationsQuery$().subscribe(count => {
log('activeDonator count: %d', count); seedTheCache()
return activeDonationCountCache.update(() => count); .then(setupCacheUpdateInterval)
.catch(err => {
const errMsg = `Error caught seeding the cache: ${err.message}`;
err.message = errMsg;
reportError(err);
});
}); });
function seedTheCache() {
return new Promise((resolve, reject) =>
Observable.defer(activeDonationsQuery$).subscribe(count => {
log('activeDonator count: %d', count);
activeDonationCountCache.update(() => count);
return resolve();
}, reject)
);
}
function setupCacheUpdateInterval() {
activeDonationUpdateInterval = setInterval( activeDonationUpdateInterval = setInterval(
() => () =>
activeDonationsQuery$().subscribe(count => { Observable.defer(activeDonationsQuery$).subscribe(
count => {
log('activeDonator count: %d', count); log('activeDonator count: %d', count);
return activeDonationCountCache.update(() => count); return activeDonationCountCache.update(() => count);
}), },
err => {
const errMsg = `Error caught updating the cache: ${err.message}`;
err.message = errMsg;
reportError(err);
}
),
activeDonationCountCacheTTL activeDonationCountCacheTTL
); );
}); return null;
}
function getCurrentActiveDonationCount$() { function getCurrentActiveDonationCount$() {
return Observable.of(activeDonationCountCache.get()); return Observable.of(activeDonationCountCache.get());

View File

@ -2,21 +2,32 @@ function isPromiseLike(thing) {
return !!thing && typeof thing.then === 'function'; return !!thing && typeof thing.then === 'function';
} }
function InMemoryCache(initialValue) { function InMemoryCache(initialValue, reportError) {
if (typeof reportError !== 'function') {
throw new Error(
'No reportError function specified for this in-memory-cache'
);
}
const cacheKey = Symbol('cacheKey'); const cacheKey = Symbol('cacheKey');
const cache = new Map(); const cache = new Map();
if (typeof initialValue !== 'undefined') {
cache.set(cacheKey, initialValue); cache.set(cacheKey, initialValue);
}
return { return {
get() { get() {
const value = cache.get(cacheKey); const value = cache.get(cacheKey);
return typeof value !== 'undefined' ? value : null; return typeof value !== 'undefined' ? value : null;
}, },
async update(fn) { async update(fn) {
const maybePromisedValue = fn(); let maybePromisedValue;
try {
maybePromisedValue = fn();
} catch (e) {
const errMsg = `InMemoryCache > update > caught: ${e.message}`;
e.message = errMsg;
reportError(e);
return null;
}
if (isPromiseLike(maybePromisedValue)) { if (isPromiseLike(maybePromisedValue)) {
return maybePromisedValue.then(value => cache.set(cacheKey, value)); return maybePromisedValue.then(value => cache.set(cacheKey, value));
} else { } else {
@ -25,6 +36,7 @@ function InMemoryCache(initialValue) {
return null; return null;
} }
}, },
clear() { clear() {
return cache.delete(cacheKey); return cache.delete(cacheKey);
} }

View File

@ -1,33 +1,40 @@
/* global describe expect it beforeEach */ /* global describe expect it */
import inMemoryCache from './in-memory-cache'; import inMemoryCache from './in-memory-cache';
import sinon from 'sinon';
describe('InMemoryCache', () => { describe('InMemoryCache', () => {
let reportErrorStub;
const theAnswer = 42; const theAnswer = 42;
const before = 'before'; const before = 'before';
const after = 'after'; const after = 'after';
const emptyCacheValue = null; const emptyCacheValue = null;
describe('get', () => { beforeEach(() => {
it('returns null for an empty cache', () => { reportErrorStub = sinon.spy();
const cache = inMemoryCache();
expect(cache.get()).toBe(emptyCacheValue);
}); });
it('throws if no report function is passed as a second argument', () => {
expect(() => inMemoryCache(null)).toThrowError(
'No reportError function specified for this in-memory-cache'
);
});
describe('get', () => {
it('returns an initial value', () => { it('returns an initial value', () => {
const cache = inMemoryCache(theAnswer); const cache = inMemoryCache(theAnswer, reportErrorStub);
expect(cache.get()).toBe(theAnswer); expect(cache.get()).toBe(theAnswer);
}); });
}); });
describe('update', () => { describe('update', () => {
it('updates the cached value', () => { it('updates the cached value', () => {
const cache = inMemoryCache(before); const cache = inMemoryCache(before, reportErrorStub);
cache.update(() => after); cache.update(() => after);
expect(cache.get()).toBe(after); expect(cache.get()).toBe(after);
}); });
it('can handle promises correctly', done => { it('can handle promises correctly', done => {
const cache = inMemoryCache(before); const cache = inMemoryCache(before, reportErrorStub);
cache.update(() => new Promise(resolve => resolve(after))); cache.update(() => new Promise(resolve => resolve(after)));
// because async // because async
setImmediate(() => { setImmediate(() => {
@ -35,12 +42,25 @@ describe('InMemoryCache', () => {
done(); done();
}); });
}); });
it('reports errors thrown from the update function', () => {
const reportErrorStub = sinon.spy();
const cache = inMemoryCache(before, reportErrorStub);
const updateError = new Error('An update error');
const updateThatThrows = () => {
throw updateError;
};
cache.update(updateThatThrows);
expect(reportErrorStub.calledWith(updateError)).toBe(true);
});
}); });
describe('clear', () => { describe('clear', () => {
it('clears the cache', () => { it('clears the cache', () => {
expect.assertions(2); expect.assertions(2);
const cache = inMemoryCache(theAnswer); const cache = inMemoryCache(theAnswer, reportErrorStub);
expect(cache.get()).toBe(theAnswer); expect(cache.get()).toBe(theAnswer);
cache.clear(); cache.clear();
expect(cache.get()).toBe(emptyCacheValue); expect(cache.get()).toBe(emptyCacheValue);