Skip to content

Adding Caching Adapter, allows caching of _Role, _User and _SCHMEA (fixes #168) #1664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions spec/CacheController.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
var CacheController = require('../src/Controllers/CacheController.js').default;

describe('CacheController', function() {
var FakeCacheAdapter;
var FakeAppID = 'foo';
var KEY = 'hello';

beforeEach(() => {
FakeCacheAdapter = {
get: () => Promise.resolve(null),
put: jasmine.createSpy('put'),
del: jasmine.createSpy('del'),
clear: jasmine.createSpy('clear')
}

spyOn(FakeCacheAdapter, 'get').and.callThrough();
});


it('should expose role and user caches', (done) => {
var cache = new CacheController(FakeCacheAdapter, FakeAppID);

expect(cache.role).not.toEqual(null);
expect(cache.role.get).not.toEqual(null);
expect(cache.user).not.toEqual(null);
expect(cache.user.get).not.toEqual(null);

done();
});


['role', 'user'].forEach((cacheName) => {
it('should prefix ' + cacheName + ' cache', () => {
var cache = new CacheController(FakeCacheAdapter, FakeAppID)[cacheName];

cache.put(KEY, 'world');
var firstPut = FakeCacheAdapter.put.calls.first();
expect(firstPut.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));

cache.get(KEY);
var firstGet = FakeCacheAdapter.get.calls.first();
expect(firstGet.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));

cache.del(KEY);
var firstDel = FakeCacheAdapter.del.calls.first();
expect(firstDel.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
});
});

it('should clear the entire cache', () => {
var cache = new CacheController(FakeCacheAdapter, FakeAppID);

cache.clear();
expect(FakeCacheAdapter.clear.calls.count()).toEqual(1);

cache.user.clear();
expect(FakeCacheAdapter.clear.calls.count()).toEqual(2);

cache.role.clear();
expect(FakeCacheAdapter.clear.calls.count()).toEqual(3);
});

it('should handle cache rejections', (done) => {

FakeCacheAdapter.get = () => Promise.reject();

var cache = new CacheController(FakeCacheAdapter, FakeAppID);

cache.get('foo').then(done, () => {
fail('Promise should not be rejected.');
});
});

});
74 changes: 74 additions & 0 deletions spec/InMemoryCache.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const InMemoryCache = require('../src/Adapters/Cache/InMemoryCache').default;


describe('InMemoryCache', function() {
var BASE_TTL = {
ttl: 10
};
var NO_EXPIRE_TTL = {
ttl: NaN
};
var KEY = 'hello';
var KEY_2 = KEY + '_2';

var VALUE = 'world';


function wait(sleep) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, sleep);
})
}

it('should destroy a expire items in the cache', (done) => {
var cache = new InMemoryCache(BASE_TTL);

cache.put(KEY, VALUE);

var value = cache.get(KEY);
expect(value).toEqual(VALUE);

wait(BASE_TTL.ttl * 5).then(() => {
value = cache.get(KEY)
expect(value).toEqual(null);
done();
});
});

it('should delete items', (done) => {
var cache = new InMemoryCache(NO_EXPIRE_TTL);
cache.put(KEY, VALUE);
cache.put(KEY_2, VALUE);
expect(cache.get(KEY)).toEqual(VALUE);
expect(cache.get(KEY_2)).toEqual(VALUE);

cache.del(KEY);
expect(cache.get(KEY)).toEqual(null);
expect(cache.get(KEY_2)).toEqual(VALUE);

cache.del(KEY_2);
expect(cache.get(KEY)).toEqual(null);
expect(cache.get(KEY_2)).toEqual(null);
done();
});

it('should clear all items', (done) => {
var cache = new InMemoryCache(NO_EXPIRE_TTL);
cache.put(KEY, VALUE);
cache.put(KEY_2, VALUE);

expect(cache.get(KEY)).toEqual(VALUE);
expect(cache.get(KEY_2)).toEqual(VALUE);
cache.clear();

expect(cache.get(KEY)).toEqual(null);
expect(cache.get(KEY_2)).toEqual(null);
done();
});

it('should deafult TTL to 5 seconds', () => {
var cache = new InMemoryCache({});
expect(cache.ttl).toEqual(5 * 1000);
});

});
59 changes: 59 additions & 0 deletions spec/InMemoryCacheAdapter.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
var InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').default;

describe('InMemoryCacheAdapter', function() {
var KEY = 'hello';
var VALUE = 'world';

function wait(sleep) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, sleep);
})
}

it('should expose promisifyed methods', (done) => {
var cache = new InMemoryCacheAdapter({
ttl: NaN
});

var noop = () => {};

// Verify all methods return promises.
Promise.all([
cache.put(KEY, VALUE),
cache.del(KEY),
cache.get(KEY),
cache.clear()
]).then(() => {
done();
});
});

