Skip to content

Commit

Permalink
Merge pull request #41 from agilecontent/release-sprint73
Browse files Browse the repository at this point in the history
Release sprint73
  • Loading branch information
rzamana authored Sep 18, 2017
2 parents ebb208d + 334e9eb commit 255513a
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 7 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

## v5.1.0
- [2017-09-08] *minor* [TM-4508](https://jiralabone.atlassian.net/browse/TM-4508) **Cache Remote Object**

## v5.0.0
- [2017-09-04] *major* [TM-4492](https://jiralabone.atlassian.net/browse/TM-4492) **Remove Cassandra from dependencies**
- [2017-09-04] *patch* [TM-4733](https://jiralabone.atlassian.net/browse/TM-4733) **Add request to dependencies**
Expand Down
30 changes: 30 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* [createCallContext](#createcallcontext)
* [createServiceLocator](#createservicelocator)
* [createFieldSelector](#createfieldselector)
* [createRemoteConfig](#createremoteconfig)
* [cacheRemoteObject](#cacheremoteobject)
* [Time services](#time-services)
* [createFixedTimeService](#createfixedtimeservice)
* [createCurrentTimeService](#createcurrenttimeservice)
Expand Down Expand Up @@ -248,6 +250,34 @@ This function accepts these parameters:

The config loader has a function `getConfigObject` that only receives a [context](#createcallcontext). It returns the config from the cache, but first refreshes the cache if the refresh time has elapsed.


#### cacheRemoteConfig

Creates a remote object loader that provides a object obtained from a JSON in the given URL. It caches the config for a period of time that can be specified, then refreshes it after that time has elapsed since the last refresh the object asynchronous.

```javascript
const tools = require('itaas-nodejs-tools');

let objectUrl = 'http://config.com/my-config.json';
let refreshTimeSeconds = 60;

let objectLoader = tools.cacheRemoteObject(objectUrl, refreshTimeSeconds);

objectLoader.getFresh(context)
.then((remoteObject) => {
console.log('My config: ' + JSON.stringify(remoteObject));
});
```

This function accepts these parameters:

| Parameter | Type | Required | Description | Default value |
|----------------------|---------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `url` | string | Yes | The URL where the object is located | - |
| `refreshTimeSeconds` | integer | No | The amount of seconds before the object cache is considered to be expired and a refresh is needed. If not specified, the config will only be retrieved once and will never be refreshed. | no refresh |

The Cache Object Remote has a function `getFresh` that only receives a [context](#createcallcontext). It returns a freshy object, and update the cache status, another function is `getCached`, that will return the cached object and check if a refresh is needed (if needed, runs the `refreshCache` assyncronous).

----

### Time services
Expand Down
96 changes: 96 additions & 0 deletions lib/cache-remote-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use strict';

const request = require('request');

class CacheRemoteObject {

static create(url, refreshTimeSeconds) {
return new CacheRemoteObject(url, refreshTimeSeconds);
}

constructor(url, refreshTimeSeconds) {
this.url = url;
this.refreshTimeSeconds = (refreshTimeSeconds) ? refreshTimeSeconds : 0;
this.isRefreshing = false;
this.lastRefresh = null;
this.remoteObject = null;
}

nextRefresh(context) {
return this.lastRefresh + (1000 * this.refreshTimeSeconds);
}

isCacheStale(context) {
return this.lastRefresh === null ||
this.nextRefresh(context) < new Date().getTime();
}

getRemoteObject(context) {
return new Promise((resolve, reject) => {
if (!this.url) {
return reject(new Error('Missing Remote Object URL.'));
}
return request(this.url, (error, response, body) => {
if (error) {
context.logger.error({ err: error }, 'Error calling the remote object service');
return reject(`Error calling url ${this.url}.`);
}

if (response.statusCode !== 200) {
context.logger.error(`Error calling the remote object service. HTTP status code: ${response.statusCode}`);
context.logger.info(`HTTP Body: ${body}`);

return reject(new Error(`Could not get a valid response from ${this.url}.`));
}

return resolve(JSON.parse(body));
});
});
}

refreshCache(context, forceRefresh) {
if (forceRefresh == undefined) {
forceRefresh = false;
}

// Already refreshing
if (!forceRefresh && this.isRefreshing) {
return Promise.resolve();
}

// It is not the time
if (!forceRefresh && !this.isCacheStale(context)) {
return Promise.resolve();
}

// Ok, lets refresh
this.isRefreshing = true;

return this.getRemoteObject(context)
.then((result) => {
this.remoteObject = result;
this.lastRefresh = new Date().getTime();

this.isRefreshing = false;
return Promise.resolve(result);
})
.catch((err) => {
this.isRefreshing = false;
context.logger.error({ err: err }, 'Failed to refresh the remote object');
return Promise.reject(err);
});
}

getCached(context) {
this.refreshCache(context); //async call

return this.remoteObject;
}

getFresh(context) {
return this.refreshCache(context, true);
}

}

module.exports = CacheRemoteObject;
3 changes: 2 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports.createFieldSelector = require('./field-selector').create;
module.exports.createFixedTimeService = require('./time-service/fixed-time-service').create;
module.exports.createCurrentTimeService = require('./time-service/current-time-service').create;
module.exports.createRemoteConfig = require('./remote-config').create;
module.exports.cacheRemoteObject = require('./cache-remote-object').create;

//Helpers
module.exports.number = require('./number-helper');
Expand All @@ -33,4 +34,4 @@ module.exports.cassandra.client = require('./cassandra/cassandra-client-wrap').c
module.exports.cassandra.consistencies = require('./cassandra/cassandra-client-wrap').consistencies;
module.exports.cassandra.createBatchQueryBuilder = require('./cassandra/batch-query-builder').create;
module.exports.cassandra.converter = {};
module.exports.cassandra.converter.map = require('./cassandra/map-converter');
module.exports.cassandra.converter.map = require('./cassandra/map-converter');
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
{
"name": "itaas-nodejs-tools",
"version": "5.0.0",
"version": "5.1.0",
"private": true,
"description": "Node.js tools",
"main": "./lib/index.js",
"bin": {
"license": "./lib/cmd/license-cmd.js"
},
"scripts": {
"test": "mocha --recursive -c test",
"test": "mocha --recursive -c --timeout 10000 test",
"test-debug": "mocha --recursive -c --debug-brk=5858 test",
"lint": "eslint .",
"coverage": "istanbul cover --include-all-sources true node_modules/mocha/bin/_mocha -- --recursive -R dot -c",
"coverage": "istanbul cover --include-all-sources true node_modules/mocha/bin/_mocha -- --timeout 10000 --recursive -R dot -c",
"nodemon": "nodemon --exec \"npm run lint && npm run coverage\"",
"test-unit": "mocha --recursive -c test/unit",
"test-unit": "mocha --timeout 10000 --recursive -c test/unit",
"license": "node lib/cmd/license-cmd.js"
},
"repository": {
Expand Down
192 changes: 192 additions & 0 deletions test/unit/cache-remote-object-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
'use strict';
/* global describe, before, it, beforeEach*/

const nock = require('nock');
const should = require('should');
const CacheRemoteObject = require('../../lib/cache-remote-object');
const uuid = require('uuid').v4;
const tools = require('../../lib/index');
const sleep = require('sleep');

const objectServerUrl = 'http://remote.object';
const objectUrl = 'http://remote.object/remoteobject.json';
const notFoundObjectUrl = 'http://remote.object/notfound.json';
const invalidObjectUrl = 'http://object.invalid/invalid.json';
const objectRefreshTime = 2;

let callId = uuid();
let config = { key: 'value' };
let logger = tools.createLogger();
let serviceLocator = tools.createServiceLocator();
let context = tools.createCallContext(callId, config, logger, serviceLocator);

const remoteObjectValue = {
result: {
key1: 'value1',
key2: 'value2',
key3: 3,
key4: true
}
};

describe('Cache Remote Object', function () {
describe('.create', function () {
it('return an CacheRemoteObject object', () => {
let remoteObject = CacheRemoteObject.create(objectUrl, objectRefreshTime);
remoteObject.should.be.an.instanceOf(CacheRemoteObject);
});
});
describe('.nextRefresh', function () {
it('return correct time for next refresh', () => {
let remoteObject = new CacheRemoteObject(objectUrl, objectRefreshTime);

remoteObject.lastRefresh = new Date().getTime();
should.equal(remoteObject.nextRefresh(context), remoteObject.lastRefresh + (1000 * objectRefreshTime));
});
});

describe('.isCacheStale', function () {
beforeEach(function () {
nock(objectServerUrl)
.get('/remoteobject.json')
.reply(200, remoteObjectValue);
});

it('should return true on first call', () => {
let remoteObject = new CacheRemoteObject(objectUrl, objectRefreshTime);

should.equal(remoteObject.isCacheStale(context), true);
});

it('should return false if cache previously refreshed', (done) => {
let remoteObject = new CacheRemoteObject(objectUrl, objectRefreshTime);

remoteObject.getFresh(context)
.then((result) => {
should.deepEqual(result, remoteObjectValue);
should.equal(remoteObject.isCacheStale(context), false);
done();
}).catch((err) => {
done(err);
});
});

it('should return true if cache previously outdated', (done) => {
let remoteObject = new CacheRemoteObject(objectUrl, objectRefreshTime);

remoteObject.getFresh(context)
.then((result) => {
should.deepEqual(result, remoteObjectValue);
sleep.sleep(objectRefreshTime + 1);
should.ok(remoteObject.isCacheStale(context));
done();
}).catch((err) => {
done(err);
});
});
});

describe('.getCached', function () {
before(function () {
nock(objectServerUrl)
.get('/remoteobject.json')
.reply(200, remoteObjectValue);
});

it('First call should return null.', function () {
let remoteObject = new CacheRemoteObject(objectUrl, objectRefreshTime);

let cached = remoteObject.getCached(context);
should.equal(cached, null);
sleep.sleep(objectRefreshTime + 2);
});
});

describe('.getFresh', function () {
before(function () {
nock(objectServerUrl)
.get('/remoteobject.json')
.times(1)
.reply(200, remoteObjectValue);

nock(objectServerUrl)
.get('/notfound.json')
.reply(404, 'Not Found');
});

it('should get error for empty url', function (done) {
let remoteObject = new CacheRemoteObject(null, objectRefreshTime);

remoteObject.getFresh(context)
.then((result) => {
done(new Error('Missing Remote Object URL.'));
})
.catch((err) => {
should.equal(err.message, 'Missing Remote Object URL.');
done();
})
.catch(done);
});

it('should get error for not found url', function (done) {
let remoteObject = new CacheRemoteObject(notFoundObjectUrl, objectRefreshTime);

remoteObject.getFresh(context)
.then((result) => {
done(new Error('Should not resolve when url not found.'));
})
.catch((err) => {
should.equal(err.message, `Could not get a valid response from ${notFoundObjectUrl}.`);
done();
})
.catch(done);
});

it('should get error for invalid url', function (done) {
let remoteObject = new CacheRemoteObject(invalidObjectUrl, objectRefreshTime);

remoteObject.getFresh(context)
.then((result) => {
done(new Error('Should not resolve when url is invalid.'));
})
.catch((err) => {
should.equal(err, `Error calling url ${invalidObjectUrl}.`);
done();
})
.catch(done);
});

it('should return object : cached and not cached.', function (done) {
let remoteObject = new CacheRemoteObject(objectUrl, objectRefreshTime);

remoteObject.getFresh(context)
.then((result) => {
should.deepEqual(result, remoteObjectValue);
remoteObjectValue.result.key5 = 'newvalue';

let cached = () => {
return remoteObject.getCached(context);
};

let notCached = () => {
nock.cleanAll();
sleep.sleep((objectRefreshTime + 1));
nock(objectServerUrl)
.get('/remoteobject.json')
.reply(200, remoteObjectValue);

return remoteObject.getFresh(context);
};

return Promise.all([cached(), notCached()]);
})
.then((results) => {
should.equal(remoteObjectValue.result.key5, 'newvalue');
should.notDeepEqual(results[0], remoteObjectValue);
should.deepEqual(results[1], remoteObjectValue);
done();
})
.catch(done);
});
});
});
Loading

0 comments on commit 255513a

Please sign in to comment.