Skip to content

Commit c2dd5c7

Browse files
committed
Adds schema caching capabilities (off by default)
1 parent 9bf21ef commit c2dd5c7

File tree

11 files changed

+150
-32
lines changed

11 files changed

+150
-32
lines changed

spec/ParseInstallation.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ let defaultColumns = require('../src/Controllers/SchemaController').defaultColum
1616
const installationSchema = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation) };
1717

1818
describe('Installations', () => {
19+
20+
beforeEach(() => {
21+
config.database.schemaCache.reset();
22+
});
23+
1924
it_exclude_dbs(['postgres'])('creates an android installation with ids', (done) => {
2025
var installId = '12345678-abcd-abcd-abcd-123456789abc';
2126
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.reset();
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/Schema.spec.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ var hasAllPODobject = () => {
2020
};
2121

2222
describe('SchemaController', () => {
23+
beforeEach(() => {
24+
config.database.schemaCache.reset();
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/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.reset();
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: 7 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,11 @@ 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+
this.database = new DatabaseController(cacheInfo.databaseController.adapter, new SchemaCache(applicationId, cacheInfo.schemaCacheTTL));
40+
}
3541

3642
this.serverURL = cacheInfo.serverURL;
3743
this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL);

src/Controllers/DatabaseController.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ const validateQuery = query => {
8080
});
8181
}
8282

83-
function DatabaseController(adapter) {
83+
function DatabaseController(adapter, schemaCache) {
8484
this.adapter = adapter;
85-
85+
this.schemaCache = schemaCache;
8686
// We don't want a mutable this.schema, because then you could have
8787
// one request that uses different schemas for different parts of
8888
// it. Instead, use loadSchema to get a schema.
@@ -107,9 +107,9 @@ DatabaseController.prototype.validateClassName = function(className) {
107107
};
108108

109109
// Returns a promise for a schemaController.
110-
DatabaseController.prototype.loadSchema = function() {
110+
DatabaseController.prototype.loadSchema = function(force = false) {
111111
if (!this.schemaPromise) {
112-
this.schemaPromise = SchemaController.load(this.adapter);
112+
this.schemaPromise = SchemaController.load(this.adapter, this.schemaCache, force);
113113
this.schemaPromise.then(() => delete this.schemaPromise);
114114
}
115115
return this.schemaPromise;
@@ -804,8 +804,8 @@ const untransformObjectACL = ({_rperm, _wperm, ...output}) => {
804804
}
805805

806806
DatabaseController.prototype.deleteSchema = function(className) {
807-
return this.loadSchema()
808-
.then(schemaController => schemaController.getOneSchema(className))
807+
return this.loadSchema(true)
808+
.then(schemaController => schemaController.getOneSchema(className, true))
809809
.catch(error => {
810810
if (error === undefined) {
811811
return { fields: {} };

src/Controllers/SchemaCache.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Parse from 'parse/node';
2+
import LRU from 'lru-cache';
3+
4+
let MAIN_SCHEMA = "__MAIN_SCHEMA";
5+
6+
export default class SchemaCache {
7+
cache: Object;
8+
9+
constructor(appId: string, timeout: number = 0, maxSize: number = 10000) {
10+
this.appId = appId;
11+
this.timeout = timeout;
12+
this.maxSize = maxSize;
13+
this.cache = new LRU({
14+
max: maxSize,
15+
maxAge: timeout
16+
});
17+
}
18+
19+
get() {
20+
if (this.timeout <= 0) {
21+
return;
22+
}
23+
return this.cache.get(this.appId+MAIN_SCHEMA);
24+
}
25+
26+
set(schema) {
27+
if (this.timeout <= 0) {
28+
return;
29+
}
30+
this.cache.set(this.appId+MAIN_SCHEMA, schema);
31+
}
32+
33+
setOneSchema(className, schema) {
34+
if (this.timeout <= 0) {
35+
return;
36+
}
37+
this.cache.set(this.appId+className, schema);
38+
}
39+
40+
getOneSchema(className) {
41+
if (this.timeout <= 0) {
42+
return;
43+
}
44+
return this.cache.get(this.appId+className);
45+
}
46+
47+
reset() {
48+
this.cache.reset();
49+
}
50+
}

src/Controllers/SchemaController.js

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -273,19 +273,22 @@ class SchemaController {
273273
data;
274274
perms;
275275

276-
constructor(databaseAdapter) {
276+
constructor(databaseAdapter, schemaCache) {
277277
this._dbAdapter = databaseAdapter;
278-
278+
this._cache = schemaCache;
279279
// this.data[className][fieldName] tells you the type of that field, in mongo format
280280
this.data = {};
281281
// this.perms[className][operation] tells you the acl-style permissions
282282
this.perms = {};
283283
}
284284

285-
reloadData() {
285+
reloadData(clearCache = false) {
286286
this.data = {};
287287
this.perms = {};
288-
return this.getAllClasses()
288+
if (clearCache) {
289+
this._cache.reset();
290+
}
291+
return this.getAllClasses(clearCache)
289292
.then(allSchemas => {
290293
allSchemas.forEach(schema => {
291294
this.data[schema.className] = injectDefaultSchema(schema).fields;
@@ -303,17 +306,39 @@ class SchemaController {
303306
});
304307
}
305308

306-
getAllClasses() {
309+
getAllClasses(clearCache = false) {
310+
if (clearCache) {
311+
this._cache.reset();
312+
}
313+
let allClasses = this._cache.get();
314+
if (allClasses && allClasses.length && !clearCache) {
315+
return Promise.resolve(allClasses);
316+
}
307317
return this._dbAdapter.getAllClasses()
308-
.then(allSchemas => allSchemas.map(injectDefaultSchema));
318+
.then(allSchemas => allSchemas.map(injectDefaultSchema))
319+
.then(allSchemas => {
320+
this._cache.set(allSchemas);
321+
return allSchemas;
322+
})
309323
}
310324

311-
getOneSchema(className, allowVolatileClasses = false) {
325+
getOneSchema(className, allowVolatileClasses = false, clearCache) {
326+
if (clearCache) {
327+
this._cache.reset();
328+
}
329+
let cached = this._cache.getOneSchema(className);
330+
if (cached && !clearCache) {
331+
return Promise.resolve(cached);
332+
}
312333
if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) {
313334
return Promise.resolve(this.data[className]);
314335
}
315336
return this._dbAdapter.getClass(className)
316337
.then(injectDefaultSchema)
338+
.then((result) => {
339+
this._cache.setOneSchema(className, result);
340+
return result;
341+
})
317342
}
318343

319344
// Create a new class that includes the three default fields.
@@ -331,6 +356,10 @@ class SchemaController {
331356

332357
return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, className }))
333358
.then(convertAdapterSchemaToParseSchema)
359+
.then((res) => {
360+
this._cache.reset();
361+
return res;
362+
})
334363
.catch(error => {
335364
if (error && error.code === Parse.Error.DUPLICATE_VALUE) {
336365
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
@@ -376,7 +405,7 @@ class SchemaController {
376405
});
377406

378407
return Promise.all(deletePromises) // Delete Everything
379-
.then(() => this.reloadData()) // Reload our Schema, so we have all the new values
408+
.then(() => this.reloadData(true)) // Reload our Schema, so we have all the new values
380409
.then(() => {
381410
let promises = insertedFields.map(fieldName => {
382411
const type = submittedFields[fieldName];
@@ -410,13 +439,13 @@ class SchemaController {
410439
// We don't have this class. Update the schema
411440
return this.addClassIfNotExists(className)
412441
// The schema update succeeded. Reload the schema
413-
.then(() => this.reloadData())
442+
.then(() => this.reloadData(true))
414443
.catch(error => {
415444
// The schema update failed. This can be okay - it might
416445
// have failed because there's a race condition and a different
417446
// client is making the exact same schema update that we want.
418447
// So just reload the schema.
419-
return this.reloadData();
448+
return this.reloadData(true);
420449
})
421450
.then(() => {
422451
// Ensure that the schema now validates
@@ -486,7 +515,7 @@ class SchemaController {
486515
}
487516
validateCLP(perms, newSchema);
488517
return this._dbAdapter.setClassLevelPermissions(className, perms)
489-
.then(() => this.reloadData());
518+
.then(() => this.reloadData(true));
490519
}
491520

492521
// Returns a promise that resolves successfully to the new schema
@@ -521,23 +550,26 @@ class SchemaController {
521550
`schema mismatch for ${className}.${fieldName}; expected ${expectedType.type || expectedType} but got ${type.type}`
522551
);
523552
}
553+
return this;
524554
}
525555

526556
return this._dbAdapter.addFieldIfNotExists(className, fieldName, type).then(() => {
527557
// The update succeeded. Reload the schema
528-
return this.reloadData();
558+
return this.reloadData(true);
529559
}, error => {
530560
//TODO: introspect the error and only reload if the error is one for which is makes sense to reload
531561

532562
// The update failed. This can be okay - it might have been a race
533563
// condition where another client updated the schema in the same
534564
// way that we wanted to. So, just reload the schema
535-
return this.reloadData();
565+
return this.reloadData(true);
536566
}).then(error => {
537567
// Ensure that the schema now validates
538568
if (!dbTypeMatchesObjectType(this.getExpectedType(className, fieldName), type)) {
539569
throw new Parse.Error(Parse.Error.INVALID_JSON, `Could not add field ${fieldName}`);
540570
}
571+
// Remove the cached schema
572+
this._cache.reset();
541573
return this;
542574
});
543575
});
@@ -562,7 +594,7 @@ class SchemaController {
562594
throw new Parse.Error(136, `field ${fieldName} cannot be changed`);
563595
}
564596

565-
return this.getOneSchema(className)
597+
return this.getOneSchema(className, false, true)
566598
.catch(error => {
567599
if (error === undefined) {
568600
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
@@ -579,8 +611,9 @@ class SchemaController {
579611
return database.adapter.deleteFields(className, schema, [fieldName])
580612
.then(() => database.adapter.deleteClass(`_Join:${fieldName}:${className}`));
581613
}
582-
583614
return database.adapter.deleteFields(className, schema, [fieldName]);
615+
}).then(() => {
616+
this._cache.reset();
584617
});
585618
}
586619

@@ -711,9 +744,9 @@ class SchemaController {
711744
}
712745

713746
// Returns a promise for a new Schema.
714-
const load = dbAdapter => {
715-
let schema = new SchemaController(dbAdapter);
716-
return schema.reloadData().then(() => schema);
747+
const load = (dbAdapter, schemaCache, clearCache) => {
748+
let schema = new SchemaController(dbAdapter, schemaCache);
749+
return schema.reloadData(clearCache).then(() => schema);
717750
}
718751

719752
// Builds a new schema (in schema API response format) out of an

src/ParseServer.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { UsersRouter } from './Routers/UsersRouter';
5353
import { PurgeRouter } from './Routers/PurgeRouter';
5454

5555
import DatabaseController from './Controllers/DatabaseController';
56+
import SchemaCache from './Controllers/SchemaCache';
5657
const SchemaController = require('./Controllers/SchemaController');
5758
import ParsePushAdapter from 'parse-server-push-adapter';
5859
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
@@ -132,6 +133,7 @@ class ParseServer {
132133
expireInactiveSessions = true,
133134
verbose = false,
134135
revokeSessionOnPasswordReset = true,
136+
schemaCacheTTL = -1, // -1 = no cache
135137
__indexBuildCompletionCallbackForTests = () => {},
136138
}) {
137139
// Initialize the node client SDK automatically
@@ -192,7 +194,8 @@ class ParseServer {
192194
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
193195
const liveQueryController = new LiveQueryController(liveQuery);
194196
const cacheController = new CacheController(cacheControllerAdapter, appId);
195-
const databaseController = new DatabaseController(databaseAdapter);
197+
const schemaCache = new SchemaCache(appId, schemaCacheTTL);
198+
const databaseController = new DatabaseController(databaseAdapter, schemaCache);
196199
const hooksController = new HooksController(appId, databaseController, webhookKey);
197200

198201
// TODO: create indexes on first creation of a _User object. Otherwise it's impossible to
@@ -244,6 +247,8 @@ class ParseServer {
244247
expireInactiveSessions: expireInactiveSessions,
245248
revokeSessionOnPasswordReset,
246249
databaseController,
250+
schemaCache,
251+
schemaCacheTTL
247252
});
248253

249254
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability

0 commit comments

Comments
 (0)