it('should get/set/clear', (done) => {
var cache = new InMemoryCacheAdapter({
ttl: NaN
});

cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(VALUE))
.then(() => cache.clear())
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(null))
.then(done);
});

it('should expire after ttl', (done) => {
var cache = new InMemoryCacheAdapter({
ttl: 10
});

cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(VALUE))
.then(wait.bind(null, 50))
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(null))
.then(done);
})

});
2 changes: 1 addition & 1 deletion spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const setServerConfiguration = configuration => {
DatabaseAdapter.clearDatabaseSettings();
currentConfiguration = configuration;
server.close();
cache.clearCache();
cache.clear();
app = express();
api = new ParseServer(configuration);
app.use('/1', api);
Expand Down
27 changes: 27 additions & 0 deletions src/Adapters/Cache/CacheAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export class CacheAdapter {
/**
* Get a value in the cache
* @param key Cache key to get
* @return Promise that will eventually resolve to the value in the cache.
*/
get(key) {}

/**
* Set a value in the cache
* @param key Cache key to set
* @param value Value to set the key
* @param ttl Optional TTL
*/
put(key, value, ttl) {}

/**
* Remove a value from the cache.
* @param key Cache key to remove
*/
del(key) {}

/**
* Empty a cache
*/
clear() {}
}
66 changes: 66 additions & 0 deletions src/Adapters/Cache/InMemoryCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const DEFAULT_CACHE_TTL = 5 * 1000;


export class InMemoryCache {
constructor({
ttl = DEFAULT_CACHE_TTL
}) {
this.ttl = ttl;
this.cache = Object.create(null);
}

get(key) {
let record = this.cache[key];
if (record == null) {
return null;
}

// Has Record and isnt expired
if (isNaN(record.expire) || record.expire >= Date.now()) {
return record.value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should return a promise to match other functions / or other functions should return something else than promises for consistency

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InMemoryCache isn't actually a CacheAdapter,

there was a cache already in the application which needed to be synchronous (AppCache), I moved that implementation here and added a TTL option to it, so we don't have two cache classes.

the InMemoryCacheAdapter wraps this in promises.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case remove all the return promises. That's odd tot have an interface that has inconsistent return values for related operations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole point are the promises.

There needs to be two caches in a parse-server one for the applications AppCache and one for actually cached data CacheController/Adapter (_Roles/_Session/etc..).

The AppCache needs to be synchronous and cant be used remotely (Caches actual JS Objects not JSON), that is where InMemoryCache comes from, this is so that we don't have two different Cache APIs in parse-server

The promise based architecture is so that users can replace CacheAdapter with remote caching systems, redis/memcached.

The default CacheAdatpter (InMemoryCacheAdapter) is something that just wraps the synchronous InMemoryCache with promises.

}

// Record has expired
delete this.cache[key];
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

}

put(key, value, ttl = this.ttl) {
if (ttl < 0 || isNaN(ttl)) {
ttl = NaN;
}

var record = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we handle value == null as a delete?

value: value,
expire: ttl + Date.now()
}

if (!isNaN(record.expire)) {
record.timeout = setTimeout(() => {
this.del(key);
}, ttl);
}

this.cache[key] = record;
}

del(key) {
var record = this.cache[key];
if (record == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also instead of loose equality id go with !record

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I avoid !record due to the problems with loose equality in js, !0 == true but that is a object that exists.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Record will never be 0 as it's ever wrapped into an object

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general rule I never use !object when checking whether or not a object exists,

obj == null is much more specific around checking if a object exists (undefined or null) and you don't have to worry about the crazy JS equality system.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it isn't more specific as undefined == null and null == undefined. but ok. Don't wanna enter the details

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the rest of the code, we check with ! for those exact cases. Less misleading.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the method retuns a Promise.resolve but in that case...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

obj == null is style choice, and is exactly the same as obj === null || obj === undefined but way less typing..

I will remove the rest of the Promise.resolves from this file.

return;
}

if (record.timeout) {
clearTimeout(record.timeout);
}

delete this.cache[key];
}

clear() {
this.cache = Object.create(null);
}

}

export default InMemoryCache;
36 changes: 36 additions & 0 deletions src/Adapters/Cache/InMemoryCacheAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {InMemoryCache} from './InMemoryCache';

export class InMemoryCacheAdapter {

constructor(ctx) {
this.cache = new InMemoryCache(ctx)
}

get(key) {
return new Promise((resolve, reject) => {
let record = this.cache.get(key);
if (record == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use null for cache miss, let's use strict equality

Copy link
Contributor Author

@blacha blacha May 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more just a guard to stop trying to JSON.parse(undefined)

return resolve(null);
}

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

put(key, value, ttl) {
this.cache.put(key, JSON.stringify(value), ttl);
return Promise.resolve();
}

del(key) {
this.cache.del(key);
return Promise.resolve();
}

clear() {
this.cache.clear();
return Promise.resolve();
}
}

export default InMemoryCacheAdapter;
Loading