diff --git a/spec/SchemaPerformance.spec.js b/spec/SchemaPerformance.spec.js index 0471871c54..f53aa0af51 100644 --- a/spec/SchemaPerformance.spec.js +++ b/spec/SchemaPerformance.spec.js @@ -204,4 +204,23 @@ describe('Schema Performance', function () { ); expect(getAllSpy.calls.count()).toBe(2); }); + + fit('can save objects', async done => { + const start_parellel = Date.now(); + await Parse.User.signUp('username', 'password'); + + const objects = []; + for (let i = 0; i < 1000; i++) { + const obj = new Parse.Object('TestObj'); + obj.set('field1', 'uuid()'); + objects.push(obj); + // if ( i == 0) { + // await obj.save(); + // } + } + await Promise.all(objects.map(o => o.save())); + const end_parellel = Date.now() - start_parellel; + console.log(end_parellel); + expect(end_parellel).toBeLessThan(6000); + }); }); diff --git a/src/Auth.js b/src/Auth.js index abd14391db..f70776f131 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -3,6 +3,7 @@ import { isDeepStrictEqual } from 'util'; import { getRequestObject, resolveError } from './triggers'; import Deprecator from './Deprecator/Deprecator'; import { logger } from './logger'; +const authPromisesByUser = {}; // An Auth object tells you who is requesting something and whether // the master key was used. @@ -174,7 +175,9 @@ Auth.prototype.getUserRoles = function () { Auth.prototype.getRolesForUser = async function () { //Stack all Parse.Role - const results = []; + if (authPromisesByUser[this.user.id]) { + return authPromisesByUser[this.user.id] + } if (this.config) { const restWhere = { users: { @@ -184,15 +187,23 @@ Auth.prototype.getRolesForUser = async function () { }, }; const RestQuery = require('./RestQuery'); - await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result => - results.push(result) - ); - } else { - await new Parse.Query(Parse.Role) - .equalTo('users', this.user) - .each(result => results.push(result.toJSON()), { useMasterKey: true }); + const findObjects = async () => { + const results = []; + await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result => + results.push(result) + ); + return results; + } + authPromisesByUser[this.user.id] = findObjects(); + return authPromisesByUser[this.user.id]; } - return results; + authPromisesByUser[this.user.id] = new Parse.Query(Parse.Role) + .equalTo('users', this.user) + .findAll({ useMasterKey: true }).then(response => { + delete authPromisesByUser[this.user.id]; + return response; + }); + return authPromisesByUser[this.user.id]; }; // Iterates through the role tree and compiles a user's roles diff --git a/src/Config.js b/src/Config.js index bd7c6f21af..e34fba3461 100644 --- a/src/Config.js +++ b/src/Config.js @@ -5,7 +5,6 @@ import { isBoolean, isString } from 'lodash'; import net from 'net'; import AppCache from './cache'; -import DatabaseController from './Controllers/DatabaseController'; import { logLevels as validLogLevels } from './Controllers/LoggerController'; import { AccountLockoutOptions, @@ -37,11 +36,7 @@ export class Config { const config = new Config(); config.applicationId = applicationId; Object.keys(cacheInfo).forEach(key => { - if (key == 'databaseController') { - config.database = new DatabaseController(cacheInfo.databaseController.adapter, config); - } else { - config[key] = cacheInfo[key]; - } + config[key] = cacheInfo[key]; }); config.mount = removeTrailingSlash(mount); config.generateSessionExpiresAt = config.generateSessionExpiresAt.bind(config); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index e3ac5723ab..e5f5ba0b50 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -407,15 +407,13 @@ class DatabaseController { loadSchema( options: LoadSchemaOptions = { clearCache: false } ): Promise { - if (this.schemaPromise != null) { + if (this.schemaPromise) { return this.schemaPromise; } - this.schemaPromise = SchemaController.load(this.adapter, options); - this.schemaPromise.then( - () => delete this.schemaPromise, + this.schemaPromise = SchemaController.load(this.adapter, options).catch( () => delete this.schemaPromise ); - return this.loadSchema(options); + return this.schemaPromise; } loadSchemaIfNeeded( diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 62757d251d..c5b3d28e12 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -687,8 +687,10 @@ const typeToString = (type: SchemaField | string): string => { // the mongo format and the Parse format. Soon, this will all be Parse format. export default class SchemaController { _dbAdapter: StorageAdapter; + _reloadingData: { [string]: any }; + _addFieldPromises: { [string]: any }; + _addClassPromises: { [string]: any }; schemaData: { [string]: Schema }; - reloadDataPromise: ?Promise; protectedFields: any; userIdRegEx: RegExp; @@ -696,6 +698,10 @@ export default class SchemaController { this._dbAdapter = databaseAdapter; this.schemaData = new SchemaData(SchemaCache.all(), this.protectedFields); this.protectedFields = Config.get(Parse.applicationId).protectedFields; + this._reloadingData = { + promise: null, + className: '', + }; const customIds = Config.get(Parse.applicationId).allowCustomObjectId; @@ -709,24 +715,39 @@ export default class SchemaController { }); } - reloadData(options: LoadSchemaOptions = { clearCache: false }): Promise { - if (this.reloadDataPromise && !options.clearCache) { - return this.reloadDataPromise; + reloadData(options: LoadSchemaOptions = { clearCache: false, className: '' }): Promise { + if (options.className) { + if (this._reloadingData.className === options.className) { + options.clearCache = false; + } else { + this._reloadingData.className = options.className; + } + } else { + this._reloadingData.className = ''; } - this.reloadDataPromise = this.getAllClasses(options) + if (this._reloadingData.promise && !options.clearCache) { + return this._reloadingData.promise; + } + this._reloadingData.promise = this.getAllClasses(options) .then( allSchemas => { this.schemaData = new SchemaData(allSchemas, this.protectedFields); - delete this.reloadDataPromise; + this._reloadingData = { + promise: null, + className: '', + }; }, err => { this.schemaData = new SchemaData(); - delete this.reloadDataPromise; + this._reloadingData = { + promise: null, + className: '', + }; throw err; } ) .then(() => {}); - return this.reloadDataPromise; + return this._reloadingData.promise; } getAllClasses(options: LoadSchemaOptions = { clearCache: false }): Promise> { @@ -741,9 +762,14 @@ export default class SchemaController { } setAllClasses(): Promise> { + const r = 'classes ' + (Math.random() + 1).toString(36).substring(7); + console.time(r); return this._dbAdapter .getAllClasses() - .then(allSchemas => allSchemas.map(injectDefaultSchema)) + .then(allSchemas => { + // console.timeEnd(r); + return allSchemas.map(injectDefaultSchema); + }) .then(allSchemas => { SchemaCache.put(allSchemas); return allSchemas; @@ -787,7 +813,7 @@ export default class SchemaController { // on success, and rejects with an error on fail. Ensure you // have authorization (master key, or client class creation // enabled) before calling this function. - async addClassIfNotExists( + addClassIfNotExists( className: string, fields: SchemaFields = {}, classLevelPermissions: any, @@ -802,8 +828,14 @@ export default class SchemaController { } return Promise.reject(validationError); } - try { - const adapterSchema = await this._dbAdapter.createClass( + if (!this._addClassPromises) { + this._addClassPromises = {}; + } + if (this._addClassPromises[className]) { + return this._addClassPromises[className]; + } + this._addClassPromises[className] = this._dbAdapter + .createClass( className, convertSchemaToAdapterSchema({ fields, @@ -811,18 +843,24 @@ export default class SchemaController { indexes, className, }) - ); - // TODO: Remove by updating schema cache directly - await this.reloadData({ clearCache: true }); - const parseSchema = convertAdapterSchemaToParseSchema(adapterSchema); - return parseSchema; - } catch (error) { - if (error && error.code === Parse.Error.DUPLICATE_VALUE) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); - } else { - throw error; - } - } + ) + .then(async adapterSchema => { + await this.reloadData({ clearCache: true }); + const parseSchema = convertAdapterSchemaToParseSchema(adapterSchema); + delete this._addClassPromises[className]; + return parseSchema; + }) + .catch(error => { + if (error && error.code === Parse.Error.DUPLICATE_VALUE) { + throw new Parse.Error( + Parse.Error.INVALID_CLASS_NAME, + `Class ${className} already exists.` + ); + } else { + throw error; + } + }); + return this._addClassPromises[className]; } updateClass( @@ -947,7 +985,7 @@ export default class SchemaController { // have failed because there's a race condition and a different // client is making the exact same schema update that we want. // So just reload the schema. - return this.reloadData({ clearCache: true }); + return this.reloadData({ clearCache: true, className }); }) .then(() => { // Ensure that the schema now validates @@ -957,7 +995,7 @@ export default class SchemaController { throw new Parse.Error(Parse.Error.INVALID_JSON, `Failed to add ${className}`); } }) - .catch(() => { + .catch(e => { // The schema still doesn't validate. Give up throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate'); }) @@ -1131,9 +1169,22 @@ export default class SchemaController { return this._dbAdapter.updateFieldOptions(className, fieldName, type); } - return this._dbAdapter + if (!this._addFieldPromises) { + this._addFieldPromises = {}; + } + + if (!this._addFieldPromises[className]) { + this._addFieldPromises[className] = {}; + } + + if (this._addFieldPromises[className][`${fieldName}-${type}`]) { + return this._addFieldPromises[className][`${fieldName}-${type}`]; + } + + this._addFieldPromises[className][`${fieldName}-${type}`] = this._dbAdapter .addFieldIfNotExists(className, fieldName, type) .catch(error => { + delete this._addFieldPromises[className][`${fieldName}-${type}`]; if (error.code == Parse.Error.INCORRECT_TYPE) { // Make sure that we throw errors when it is appropriate to do so. throw error; @@ -1144,12 +1195,14 @@ export default class SchemaController { return Promise.resolve(); }) .then(() => { + delete this._addFieldPromises[className][`${fieldName}-${type}`]; return { className, fieldName, type, }; }); + return this._addFieldPromises[className][`${fieldName}-${type}`]; } ensureFields(fields: any) { @@ -1270,7 +1323,7 @@ export default class SchemaController { if (enforceFields.length !== 0) { // TODO: Remove by updating schema cache directly - await this.reloadData({ clearCache: true }); + await this.reloadData({ clearCache: true, className }); } this.ensureFields(enforceFields); diff --git a/src/Controllers/index.js b/src/Controllers/index.js index 0a9b3db57d..2bca7ef2f8 100644 --- a/src/Controllers/index.js +++ b/src/Controllers/index.js @@ -61,6 +61,7 @@ export function getControllers(options: ParseServerOptions) { parseGraphQLController, liveQueryController, databaseController, + database: databaseController, hooksController, authDataManager, schemaCache: SchemaCache,