diff --git a/spec/EnableSingleSchemaCache.spec.js b/spec/EnableSingleSchemaCache.spec.js new file mode 100644 index 00000000..d0dc996a --- /dev/null +++ b/spec/EnableSingleSchemaCache.spec.js @@ -0,0 +1,49 @@ +const auth = require('../src/Auth'); +const Config = require('../src/Config'); +const rest = require('../src/rest'); + +describe('Enable single schema cache', () => { + beforeEach((done) => { + reconfigureServer({ + enableSingleSchemaCache: true, + schemaCacheTTL: 30000 + }).then(() => { + done(); + }); + }); + + it('can perform multiple create and query operations', (done) => { + let config = fakeRequestForConfig(); + let nobody = auth.nobody(config); + rest.create(config, nobody, 'Foo', {type: 1}).then(() => { + config = fakeRequestForConfig(); + nobody = auth.nobody(config); + return rest.create(config, nobody, 'Foo', {type: 2}); + }).then(() => { + config = fakeRequestForConfig(); + nobody = auth.nobody(config); + return rest.create(config, nobody, 'Bar'); + }).then(() => { + config = fakeRequestForConfig(); + nobody = auth.nobody(config); + return rest.find(config, nobody, 'Bar', {type: 1}); + }).then((response) => { + fail('Should throw error'); + done(); + }, (error) => { + config = fakeRequestForConfig(); + nobody = auth.nobody(config); + expect(error).toBeDefined(); + return rest.find(config, nobody, 'Foo', {type: 1}); + }).then((response) => { + config = fakeRequestForConfig(); + nobody = auth.nobody(config); + expect(response.results.length).toEqual(1); + done(); + }); + }); +}); + +const fakeRequestForConfig = function() { + return new Config('test'); +}; diff --git a/spec/SchemaCache.spec.js b/spec/SchemaCache.spec.js index 12e9fb38..a27abd98 100644 --- a/spec/SchemaCache.spec.js +++ b/spec/SchemaCache.spec.js @@ -3,35 +3,66 @@ var InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter') var SchemaCache = require('../src/Controllers/SchemaCache').default; describe('SchemaCache', () => { - var schemaCache; + let cacheController; - beforeEach(() => { - var cacheAdapter = new InMemoryCacheAdapter({}); - var cacheController = new CacheController(cacheAdapter, 'appId'); - schemaCache = new SchemaCache(cacheController); - }); + beforeEach(() => { + const cacheAdapter = new InMemoryCacheAdapter({}); + cacheController = new CacheController(cacheAdapter, 'appId'); + }); - it('can retrieve a single schema after all schemas stored', (done) => { - var allSchemas = [{ - className: 'Class1' - }, { - className: 'Class2' - }]; - schemaCache.setAllClasses(allSchemas); - schemaCache.getOneSchema('Class2').then((schema) => { - expect(schema).not.toBeNull(); - done(); - }); - }); + it('can retrieve a single schema after all schemas stored', (done) => { + const schemaCache = new SchemaCache(cacheController); + const allSchemas = [{ + className: 'Class1' + }, { + className: 'Class2' + }]; + schemaCache.setAllClasses(allSchemas).then(() => { + return schemaCache.getOneSchema('Class2'); + }).then((schema) => { + expect(schema).not.toBeNull(); + done(); + }); + }); - it('does not return all schemas after a single schema is stored', (done) => { - var schema = { - className: 'Class1' - }; - schemaCache.setOneSchema('Class1', schema); - schemaCache.getAllClasses().then((allSchemas) => { - expect(allSchemas).toBeNull(); - done(); - }); - }); + it('does not return all schemas after a single schema is stored', (done) => { + const schemaCache = new SchemaCache(cacheController); + const schema = { + className: 'Class1' + }; + schemaCache.setOneSchema(schema.className, schema).then(() => { + return schemaCache.getAllClasses(); + }).then((allSchemas) => { + expect(allSchemas).toBeNull(); + done(); + }); + }); + + it('doesn\'t persist cached data by default', (done) => { + const schemaCache = new SchemaCache(cacheController); + const schema = { + className: 'Class1' + }; + schemaCache.setOneSchema(schema.className, schema).then(() => { + const anotherSchemaCache = new SchemaCache(cacheController); + return anotherSchemaCache.getOneSchema(schema.className).then((schema) => { + expect(schema).toBeNull(); + done(); + }); + }); + }); + + it('can persist cached data', (done) => { + const schemaCache = new SchemaCache(cacheController, 5000, true); + const schema = { + className: 'Class1' + }; + schemaCache.setOneSchema(schema.className, schema).then(() => { + const anotherSchemaCache = new SchemaCache(cacheController, 5000, true); + return anotherSchemaCache.getOneSchema(schema.className).then((schema) => { + expect(schema).not.toBeNull(); + done(); + }); + }); + }); }); diff --git a/src/Config.js b/src/Config.js index d8d2ab26..016a3fe0 100644 --- a/src/Config.js +++ b/src/Config.js @@ -37,11 +37,12 @@ export class Config { // Create a new DatabaseController per request if (cacheInfo.databaseController) { - const schemaCache = new SchemaCache(cacheInfo.cacheController, cacheInfo.schemaCacheTTL); + const schemaCache = new SchemaCache(cacheInfo.cacheController, cacheInfo.schemaCacheTTL, cacheInfo.enableSingleSchemaCache); this.database = new DatabaseController(cacheInfo.databaseController.adapter, schemaCache); } this.schemaCacheTTL = cacheInfo.schemaCacheTTL; + this.enableSingleSchemaCache = cacheInfo.enableSingleSchemaCache; this.serverURL = cacheInfo.serverURL; this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index e966f062..986a71d5 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -515,7 +515,10 @@ DatabaseController.prototype.canAddField = function(schema, className, object, a // Returns a promise. DatabaseController.prototype.deleteEverything = function() { this.schemaPromise = null; - return this.adapter.deleteAllClasses(); + return Promise.all([ + this.adapter.deleteAllClasses(), + this.schemaCache.clear() + ]); }; // Finds the keys in a query. Returns a Set. REST format only diff --git a/src/Controllers/SchemaCache.js b/src/Controllers/SchemaCache.js index 63dc481d..f1286f47 100644 --- a/src/Controllers/SchemaCache.js +++ b/src/Controllers/SchemaCache.js @@ -8,13 +8,16 @@ import defaults from '../defaults'; export default class SchemaCache { cache: Object; - constructor(cacheController, ttl = defaults.schemaCacheTTL) { + constructor(cacheController, ttl = defaults.schemaCacheTTL, randomizePrefix = false) { this.ttl = ttl; if (typeof ttl == 'string') { this.ttl = parseInt(ttl); } this.cache = cacheController; - this.prefix = SCHEMA_CACHE_PREFIX+randomString(20); + this.prefix = SCHEMA_CACHE_PREFIX; + if (!randomizePrefix) { + this.prefix += randomString(20); + } } put(key, value) { diff --git a/src/ParseServer.js b/src/ParseServer.js index fcc27de7..3752d44c 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -139,6 +139,7 @@ class ParseServer { expireInactiveSessions = defaults.expireInactiveSessions, revokeSessionOnPasswordReset = defaults.revokeSessionOnPasswordReset, schemaCacheTTL = defaults.schemaCacheTTL, // cache for 5s + enableSingleSchemaCache = false, __indexBuildCompletionCallbackForTests = () => {}, }) { // Initialize the node client SDK automatically @@ -181,7 +182,7 @@ class ParseServer { const analyticsController = new AnalyticsController(analyticsControllerAdapter); const liveQueryController = new LiveQueryController(liveQuery); - const databaseController = new DatabaseController(databaseAdapter, new SchemaCache(cacheController, schemaCacheTTL)); + const databaseController = new DatabaseController(databaseAdapter, new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache)); const hooksController = new HooksController(appId, databaseController, webhookKey); const dbInitPromise = databaseController.performInitizalization(); @@ -221,7 +222,8 @@ class ParseServer { jsonLogs, revokeSessionOnPasswordReset, databaseController, - schemaCacheTTL + schemaCacheTTL, + enableSingleSchemaCache }); // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability diff --git a/src/cli/definitions/parse-server.js b/src/cli/definitions/parse-server.js index b4eaa90f..73822dae 100644 --- a/src/cli/definitions/parse-server.js +++ b/src/cli/definitions/parse-server.js @@ -195,6 +195,11 @@ export default { help: "The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 0; disabled.", action: numberParser("schemaCacheTTL"), }, + "enableSingleSchemaCache": { + env: "PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE", + help: "Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA. Defaults to false, i.e. unique schema cache per request.", + action: booleanParser + }, "cluster": { help: "Run with cluster, optionally set the number of processes default to os.cpus().length", action: numberOrBoolParser("cluster")