diff --git a/api-server/server/models/donation.js b/api-server/server/models/donation.js index 9eee629f8c..6b8db5ea89 100644 --- a/api-server/server/models/donation.js +++ b/api-server/server/models/donation.js @@ -1,6 +1,7 @@ import { Observable } from 'rx'; import debug from 'debug'; +import { reportError } from '../middlewares/error-reporter'; import InMemoryCache from '../utils/in-memory-cache'; const log = debug('fcc:boot:donate'); @@ -9,7 +10,7 @@ const fiveMinutes = 1000 * 60 * 5; export default function(Donation) { let activeDonationUpdateInterval = null; const activeDonationCountCacheTTL = fiveMinutes; - const activeDonationCountCache = InMemoryCache(0); + const activeDonationCountCache = InMemoryCache(0, reportError); const activeDonationsQuery$ = () => Donation.find$({ // eslint-disable-next-line no-undefined @@ -23,25 +24,50 @@ export default function(Donation) { } process.on('exit', cleanUp); + Donation.on('dataSourceAttached', () => { Donation.find$ = Observable.fromNodeCallback(Donation.find.bind(Donation)); Donation.findOne$ = Observable.fromNodeCallback( Donation.findOne.bind(Donation) ); - activeDonationsQuery$().subscribe(count => { - log('activeDonator count: %d', count); - return activeDonationCountCache.update(() => count); - }); + seedTheCache() + .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( () => - activeDonationsQuery$().subscribe(count => { - log('activeDonator count: %d', count); - return activeDonationCountCache.update(() => count); - }), + Observable.defer(activeDonationsQuery$).subscribe( + count => { + log('activeDonator count: %d', count); + return activeDonationCountCache.update(() => count); + }, + err => { + const errMsg = `Error caught updating the cache: ${err.message}`; + err.message = errMsg; + reportError(err); + } + ), activeDonationCountCacheTTL ); - }); + return null; + } function getCurrentActiveDonationCount$() { return Observable.of(activeDonationCountCache.get()); diff --git a/api-server/server/utils/in-memory-cache.js b/api-server/server/utils/in-memory-cache.js index 3239c88649..4fc280c85d 100644 --- a/api-server/server/utils/in-memory-cache.js +++ b/api-server/server/utils/in-memory-cache.js @@ -2,21 +2,32 @@ function isPromiseLike(thing) { 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 cache = new Map(); - - if (typeof initialValue !== 'undefined') { - cache.set(cacheKey, initialValue); - } + cache.set(cacheKey, initialValue); return { get() { const value = cache.get(cacheKey); return typeof value !== 'undefined' ? value : null; }, + 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)) { return maybePromisedValue.then(value => cache.set(cacheKey, value)); } else { @@ -25,6 +36,7 @@ function InMemoryCache(initialValue) { return null; } }, + clear() { return cache.delete(cacheKey); } diff --git a/api-server/server/utils/in-memory-cache.test.js b/api-server/server/utils/in-memory-cache.test.js index 443e96a222..c0954f5f77 100644 --- a/api-server/server/utils/in-memory-cache.test.js +++ b/api-server/server/utils/in-memory-cache.test.js @@ -1,33 +1,40 @@ -/* global describe expect it beforeEach */ +/* global describe expect it */ import inMemoryCache from './in-memory-cache'; +import sinon from 'sinon'; describe('InMemoryCache', () => { + let reportErrorStub; const theAnswer = 42; const before = 'before'; const after = 'after'; const emptyCacheValue = null; - describe('get', () => { - it('returns null for an empty cache', () => { - const cache = inMemoryCache(); - expect(cache.get()).toBe(emptyCacheValue); - }); + beforeEach(() => { + reportErrorStub = sinon.spy(); + }); + 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', () => { - const cache = inMemoryCache(theAnswer); + const cache = inMemoryCache(theAnswer, reportErrorStub); expect(cache.get()).toBe(theAnswer); }); }); describe('update', () => { it('updates the cached value', () => { - const cache = inMemoryCache(before); + const cache = inMemoryCache(before, reportErrorStub); cache.update(() => after); expect(cache.get()).toBe(after); }); it('can handle promises correctly', done => { - const cache = inMemoryCache(before); + const cache = inMemoryCache(before, reportErrorStub); cache.update(() => new Promise(resolve => resolve(after))); // because async setImmediate(() => { @@ -35,12 +42,25 @@ describe('InMemoryCache', () => { 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', () => { it('clears the cache', () => { expect.assertions(2); - const cache = inMemoryCache(theAnswer); + const cache = inMemoryCache(theAnswer, reportErrorStub); expect(cache.get()).toBe(theAnswer); cache.clear(); expect(cache.get()).toBe(emptyCacheValue);