Skip to content

Commit 708a180

Browse files
flovilmartRafael Santos
authored andcommitted
Adds schema caching capabilities (5s by default) (parse-community#2286)
* Adds schema caching capabilities (off by default) * Use InMemoryCacheAdapter * Uses proper adapter to generate a cache * Fix bugs when running disabled cache * nits * nits * Use options object instead of boolean * Imrpove concurrency of loadSchema * Adds testing with SCHEMA_CACHE_ON * Use CacheController instead of generator - Makes caching SchemaCache use a generated prefix - Makes clearing the SchemaCache clear only the cached schema keys - Enable cache by default (ttl 5s)
1 parent 5ec65d7 commit 708a180

14 files changed

+214
-62
lines changed

spec/ParseInstallation.spec.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ let Parse = require('parse/node').Parse;
99
let rest = require('../src/rest');
1010
let request = require("request");
1111

12-
let config = new Config('test');
13-
let database = config.database;
12+
let config;
13+
let database;
1414
let defaultColumns = require('../src/Controllers/SchemaController').defaultColumns;
1515

1616
const installationSchema = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation) };
1717

1818
describe('Installations', () => {
19+
20+
beforeEach(() => {
21+
config = new Config('test');
22+
database = config.database;
23+
});
24+
1925
it_exclude_dbs(['postgres'])('creates an android installation with ids', (done) => {
2026
var installId = '12345678-abcd-abcd-abcd-123456789abc';
2127
var device = 'android';

spec/PointerPermissions.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ var Schema = require('../src/Controllers/SchemaController');
44
var Config = require('../src/Config');
55

66
describe('Pointer Permissions', () => {
7+
8+
beforeEach(() => {
9+
new Config(Parse.applicationId).database.schemaCache.clear();
10+
});
11+
712
it_exclude_dbs(['postgres'])('should work with find', (done) => {
813
let config = new Config(Parse.applicationId);
914
let user = new Parse.User();

spec/RestQuery.spec.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@ var rest = require('../src/rest');
88
var querystring = require('querystring');
99
var request = require('request');
1010

11-
var config = new Config('test');
12-
let database = config.database;
11+
var config;
12+
let database;
1313
var nobody = auth.nobody(config);
1414

1515
describe('rest query', () => {
16+
17+
beforeEach(() => {
18+
config = new Config('test');
19+
database = config.database;
20+
});
21+
1622
it('basic query', (done) => {
1723
rest.create(config, nobody, 'TestObject', {}).then(() => {
1824
return rest.find(config, nobody, 'TestObject', {});

spec/Schema.spec.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ var Config = require('../src/Config');
44
var SchemaController = require('../src/Controllers/SchemaController');
55
var dd = require('deep-diff');
66

7-
var config = new Config('test');
7+
var config;
88

99
var hasAllPODobject = () => {
1010
var obj = new Parse.Object('HasAllPOD');
@@ -20,6 +20,10 @@ var hasAllPODobject = () => {
2020
};
2121

2222
describe('SchemaController', () => {
23+
beforeEach(() => {
24+
config = new Config('test');
25+
});
26+
2327
it('can validate one object', (done) => {
2428
config.database.loadSchema().then((schema) => {
2529
return schema.validateObject('TestObject', {a: 1, b: 'yo', c: false});

spec/helper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ var defaultConfiguration = {
5959
myoauth: {
6060
module: path.resolve(__dirname, "myoauth") // relative path as it's run from src
6161
}
62-
},
62+
}
6363
};
6464

6565
let openConnections = {};

spec/schemas.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ var masterKeyHeaders = {
116116
};
117117

118118
describe('schemas', () => {
119+
120+
beforeEach(() => {
121+
config.database.schemaCache.clear();
122+
});
123+
119124
it('requires the master key to get all schemas', (done) => {
120125
request.get({
121126
url: 'http://localhost:8378/1/schemas',

src/Config.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// mount is the URL for the root of the API; includes http, domain, etc.
44

55
import AppCache from './cache';
6+
import SchemaCache from './Controllers/SchemaCache';
7+
import DatabaseController from './Controllers/DatabaseController';
68

79
function removeTrailingSlash(str) {
810
if (!str) {
@@ -31,7 +33,14 @@ export class Config {
3133
this.fileKey = cacheInfo.fileKey;
3234
this.facebookAppIds = cacheInfo.facebookAppIds;
3335
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
34-
this.database = cacheInfo.databaseController;
36+
37+
// Create a new DatabaseController per request
38+
if (cacheInfo.databaseController) {
39+
const schemaCache = new SchemaCache(cacheInfo.cacheController, cacheInfo.schemaCacheTTL);
40+
this.database = new DatabaseController(cacheInfo.databaseController.adapter, schemaCache);
41+
}
42+
43+
this.schemaCacheTTL = cacheInfo.schemaCacheTTL;
3544

3645
this.serverURL = cacheInfo.serverURL;
3746
this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL);

src/Controllers/CacheController.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ function joinKeys(...keys) {
1313
* eg "Role" or "Session"
1414
*/
1515
export class SubCache {
16-
constructor(prefix, cacheController) {
16+
constructor(prefix, cacheController, ttl) {
1717
this.prefix = prefix;
1818
this.cache = cacheController;
19+
this.ttl = ttl;
1920
}
2021

2122
get(key) {

src/Controllers/DatabaseController.js

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// A database adapter that works with data exported from the hosted
1+
// A database adapter that works with data exported from the hosted
22
// Parse database.
33

44
import intersect from 'intersect';
@@ -7,7 +7,8 @@ import _ from 'lodash';
77
var mongodb = require('mongodb');
88
var Parse = require('parse/node').Parse;
99

10-
var SchemaController = require('../Controllers/SchemaController');
10+
var SchemaController = require('./SchemaController');
11+
1112
const deepcopy = require('deepcopy');
1213

1314
function addWriteACL(query, acl) {
@@ -44,7 +45,7 @@ const transformObjectACL = ({ ACL, ...result }) => {
4445
return result;
4546
}
4647

47-
const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token'];
48+
const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at'];
4849
const validateQuery = query => {
4950
if (query.ACL) {
5051
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
@@ -80,18 +81,13 @@ const validateQuery = query => {
8081
});
8182
}
8283

83-
function DatabaseController(adapter, { skipValidation } = {}) {
84+
function DatabaseController(adapter, schemaCache) {
8485
this.adapter = adapter;
85-
86+
this.schemaCache = schemaCache;
8687
// We don't want a mutable this.schema, because then you could have
8788
// one request that uses different schemas for different parts of
8889
// it. Instead, use loadSchema to get a schema.
8990
this.schemaPromise = null;
90-
this.skipValidation = !!skipValidation;
91-
}
92-
93-
DatabaseController.prototype.WithoutValidation = function() {
94-
return new DatabaseController(this.adapter, { skipValidation: true });
9591
}
9692

9793
DatabaseController.prototype.collectionExists = function(className) {
@@ -105,20 +101,18 @@ DatabaseController.prototype.purgeCollection = function(className) {
105101
};
106102

107103
DatabaseController.prototype.validateClassName = function(className) {
108-
if (this.skipValidation) {
109-
return Promise.resolve();
110-
}
111104
if (!SchemaController.classNameIsValid(className)) {
112105
return Promise.reject(new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className));
113106
}
114107
return Promise.resolve();
115108
};
116109

117110
// Returns a promise for a schemaController.
118-
DatabaseController.prototype.loadSchema = function() {
111+
DatabaseController.prototype.loadSchema = function(options = {clearCache: false}) {
119112
if (!this.schemaPromise) {
120-
this.schemaPromise = SchemaController.load(this.adapter);
121-
this.schemaPromise.then(() => delete this.schemaPromise);
113+
this.schemaPromise = SchemaController.load(this.adapter, this.schemaCache, options);
114+
this.schemaPromise.then(() => delete this.schemaPromise,
115+
() => delete this.schemaPromise);
122116
}
123117
return this.schemaPromise;
124118
};
@@ -129,7 +123,7 @@ DatabaseController.prototype.loadSchema = function() {
129123
DatabaseController.prototype.redirectClassNameForKey = function(className, key) {
130124
return this.loadSchema().then((schema) => {
131125
var t = schema.getExpectedType(className, key);
132-
if (t.type == 'Relation') {
126+
if (t && t.type == 'Relation') {
133127
return t.targetClass;
134128
} else {
135129
return className;
@@ -184,13 +178,12 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => {
184178
// acl: a list of strings. If the object to be updated has an ACL,
185179
// one of the provided strings must provide the caller with
186180
// write permissions.
187-
const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token'];
181+
const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at'];
188182
DatabaseController.prototype.update = function(className, query, update, {
189183
acl,
190184
many,
191185
upsert,
192-
} = {}) {
193-
186+
} = {}, skipSanitization = false) {
194187
const originalUpdate = update;
195188
// Make a copy of the object, so we don't mutate the incoming data.
196189
update = deepcopy(update);
@@ -252,7 +245,7 @@ DatabaseController.prototype.update = function(className, query, update, {
252245
if (!result) {
253246
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'));
254247
}
255-
if (this.skipValidation) {
248+
if (skipSanitization) {
256249
return Promise.resolve(result);
257250
}
258251
return sanitizeDatabaseResult(originalUpdate, result);
@@ -549,8 +542,10 @@ DatabaseController.prototype.reduceInRelation = function(className, query, schem
549542
return Promise.all(ors.map((aQuery, index) => {
550543
return this.reduceInRelation(className, aQuery, schema).then((aQuery) => {
551544
query['$or'][index] = aQuery;
552-
})
553-
}));
545+
});
546+
})).then(() => {
547+
return Promise.resolve(query);
548+
});
554549
}
555550

556551
let promises = Object.keys(query).map((key) => {
@@ -811,8 +806,8 @@ const untransformObjectACL = ({_rperm, _wperm, ...output}) => {
811806
}
812807

813808
DatabaseController.prototype.deleteSchema = function(className) {
814-
return this.loadSchema()
815-
.then(schemaController => schemaController.getOneSchema(className))
809+
return this.loadSchema(true)
810+
.then(schemaController => schemaController.getOneSchema(className, true))
816811
.catch(error => {
817812
if (error === undefined) {
818813
return { fields: {} };

src/Controllers/SchemaCache.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const MAIN_SCHEMA = "__MAIN_SCHEMA";
2+
const SCHEMA_CACHE_PREFIX = "__SCHEMA";
3+
const ALL_KEYS = "__ALL_KEYS";
4+
5+
import { randomString } from '../cryptoUtils';
6+
7+
export default class SchemaCache {
8+
cache: Object;
9+
10+
constructor(cacheController, ttl = 30) {
11+
this.ttl = ttl;
12+
if (typeof ttl == 'string') {
13+
this.ttl = parseInt(ttl);
14+
}
15+
this.cache = cacheController;
16+
this.prefix = SCHEMA_CACHE_PREFIX+randomString(20);
17+
}
18+
19+
put(key, value) {
20+
return this.cache.get(this.prefix+ALL_KEYS).then((allKeys) => {
21+
allKeys = allKeys || {};
22+
allKeys[key] = true;
23+
return Promise.all([this.cache.put(this.prefix+ALL_KEYS, allKeys, this.ttl), this.cache.put(key, value, this.ttl)]);
24+
});
25+
}
26+
27+
getAllClasses() {
28+
if (!this.ttl) {
29+
return Promise.resolve(null);
30+
}
31+
return this.cache.get(this.prefix+MAIN_SCHEMA);
32+
}
33+
34+
setAllClasses(schema) {
35+
if (!this.ttl) {
36+
return Promise.resolve(null);
37+
}
38+
return this.put(this.prefix+MAIN_SCHEMA, schema);
39+
}
40+
41+
setOneSchema(className, schema) {
42+
if (!this.ttl) {
43+
return Promise.resolve(null);
44+
}
45+
return this.put(this.prefix+className, schema);
46+
}
47+
48+
getOneSchema(className) {
49+
if (!this.ttl) {
50+
return Promise.resolve(null);
51+
}
52+
return this.cache.get(this.prefix+className);
53+
}
54+
55+
clear() {
56+
// That clears all caches...
57+
let promise = Promise.resolve();
58+
return this.cache.get(this.prefix+ALL_KEYS).then((allKeys) => {
59+
if (!allKeys) {
60+
return;
61+
}
62+
let promises = Object.keys(allKeys).map((key) => {
63+
return this.cache.del(key);
64+
});
65+
return Promise.all(promises);
66+
});
67+
}
68+
}

0 commit comments

Comments
 (0)