Skip to content

Commit

Permalink
Merge pull request #115 from rafinskipg/master
Browse files Browse the repository at this point in the history
cache for clients and users
  • Loading branch information
rafinskipg authored Aug 23, 2016
2 parents 79ad3a5 + d100b25 commit 066af68
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 28 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ In order to accomplish cache and cache invalidation, phrase models can define so
"cache"
],
"cache": {
"type" : "user",
"type" : "client",
"duration": "5m"
},
"doc": { ... }
Expand All @@ -334,6 +334,7 @@ When a request is made to the `POST` method, the `cache` middleware will hit in,
Cache can be used for:
- Client requests
- User requests
- Anonymous requests

The cache for client request is the most common type of cache, client requests are made with a `client token` (which refers normally to a public resource), or without token at all.

Expand All @@ -348,6 +349,14 @@ Each cached endpoint can have a `duration` (time to live), some examples of avai

_See [parse-duration](https://www.npmjs.com/package/parse-duration) for valid values_

By default the cache system works with the level of security that the token has. That means that:
- A user token in the request will create/invalidate a user cache entry
- A client token in the request will create/invalidate a client cache entry
- No token in the request will create/invalidate an anonymous cache entry

If you want to change the cache levels you can "low" them as you need. For Ex: Setting `type: client` will force a client cache for users.


## Logs

**Composr** is shipped with built-in **bunyan** and **winston** support.
Expand Down
20 changes: 10 additions & 10 deletions src/lib/modules/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ var corbel = require('corbel-js')
var timeParser = require('parse-duration')

var DEFAULT_CACHE_DURATION = '1m'
var CLIENT_CACHE_TYPE = 'client'
// var CLIENT_CACHE_TYPE = 'client'
var USER_CACHE_TYPE = 'user'
var ANONYMOUS_CACHE_TYPE = 'anonymous'

function add (path, verb, authorization, version, data, options) {
var type = options.type || CLIENT_CACHE_TYPE
var key = getKey(path, verb, authorization, version, type)
var key = getKey(path, verb, authorization, version, options.type)
var duration = options.duration || DEFAULT_CACHE_DURATION
var msDuration = timeParser(duration)

logger.debug('[Cache]', 'Adding item to', type, 'cache', key, 'with a duration of: ', msDuration, '(ms)')
logger.debug('[Cache]', 'Adding item to cache', key, 'with a duration of: ', msDuration, '(ms)')

redisConnector.set(key, {
duration: msDuration,
Expand All @@ -25,8 +25,7 @@ function add (path, verb, authorization, version, data, options) {
}

function get (path, verb, authorization, version, options) {
var type = options.type || CLIENT_CACHE_TYPE
var key = getKey(path, verb, authorization, version, type)
var key = getKey(path, verb, authorization, version, options.type)
logger.debug('[Cache]', 'Fetching item from cache', key)

return redisConnector.get(key)
Expand Down Expand Up @@ -54,11 +53,12 @@ function getKey (path, verb, authorization, version, type) {
return identifier + '-' + version + '-' + verb + '-' + path
}

function getIdentifier (authorization, type) {
function getIdentifier (authorization, maybeType) {
var type = maybeType || USER_CACHE_TYPE
var identifier = 'no-token'
var authorizationSanitized = authorization ? authorization.replace('Bearer ', '') : ''

if (authorizationSanitized) {
if (authorizationSanitized && type !== ANONYMOUS_CACHE_TYPE) {
try {
var decoded = corbel.jwt.decode(authorizationSanitized)
if (decoded.userId && type === USER_CACHE_TYPE) {
Expand All @@ -75,13 +75,13 @@ function getIdentifier (authorization, type) {
}

function remove (path, verb, authorization, version, domain, options) {
var key = getKey(path, verb, authorization, version)
var key = getKey(path, verb, authorization, version, options.type)

if (options && options.invalidate) {
options.invalidate.forEach(function (url) {
url = domain + '/' + url + '*'
// Adding the domain is mandatory since urls in the phrase model doesnt know about the domain
var keyWithPattern = getKey(url, verb, authorization, version)
var keyWithPattern = getKey(url, verb, authorization, version, options.type)
invalidateWildcard(keyWithPattern)
})
}
Expand Down
21 changes: 18 additions & 3 deletions src/lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,30 @@ function doCheckCache (routeItem, response, path, authorization) {
}
}

/* function executePhraseOrGetCachedResponse (req, res, next, routeItem) {
var hasCacheMiddleware = routeItem.phrase.json[routeItem.verb].middlewares && routeItem.phrase.json[routeItem.verb].middlewares.indexOf('cache') !== -1
// If not in cache,
// execute phrae
// add to cache
//
// If in cache
// return from cache
// if expiry is near
// execute phrase ONCE
// add to cache with new expiry
//
} */

/**
* [executePhraseById description]
* [executePhrase description]
* @param {[type]} req [description]
* @param {[type]} res [description]
* @param {Function} next [description]
* @param {[type]} routeItem [description]
* @return {[type]} [description]
*/
function executePhraseById (req, res, next, routeItem) {
function executePhrase (req, res, next, routeItem) {
var params = executionMode({
corbelDriver: req.corbelDriver,
req: req,
Expand Down Expand Up @@ -176,7 +191,7 @@ function bindRoutes (server, routeObjects) {
args = args.concat(corbelDriverSetupHook)
args = args.concat(metricsHook)
args = args.concat(function bindRoute (req, res, next) {
executePhraseById(req, res, next, item)
executePhrase(req, res, next, item)
})

server[item.restifyVerb].apply(server, args)
Expand Down
134 changes: 126 additions & 8 deletions test/integration/specs/phrases/cacheInvalidation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
var request = require('supertest')
var chai = require('chai')
var expect = chai.expect
var corbel = require('corbel-js')
var phraseCacheInvalidate = require('../../../fixtures/phrases/phraseCacheInvalidate.json')
var phraseCacheUser = require('../../../fixtures/phrases/phraseCacheUser.json')
var phraseCacheProfile = require('../../../fixtures/phrases/phraseCacheProfile.json')
Expand All @@ -11,9 +12,34 @@ function test (server) {
describe('Cache invalidation between endpoints', function () {
this.timeout(20000)

var cachedValue
var cachedValueAnonymous, cachedValueUser1, cachedValueUser2, cachedValueClient
var userAccessToken, userAccessToken2, clientAccessToken

before(function (done) {
var optUser = {
iss: 1,
aud: 'a',
userId: 'user1',
clientId: '66666'
}

var optUser2 = {
iss: 1,
aud: 'a',
userId: 'user2',
clientId: '66666'
}

var optClient = {
iss: 1,
aud: 'a',
clientId: '66666'
}

userAccessToken = corbel.jwt.generate(optUser, 'asd')
userAccessToken2 = corbel.jwt.generate(optUser2, 'asd')
clientAccessToken = corbel.jwt.generate(optClient, 'asd')

var phrases = [
phraseCacheInvalidate, phraseCacheUser, phraseCacheProfile
]
Expand All @@ -28,26 +54,117 @@ function test (server) {
it('returns an example response', function (done) {
request(server.app)
.get('/cache:domain/cache/user')
.set('Authorization', 'Bearer ' + userAccessToken)
.expect(200)
.end(function (err, response) {
cachedValue = response.body
cachedValueUser1 = response.body
done(err)
})
})

it('requesting again causes the cached response to come back', function (done) {
it('returns an example response for anonymous', function (done) {
request(server.app)
.get('/cache:domain/cache/user')
.expect(200)
.end(function (err, response) {
expect(response.body).to.equals(cachedValue)
cachedValueAnonymous = response.body
done(err)
})
})

it('calling post on the other phrase invalidates the cache', function (done) {
it('returns an example response for client', function (done) {
request(server.app)
.get('/cache:domain/cache/user')
.set('Authorization', 'Bearer ' + clientAccessToken)
.expect(200)
.end(function (err, response) {
cachedValueClient = response.body
done(err)
})
})

it('requesting again causes the cached response to come back for the user', function (done) {
request(server.app)
.get('/cache:domain/cache/user')
.set('Authorization', 'Bearer ' + userAccessToken)
.expect(200)
.end(function (err, response) {
expect(response.body).to.equals(cachedValueUser1)
done(err)
})
})

it('requesting with another user gives different response', function (done) {
request(server.app)
.get('/cache:domain/cache/user')
.set('Authorization', 'Bearer ' + userAccessToken2)
.expect(200)
.end(function (err, response) {
expect(response.body).not.to.equals(cachedValueUser1)
cachedValueUser2 = response.body
done(err)
})
})

it('requesting again causes the cached response to come back for the user2', function (done) {
request(server.app)
.get('/cache:domain/cache/user')
.set('Authorization', 'Bearer ' + userAccessToken2)
.expect(200)
.end(function (err, response) {
expect(response.body).to.equals(cachedValueUser2)
done(err)
})
})

it('requesting again causes the cached response to come back for the client', function (done) {
request(server.app)
.get('/cache:domain/cache/user')
.set('Authorization', 'Bearer ' + clientAccessToken)
.expect(200)
.end(function (err, response) {
expect(response.body).to.equals(cachedValueClient)
done(err)
})
})

it('calling post on the other phrase invalidates the cache (for anonymous user only)', function (done) {
request(server.app)
.post('/cache:domain/cache/invalidateusercache')
.expect(200)
.end(function (err, response) {
expect(response.body).to.be.a('string')
expect(response.body).to.equals('OK')
done(err)
})
})

it('once invalidated the result is new for anonymous', function (done) {
request(server.app)
.get('/cache:domain/cache/user')
.expect(200)
.end(function (err, response) {
expect(response.body).to.be.a('number')
expect(response.body).to.not.equals(cachedValueAnonymous)
done(err)
})
})

it('mantains the user cache ok', function (done) {
request(server.app)
.get('/cache:domain/cache/user')
.set('Authorization', 'Bearer ' + userAccessToken)
.expect(200)
.end(function (err, response) {
expect(response.body).to.equals(cachedValueUser1)
done(err)
})
})

it('calling post on the other phrase invalidates the cache (for the user only)', function (done) {
request(server.app)
.post('/cache:domain/cache/invalidateusercache')
.set('Authorization', 'Bearer ' + userAccessToken)
.expect(200)
.end(function (err, response) {
expect(response.body).to.be.a('string')
Expand All @@ -59,10 +176,11 @@ function test (server) {
it('once invalidated the result is new', function (done) {
request(server.app)
.get('/cache:domain/cache/user')
.set('Authorization', 'Bearer ' + userAccessToken)
.expect(200)
.end(function (err, response) {
expect(response.body).to.be.a('number')
expect(response.body).to.not.equals(cachedValue)
expect(response.body).to.not.equals(cachedValueUser1)
done(err)
})
})
Expand All @@ -85,7 +203,7 @@ function test (server) {
.get('/cache:domain/cache/user?query=pepito')
.expect(200)
.end(function (err, response) {
cachedValue = response.body
cachedValueAnonymous = response.body
done(err)
})
})
Expand All @@ -107,7 +225,7 @@ function test (server) {
.expect(200)
.end(function (err, response) {
expect(response.body).to.be.a('number')
expect(response.body).to.not.equals(cachedValue)
expect(response.body).to.not.equals(cachedValueAnonymous)
done(err)
})
})
Expand Down
17 changes: 11 additions & 6 deletions test/integration/specs/phrases/cachedResponses.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,15 @@ function test (server) {
var optUser = {
iss: 1,
aud: 'a',
userId: 'user1'
userId: 'user1',
clientId: '54313'
}

var optUser2 = {
iss: 1,
aud: 'a',
userId: 'user2'
userId: 'user2',
clientId: '54313'
}

var optClient = {
Expand All @@ -235,22 +237,25 @@ function test (server) {
userAccessToken2 = corbel.jwt.generate(optUser2, 'asd')
clientAccessToken = corbel.jwt.generate(optClient, 'asd')

var promise1 = requestCache(userAccessToken)
requestCache(userAccessToken)
.then(function (response) {
responseUser1 = response
done()
})
})

var promise2 = requestCache(userAccessToken2)
it('Returns the same response for all of them', function (done) {
var promise1 = requestCache(userAccessToken2)
.then(function (response) {
responseUser2 = response
})

var promise3 = requestCache(clientAccessToken)
var promise2 = requestCache(clientAccessToken)
.then(function (response) {
responseClient = response
})

Promise.all([promise1, promise2, promise3])
Promise.all([promise1, promise2])
.then(function () {
expect(responseUser1).to.equals(responseUser2)
expect(responseClient).to.equals(responseUser1)
Expand Down

0 comments on commit 066af68

Please sign in to comment.