diff --git a/cache/models/samples.js b/cache/models/samples.js index 460edc040f..c22fc6c2c1 100644 --- a/cache/models/samples.js +++ b/cache/models/samples.js @@ -512,12 +512,12 @@ module.exports = { */ findSamples(logObject, method, params) { const opts = getOptionsFromReq(params); - const commands = []; const response = []; // get all Samples sorted lexicographically return redisClient.sortAsync(constants.indexKey.sample, 'alpha') .then((allSampKeys) => { + const commands = []; const filteredSampKeys = applyFiltersOnSampKeys(allSampKeys, opts); // add to commands diff --git a/cache/sampleStoreTimeout.js b/cache/sampleStoreTimeout.js new file mode 100644 index 0000000000..ea3130ffe4 --- /dev/null +++ b/cache/sampleStoreTimeout.js @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2017, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * ./cache/sampleStoreTimeout.js + * + * Timeout samples + */ +'use strict'; // eslint-disable-line strict +const sampleStore = require('./sampleStore'); +const redisClient = require('./redisCache').client.sampleStore; +const isTimedOut = require('../db/helpers/sampleUtils').isTimedOut; +const constants = require('../api/v1/constants'); +const ONE = 1; +const TWO = 2; + +module.exports = { + + /** + * Invalidates samples which were last updated before the "timeout" specified + * by the aspect. Get all samples and corresponding aspects. If the sample + * should be timed out, set sample value, status, previous status, status + * changed at and updated at fields. + * @param {Date} now - Date object + * @returns {Promise} - Resolves to the number of evaluated and timed out + * samples + */ + doTimeout(now) { + const curr = now || new Date(); + let numberTimedOut = 0; + let numberEvaluated = 0; + + return new Promise((resolve, reject) => { + redisClient.smembersAsync(sampleStore.constants.indexKey.sample) + .then((allSamples) => { + const commands = []; + const aspectType = sampleStore.constants.objectType.aspect; + + allSamples.forEach((sampKey) => { + const aspectName = sampKey.split('|')[ONE]; + commands.push(['hgetall', sampKey]); // get sample + commands.push( + ['hgetall', sampleStore.toKey(aspectType, aspectName)] + ); + }); + + return redisClient.batch(commands).execAsync(); + }) + .then((redisResponses) => { + const samples = []; + const aspects = []; + const sampCmds = []; + + for (let num = 0; num < redisResponses.length; num += TWO) { + samples.push(redisResponses[num]); + aspects.push(redisResponses[num + ONE]); + } + + for (let num = 0; num < samples.length; num++) { + const samp = samples[num]; + const asp = aspects[num]; + const sampUpdDateTime = new Date(samp.updatedAt); + if (asp && isTimedOut(asp.timeout, curr, sampUpdDateTime)) { + const sampType = sampleStore.constants.objectType.sample; + + const objToUpdate = { + value: constants.statuses.Timeout, + status: constants.statuses.Timeout, + previousStatus: samp.status, + statusChangedAt: new Date().toString(), + updatedAt: new Date().toString(), + }; + sampCmds.push([ + 'hmset', + sampleStore.toKey(sampType, samp.name), + objToUpdate, + ]); + numberTimedOut++; + } + } + + numberEvaluated = samples.length; + return redisClient.batch(sampCmds).execAsync(); + }) + .then(() => resolve({ numberEvaluated, numberTimedOut })) + .catch(reject); + }); + }, +}; diff --git a/clock/scheduledJobs/sampleTimeoutJob.js b/clock/scheduledJobs/sampleTimeoutJob.js index 2b78c4791f..520aed226f 100644 --- a/clock/scheduledJobs/sampleTimeoutJob.js +++ b/clock/scheduledJobs/sampleTimeoutJob.js @@ -14,6 +14,7 @@ */ const featureToggles = require('feature-toggles'); const dbSample = require('../../db/index').Sample; +const sampleStoreTimeout = require('../../cache/sampleStoreTimeout'); /** * Execute the call to check for sample timeouts. @@ -21,6 +22,10 @@ const dbSample = require('../../db/index').Sample; * @returns {Promise} */ function execute() { + if (featureToggles.isFeatureEnabled('enableRedisSampleStore')) { + return sampleStoreTimeout.doTimeout(); + } + return dbSample.doTimeout(); } // execute diff --git a/tests/cache/models/samples/timeout.js b/tests/cache/models/samples/timeout.js new file mode 100644 index 0000000000..7df12e3bc8 --- /dev/null +++ b/tests/cache/models/samples/timeout.js @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2017, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * tests/cache/models/samples/timeout.js + */ +'use strict'; // eslint-disable-line strict + +const tu = require('../../../testUtils'); +const rtu = require('../redisTestUtil'); +const samstoinit = require('../../../../cache/sampleStoreInit'); +const doTimeout = require('../../../../cache/sampleStoreTimeout').doTimeout; +const redisClient = require('../../../../cache/redisCache').client.sampleStore; +const expect = require('chai').expect; +const Sample = tu.db.Sample; +const Aspect = tu.db.Aspect; +const Subject = tu.db.Subject; + +describe(`api::cache::timeout`, () => { + let updatedAt; + const defaultForStatus = 'Timeout'; + const twentyFourhours = 24; + const hundredDays = 100; + const tenSeconds = 10; + const fiveMinutes = 5; + + before(() => tu.toggleOverride('enableRedisSampleStore', true)); + beforeEach((done) => { + Aspect.create({ + isPublished: true, + name: `${tu.namePrefix}OneSecond`, + timeout: '1s', + valueType: 'NUMERIC', + criticalRange: [0, 0], + warningRange: [1, 1], + infoRange: [2, 2], + okRange: [3, 3], + }) + .then(() => Aspect.create({ + isPublished: true, + name: `${tu.namePrefix}TwoMinutes`, + timeout: '2m', + valueType: 'NUMERIC', + criticalRange: [0, 0], + warningRange: [1, 1], + infoRange: [2, 2], + okRange: [3, 3], + })) + .then(() => Aspect.create({ + isPublished: true, + name: `${tu.namePrefix}ThreeHours`, + timeout: '3H', + valueType: 'NUMERIC', + criticalRange: [0, 0], + warningRange: [1, 1], + infoRange: [2, 2], + okRange: [3, 3], + })) + .then(() => Aspect.create({ + isPublished: true, + name: `${tu.namePrefix}NinetyDays`, + timeout: '90D', + valueType: 'NUMERIC', + criticalRange: [0, 0], + warningRange: [1, 1], + infoRange: [2, 2], + okRange: [3, 3], + })) + .then(() => Subject.create({ + isPublished: true, + name: `${tu.namePrefix}Subject`, + })) + .then(() => Sample.bulkUpsertByName([ + { name: `${tu.namePrefix}Subject|${tu.namePrefix}OneSecond`, value: 1 }, + { name: `${tu.namePrefix}Subject|${tu.namePrefix}TwoMinutes`, value: 1 }, + { name: `${tu.namePrefix}Subject|${tu.namePrefix}ThreeHours`, value: 2 }, + { name: `${tu.namePrefix}Subject|${tu.namePrefix}NinetyDays`, value: 3 }, + ])) + .then(() => Sample.findAll({ + attributes: ['name', 'updatedAt'], + where: { + name: { + $ilike: `${tu.namePrefix}Subject|%`, + }, + }, + }) + .each((s) => { + updatedAt = s.updatedAt; + })) + .then(() => samstoinit.eradicate()) + .then(() => samstoinit.init()) + .then(() => done()) + .catch(done); + }); + + afterEach(rtu.forceDelete); + after(() => tu.toggleOverride('enableRedisSampleStore', false)); + + it('simulate 100 days in the future', (done) => { + const mockUpdatedAt = updatedAt; + mockUpdatedAt.setHours(updatedAt.getHours() + + (twentyFourhours * hundredDays)); + doTimeout(mockUpdatedAt) + .then((res) => { + expect(res).to.eql({ numberEvaluated: 4, numberTimedOut: 4 }); + }) + .then(() => redisClient.keysAsync( + `samsto:sample:${tu.namePrefix}Subject|*`.toLowerCase()) + ) + .then((sNames) => { + const commands = []; + sNames.forEach((s) => { + commands.push(['hgetall', s]); + }); + return redisClient.batch(commands).execAsync(); + }) + .then((samples) => { + samples.forEach((s) => { + expect(s.status).to.equal(defaultForStatus); + }); + done(); + }) + .catch(done); + }); + + it('simulate 1 day in the future', (done) => { + const mockUpdatedAt = updatedAt; + mockUpdatedAt.setHours(updatedAt.getHours() + twentyFourhours); + doTimeout(mockUpdatedAt) + .then((res) => { + expect(res).to.eql({ numberEvaluated: 4, numberTimedOut: 3 }); + }) + .then(() => redisClient.keysAsync( + `samsto:sample:${tu.namePrefix}Subject|*`.toLowerCase()) + ) + .then((sNames) => { + const commands = []; + sNames.forEach((s) => { + commands.push(['hgetall', s]); + }); + return redisClient.batch(commands).execAsync(); + }) + .then((samples) => { + samples.forEach((s) => { + switch (s.name) { + case `${tu.namePrefix}Subject|${tu.namePrefix}OneSecond`: + expect(s.status).to.equal(defaultForStatus); + break; + case `${tu.namePrefix}Subject|${tu.namePrefix}TwoMinutes`: + expect(s.status).to.equal(defaultForStatus); + break; + case `${tu.namePrefix}Subject|${tu.namePrefix}ThreeHours`: + expect(s.status).to.equal(defaultForStatus); + break; + case `${tu.namePrefix}Subject|${tu.namePrefix}NinetyDays`: + expect(s.status).to.not.equal(defaultForStatus); + break; + } + }); + }) + .then(() => done()) + .catch(done); + }); + + it('simulate 5 minutes in the future', (done) => { + const mockUpdatedAt = updatedAt; + mockUpdatedAt.setMinutes(updatedAt.getMinutes() + fiveMinutes); + doTimeout(mockUpdatedAt) + .then((res) => { + expect(res).to.eql({ numberEvaluated: 4, numberTimedOut: 2 }); + }) + .then(() => redisClient.keysAsync( + `samsto:sample:${tu.namePrefix}Subject|*`.toLowerCase()) + ) + .then((sNames) => { + const commands = []; + sNames.forEach((s) => { + commands.push(['hgetall', s]); + }); + return redisClient.batch(commands).execAsync(); + }) + .then((samples) => { + samples.forEach((s) => { + switch (s.name) { + case `${tu.namePrefix}Subject|${tu.namePrefix}OneSecond`: + expect(s.status).to.equal(defaultForStatus); + break; + case `${tu.namePrefix}Subject|${tu.namePrefix}TwoMinutes`: + expect(s.status).to.equal(defaultForStatus); + break; + case `${tu.namePrefix}Subject|${tu.namePrefix}ThreeHours`: + expect(s.status).to.not.equal(null); + break; + case `${tu.namePrefix}Subject|${tu.namePrefix}NinetyDays`: + expect(s.status).to.not.equal(null); + break; + } + }); + }) + .then(() => done()) + .catch(done); + }); + + it('simulate 10 seconds in the past', (done) => { + const mockUpdatedAt = updatedAt; + mockUpdatedAt.setSeconds(updatedAt.getSeconds() - tenSeconds); + doTimeout(mockUpdatedAt) + .then((res) => { + expect(res).to.eql({ numberEvaluated: 4, numberTimedOut: 0 }); + }) + .then(() => redisClient.keysAsync( + `samsto:sample:${tu.namePrefix}Subject|*`.toLowerCase()) + ) + .then((sNames) => { + const commands = []; + sNames.forEach((s) => { + commands.push(['hgetall', s]); + }); + return redisClient.batch(commands).execAsync(); + }) + .then((samples) => { + samples.forEach((s) => { + switch (s.name) { + case `${tu.namePrefix}Subject|${tu.namePrefix}OneSecond`: + expect(s.status).to.not.equal(null); + break; + case `${tu.namePrefix}Subject|${tu.namePrefix}TwoMinutes`: + expect(s.status).to.not.equal(null); + break; + case `${tu.namePrefix}Subject|${tu.namePrefix}ThreeHours`: + expect(s.status).to.not.equal(null); + break; + case `${tu.namePrefix}Subject|${tu.namePrefix}NinetyDays`: + expect(s.status).to.not.equal(null); + break; + } + }); + }) + .then(() => done()) + .catch(done); + }); +});