Skip to content

Commit

Permalink
Persist samplestore to db (#266)
Browse files Browse the repository at this point in the history
* Enable config of multiple redis instances.

* no message

* no message

* no message

* Provide a utility fn to populate the sampleStore from db.
If the sampleStore index keys are not present, populate the sampleStore from the db on server start.

* no message

* no message

* no message

* no message

* no message

* no message

* no message

* no message

* Per Pallavi’s comments

* no message

* no message

* per Shriram comment

* no message

* Aspects from db (not just from samples)

* set/reset initial feature enablement state

* no message

* Store the lowercase aspect name of each sample in its subject set

* Define env var PERSIST_REDIS_SAMPLE_STORE_MILLISECONDS.
Define a new clock job using PERSIST_REDIS_SAMPLE_STORE_MILLISECONDS. If ENABLE_REDIS_SAMPLE_STORE env var is true, this new job will truncate the db samples table and insert all the samples from the redis sample store into the table.

* no message

* no message

* In the test, after populating redis, delete/change a sample from postgres db, then persist and assert that we get all samples back.
  • Loading branch information
iamigo authored and shriramshankar committed Mar 2, 2017
1 parent 64e340a commit e570d43
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 12 deletions.
55 changes: 55 additions & 0 deletions cache/sampleStorePersist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* 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/sampleStorePersist.js
*
* Functions for saving the redis sample store to the db.
*/
'use strict'; // eslint-disable-line strict
const featureToggles = require('feature-toggles');
const Sample = require('../db').Sample;
const redisClient = require('./redisCache').client.sampleStore;
const samsto = require('./sampleStore');
const constants = samsto.constants;

/**
* Truncate the sample table in the db and persist all the samples from redis
* into the empty table.
*
* @returns {Promise} which resolves to true upon complete if redis sample
* store feature is enabled, or false on error or if feature is disabled.
*/
function persist() {
if (!featureToggles.isFeatureEnabled(constants.featureName)) {
return Promise.resolve(false);
}

return Sample.destroy({ truncate: true, force: true })
.then(() => redisClient.smembersAsync(constants.indexKey.sample))
.then((keys) => keys.map((key) => ['hgetall', key]))
.then((cmds) => redisClient.batch(cmds).execAsync())
.then((res) => {
const samplesToCreate = res.map((sample) => {
sample.relatedLinks = JSON.parse(sample.relatedLinks);
return sample;
});
return Sample.bulkCreate(samplesToCreate);
})
.then(() => console.log('persisted redis sample store to db'))
.then(() => true)
.catch((err) => {
// NO-OP
console.error(err); // eslint-disable-line no-console
Promise.resolve(false);
});
} // persist

module.exports = {
persist,
};
6 changes: 4 additions & 2 deletions clock/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
* inside the main web process.
*/
const conf = require('../config');
const env = conf.environment[conf.nodeEnv];
const sampleTimeoutJob = require('./scheduledJobs/sampleTimeoutJob');
const persistSampleStoreJob = require('./scheduledJobs/persistSampleStoreJob');

/*
* Add all the scheduled work here.
*/
setInterval(sampleTimeoutJob.enqueue, env.checkTimeoutIntervalMillis);
setInterval(sampleTimeoutJob.enqueue, conf.checkTimeoutIntervalMillis);
setInterval(persistSampleStoreJob.enqueue,
conf.persistRedisSampleStoreMilliseconds);
47 changes: 47 additions & 0 deletions clock/scheduledJobs/persistSampleStoreJob.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* 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
*/

/**
* clock/scheduledJobs/persistSampleStoreJob.js
*
* Executes the process to save the redis sample store to postgres db. If
* worker process is enabled, enqueues a job, otherwise just executes directly
* in this process.
*/
const featureToggles = require('feature-toggles');
const sampleStore = require('../../cache/sampleStore');
const sampleStorePersist = require('../../cache/sampleStorePersist');
const jwr = '../../jobQueue/jobWrapper';
const jse = '../../jobQueue/setup';

/**
* Execute the call to check for sample timeouts.
*
* @returns {Promise}
*/
function execute() {
return sampleStorePersist.persist();
} // execute

module.exports = {
enqueue() {
if (featureToggles.isFeatureEnabled(sampleStore.constants.featureName)) {
const jobWrapper = require(jwr); // eslint-disable-line global-require
const jobType =
require(jse).jobType; // eslint-disable-line global-require
jobWrapper.createJob(jobType.PERSIST_SAMPLE_STORE,
{ reqStartTime: Date.now() });
return Promise.resolve(true);
}

// If not using worker process, execute directly;
return execute();
},

execute,
};
17 changes: 7 additions & 10 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const rateWindow = pe.RATE_WINDOW;
const endpointToLimit = pe.ENDPOINT_TO_LIMIT;
const httpMethodToLimit = pe.HTTP_METHOD_TO_LIMIT;

const DEFAULT_PERSIST_REDIS_SAMPLE_STORE_MILLISECONDS = 120000; // 2min

/*
* name of the environment variable containing the read-only
* database names as CSV
Expand Down Expand Up @@ -144,8 +146,6 @@ module.exports = {
// as well to enable database migraton in the environment.
environment: {
build: {
checkTimeoutIntervalMillis: pe.CHECK_TIMEOUT_INTERVAL_MILLIS ||
DEFAULT_CHECK_TIMEOUT_INTERVAL_MILLIS,
dbLogging: false, // console.log | false | ...
dbUrl: defaultDbUrl,
defaultNodePort: defaultPort,
Expand All @@ -156,8 +156,6 @@ module.exports = {
'7265666f637573726f636b7377697468677265656e6f776c7373616e6672616e',
},
development: {
checkTimeoutIntervalMillis: pe.CHECK_TIMEOUT_INTERVAL_MILLIS ||
DEFAULT_CHECK_TIMEOUT_INTERVAL_MILLIS,
dbLogging: false, // console.log | false | ...
dbUrl: defaultDbUrl,
defaultNodePort: defaultPort,
Expand All @@ -172,8 +170,6 @@ module.exports = {
'7265666f637573726f636b7377697468677265656e6f776c7373616e6672616e',
},
production: {
checkTimeoutIntervalMillis: pe.CHECK_TIMEOUT_INTERVAL_MILLIS ||
DEFAULT_CHECK_TIMEOUT_INTERVAL_MILLIS,
dbLogging: false, // console.log | false | ...
dbUrl: pe.DATABASE_URL,
ipWhitelist: iplist,
Expand All @@ -186,8 +182,6 @@ module.exports = {
'7265666f637573726f636b7377697468677265656e6f776c7373616e6672616e',
},
testWhitelistLocalhost: {
checkTimeoutIntervalMillis: pe.CHECK_TIMEOUT_INTERVAL_MILLIS ||
DEFAULT_CHECK_TIMEOUT_INTERVAL_MILLIS,
dbLogging: false, // console.log | false | ...
dbUrl: defaultDbUrl,
defaultNodePort: defaultPort,
Expand All @@ -197,8 +191,6 @@ module.exports = {
'7265666f637573726f636b7377697468677265656e6f776c7373616e6672616e',
},
testBlockAllhosts: {
checkTimeoutIntervalMillis: pe.CHECK_TIMEOUT_INTERVAL_MILLIS ||
DEFAULT_CHECK_TIMEOUT_INTERVAL_MILLIS,
dbLogging: false, // console.log | false | ...
dbUrl: defaultDbUrl,
defaultNodePort: defaultPort,
Expand All @@ -209,13 +201,18 @@ module.exports = {
},
},

checkTimeoutIntervalMillis: pe.CHECK_TIMEOUT_INTERVAL_MILLIS ||
DEFAULT_CHECK_TIMEOUT_INTERVAL_MILLIS,
CACHE_EXPIRY_IN_SECS,
JOB_QUEUE_TTL_SECONDS,
deprioritizeJobsFrom,
endpointToLimit,
httpMethodToLimit,
nodeEnv,
payloadLimit,
persistRedisSampleStoreMilliseconds:
pe.PERSIST_REDIS_SAMPLE_STORE_MILLISECONDS ||
DEFAULT_PERSIST_REDIS_SAMPLE_STORE_MILLISECONDS,
port,
prioritizeJobsFrom,
rateLimit,
Expand Down
1 change: 1 addition & 0 deletions jobQueue/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = {
jobQueue: kue.createQueue(redisOptions),
jobType: {
BULKUPSERTSAMPLES: 'bulkUpsertSamples',
PERSIST_SAMPLE_STORE: 'PERSIST_SAMPLE_STORE',
SAMPLE_TIMEOUT: 'SAMPLE_TIMEOUT',
},
ttlForJobs: conf.JOB_QUEUE_TTL_SECONDS,
Expand Down
136 changes: 136 additions & 0 deletions tests/cache/persist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* 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/persist.js
*/
'use strict'; // eslint-disable-line strict
const sampleStore = require('../../cache/sampleStore');
const samstoinit = require('../../cache/sampleStoreInit');
const samstopersist = require('../../cache/sampleStorePersist');
const redisClient = require('../../cache/redisCache').client.sampleStore;
const featureToggles = require('feature-toggles');
const expect = require('chai').expect;
const tu = require('../testUtils');
const u = require('./utils');
const Sample = tu.db.Sample;
const Aspect = tu.db.Aspect;
const Subject = tu.db.Subject;
const initialFeatureState = featureToggles
.isFeatureEnabled(sampleStore.constants.featureName);

describe('persist sample store back to db', () => {
let a1;
let a2;
let a3;
let s1;
let s2;
let s3;

before((done) => {
tu.toggleOverride(sampleStore.constants.featureName, true);
Aspect.create({
isPublished: true,
name: `${tu.namePrefix}Aspect1`,
timeout: '30s',
valueType: 'NUMERIC',
criticalRange: [0, 1],
relatedLinks: [
{ name: 'Google', value: 'http://www.google.com' },
{ name: 'Yahoo', value: 'http://www.yahoo.com' },
],
})
.then((created) => (a1 = created))
.then(() => Aspect.create({
isPublished: true,
name: `${tu.namePrefix}Aspect2`,
timeout: '10m',
valueType: 'BOOLEAN',
okRange: [10, 100],
}))
.then((created) => (a2 = created))
.then(() => Aspect.create({
isPublished: true,
name: `${tu.namePrefix}Aspect3`,
timeout: '10m',
valueType: 'BOOLEAN',
okRange: [10, 100],
}))
.then((created) => (a3 = created))
.then(() => Subject.create({
isPublished: true,
name: `${tu.namePrefix}Subject1`,
}))
.then((created) => (s1 = created))
.then(() => Subject.create({
isPublished: true,
name: `${tu.namePrefix}Subject2`,
parentId: s1.id,
}))
.then((created) => (s2 = created))
.then(() => Subject.create({
isPublished: true,
name: `${tu.namePrefix}Subject3`,
parentId: s1.id,
}))
.then((created) => (s3 = created))
.then(() => Sample.create({
subjectId: s2.id,
aspectId: a1.id,
value: '0',
relatedLinks: [
{ name: 'Salesforce', value: 'http://www.salesforce.com' },
]
}))
.then(() => Sample.create({
subjectId: s2.id,
aspectId: a2.id,
value: '50',
relatedLinks: [
{ name: 'Salesforce', value: 'http://www.salesforce.com' },
]
}))
.then(() => Sample.create({
subjectId: s3.id,
aspectId: a1.id,
value: '5',
relatedLinks: [
{ name: 'Salesforce', value: 'http://www.salesforce.com' },
]
}))
.then(() => done())
.catch(done);
});

after((done) => {
u.forceDelete(done)
.then(() => redisClient.flushallAsync())
.then(() => tu.toggleOverride(sampleStore.constants.featureName,
initialFeatureState))
.then(() => done())
.catch(done);
});

it('ok', (done) => {
samstoinit.eradicate()
.then(() => samstoinit.populate())
.then(() => redisClient.scardAsync(sampleStore.constants.indexKey.sample))
.then((res) => expect(res).to.eql(3))
.then(() => Sample.destroy({
where: {
name: `${tu.namePrefix}Subject1.${tu.namePrefix}Subject2` +
`|${tu.namePrefix}Aspect1`,
},
}))
.then(() => samstopersist.persist())
.then(() => Sample.findAll())
.then((res) => expect(res.length).to.eql(3))
.then(() => done())
.catch(done);
});
});
42 changes: 42 additions & 0 deletions tests/clock/persistSampleStoreJob.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* 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/clock/persistSampleStoreJob.js
*/
const expect = require('chai').expect;
const j = require('../../clock/scheduledJobs/persistSampleStoreJob');
const tu = require('../testUtils');
const featureToggles = require('feature-toggles');
const sampleStore = require('../../cache/sampleStore');
const initialFeatureState = featureToggles
.isFeatureEnabled(sampleStore.constants.featureName);

describe('persistSampleStoreJob', () => {
before(() => tu.toggleOverride(sampleStore.constants.featureName, false));
after(() => tu.toggleOverride(sampleStore.constants.featureName,
initialFeatureState));
it('ok, feature not enabled', (done) => {
j.execute()
.then((res) => expect(res).to.be.false)
.then(() => done())
.catch(done);
});
});

describe('persistSampleStoreJob', () => {
before(() => tu.toggleOverride(sampleStore.constants.featureName, true));
after(() => tu.toggleOverride(sampleStore.constants.featureName,
initialFeatureState));
it('ok, feature enabled', (done) => {
j.execute()
.then((res) => expect(res).to.be.true)
.then(() => done())
.catch(done);
});
});

0 comments on commit e570d43

Please sign in to comment.