feat: Asynchronous initialization of Parse Server (#8232)

BREAKING CHANGE: This release introduces the asynchronous initialization of Parse Server to prevent mounting Parse Server before being ready to receive request; it changes how Parse Server is imported, initialized and started; it also removes the callback `serverStartComplete`; see the [Parse Server 6 migration guide](https://github.com/parse-community/parse-server/blob/alpha/6.0.0.md) for more details (#8232)
This commit is contained in:
Daniel
2022-12-22 01:30:13 +11:00
committed by GitHub
parent db9941c5a6
commit 99fcf45e55
21 changed files with 494 additions and 310 deletions

View File

@@ -466,10 +466,6 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE',
help: 'Callback when server has closed',
},
serverStartComplete: {
env: 'PARSE_SERVER_SERVER_START_COMPLETE',
help: 'Callback when server has started',
},
serverURL: {
env: 'PARSE_SERVER_URL',
help: 'URL to your parse server with http:// or https://.',

View File

@@ -85,7 +85,6 @@
* @property {SchemaOptions} schema Defined schema
* @property {SecurityOptions} security The security options to identify and report weak security settings.
* @property {Function} serverCloseComplete Callback when server has closed
* @property {Function} serverStartComplete Callback when server has started
* @property {String} serverURL URL to your parse server with http:// or https://.
* @property {Number} sessionLength Session duration, in seconds, defaults to 1 year
* @property {Boolean} silent Disables console output

View File

@@ -271,8 +271,6 @@ export interface ParseServerOptions {
:ENV: PARSE_SERVER_PLAYGROUND_PATH
:DEFAULT: /playground */
playgroundPath: ?string;
/* Callback when server has started */
serverStartComplete: ?(error: ?Error) => void;
/* Defined schema
:ENV: PARSE_SERVER_SCHEMA
*/

View File

@@ -64,73 +64,85 @@ class ParseServer {
const {
appId = requiredParameter('You must provide an appId!'),
masterKey = requiredParameter('You must provide a masterKey!'),
cloud,
security,
javascriptKey,
serverURL = requiredParameter('You must provide a serverURL!'),
serverStartComplete,
schema,
} = options;
// Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
Parse.serverURL = serverURL;
const allControllers = controllers.getControllers(options);
const {
loggerController,
databaseController,
hooksController,
liveQueryController,
} = allControllers;
options.state = 'initialized';
this.config = Config.put(Object.assign({}, options, allControllers));
logging.setLogger(allControllers.loggerController);
}
logging.setLogger(loggerController);
/**
* Starts Parse Server as an express app; this promise resolves when Parse Server is ready to accept requests.
*/
// Note: Tests will start to fail if any validation happens after this is called.
databaseController
.performInitialization()
.then(() => hooksController.load())
.then(async () => {
const startupPromises = [];
if (schema) {
startupPromises.push(new DefinedSchemas(schema, this.config).execute());
}
if (
options.cacheAdapter &&
options.cacheAdapter.connect &&
typeof options.cacheAdapter.connect === 'function'
) {
startupPromises.push(options.cacheAdapter.connect());
}
startupPromises.push(liveQueryController.connect());
await Promise.all(startupPromises);
if (serverStartComplete) {
serverStartComplete();
}
})
.catch(error => {
if (serverStartComplete) {
serverStartComplete(error);
} else {
console.error(error);
process.exit(1);
}
});
if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
cloud(Parse);
} else if (typeof cloud === 'string') {
require(path.resolve(process.cwd(), cloud));
} else {
throw "argument 'cloud' must either be a string or a function";
async start() {
try {
if (this.config.state === 'ok') {
return this;
}
}
if (security && security.enableCheck && security.enableCheckLog) {
new CheckRunner(options.security).run();
this.config.state = 'starting';
Config.put(this.config);
const {
databaseController,
hooksController,
cloud,
security,
schema,
cacheAdapter,
liveQueryController,
} = this.config;
try {
await databaseController.performInitialization();
} catch (e) {
if (e.code !== Parse.Error.DUPLICATE_VALUE) {
throw e;
}
}
await hooksController.load();
const startupPromises = [];
if (schema) {
startupPromises.push(new DefinedSchemas(schema, this.config).execute());
}
if (cacheAdapter?.connect && typeof cacheAdapter.connect === 'function') {
startupPromises.push(cacheAdapter.connect());
}
startupPromises.push(liveQueryController.connect());
await Promise.all(startupPromises);
if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
await Promise.resolve(cloud(Parse));
} else if (typeof cloud === 'string') {
let json;
if (process.env.npm_package_json) {
json = require(process.env.npm_package_json);
}
if (process.env.npm_package_type === 'module' || json?.type === 'module') {
await import(path.resolve(process.cwd(), cloud)).default;
} else {
require(path.resolve(process.cwd(), cloud));
}
} else {
throw "argument 'cloud' must either be a string or a function";
}
await new Promise(resolve => setTimeout(resolve, 10));
}
if (security && security.enableCheck && security.enableCheckLog) {
new CheckRunner(security).run();
}
this.config.state = 'ok';
Config.put(this.config);
return this;
} catch (error) {
console.error(error);
this.config.state = 'error';
throw error;
}
}
@@ -182,8 +194,12 @@ class ParseServer {
);
api.use('/health', function (req, res) {
res.status(options.state === 'ok' ? 200 : 503);
if (options.state === 'starting') {
res.set('Retry-After', 1);
}
res.json({
status: 'ok',
status: options.state,
});
});
@@ -266,10 +282,16 @@ class ParseServer {
/**
* starts the parse server's express app
* @param {ParseServerOptions} options to use to start the server
* @param {Function} callback called when the server has started
* @returns {ParseServer} the parse server instance
*/
async start(options: ParseServerOptions, callback: ?() => void) {
async startApp(options: ParseServerOptions) {
try {
await this.start();
} catch (e) {
console.error('Error on ParseServer.startApp: ', e);
throw e;
}
const app = express();
if (options.middleware) {
let middleware;
@@ -280,7 +302,6 @@ class ParseServer {
}
app.use(middleware);
}
app.use(options.mountPath, this.app);
if (options.mountGraphQL === true || options.mountPlayground === true) {
@@ -308,8 +329,11 @@ class ParseServer {
parseGraphQLServer.applyPlayground(app);
}
}
const server = app.listen(options.port, options.host, callback);
const server = await new Promise(resolve => {
app.listen(options.port, options.host, function () {
resolve(this);
});
});
this.server = server;
if (options.startLiveQueryServer || options.liveQueryServerOptions) {
@@ -330,12 +354,11 @@ class ParseServer {
/**
* Creates a new ParseServer and starts it.
* @param {ParseServerOptions} options used to start the server
* @param {Function} callback called when the server has started
* @returns {ParseServer} the parse server instance
*/
static start(options: ParseServerOptions, callback: ?() => void) {
static async startApp(options: ParseServerOptions) {
const parseServer = new ParseServer(options);
return parseServer.start(options, callback);
return parseServer.startApp(options);
}
/**

View File

@@ -7,6 +7,8 @@ import { internalCreateSchema, internalUpdateSchema } from '../Routers/SchemasRo
import { defaultColumns, systemClasses } from '../Controllers/SchemaController';
import { ParseServerOptions } from '../Options';
import * as Migrations from './Migrations';
import Auth from '../Auth';
import rest from '../rest';
export class DefinedSchemas {
config: ParseServerOptions;
@@ -96,9 +98,10 @@ export class DefinedSchemas {
}, 20000);
}
// Hack to force session schema to be created
await this.createDeleteSession();
this.allCloudSchemas = await Parse.Schema.all();
// @flow-disable-next-line
const schemaController = await this.config.database.loadSchema();
this.allCloudSchemas = await schemaController.getAllClasses();
clearTimeout(timeout);
await Promise.all(this.localSchemas.map(async localSchema => this.saveOrUpdate(localSchema)));
@@ -171,9 +174,8 @@ export class DefinedSchemas {
// Create a fake session since Parse do not create the _Session until
// a session is created
async createDeleteSession() {
const session = new Parse.Session();
await session.save(null, { useMasterKey: true });
await session.destroy({ useMasterKey: true });
const { response } = await rest.create(this.config, Auth.master(this.config), '_Session', {});
await rest.del(this.config, Auth.master(this.config), '_Session', response.objectId);
}
async saveOrUpdate(localSchema: Migrations.JSONSchema) {

View File

@@ -68,16 +68,26 @@ runner({
cluster.fork();
});
} else {
ParseServer.start(options, () => {
printSuccessMessage();
});
ParseServer.startApp(options)
.then(() => {
printSuccessMessage();
})
.catch(e => {
console.error(e);
process.exit(1);
});
}
} else {
ParseServer.start(options, () => {
logOptions();
console.log('');
printSuccessMessage();
});
ParseServer.startApp(options)
.then(() => {
logOptions();
console.log('');
printSuccessMessage();
})
.catch(e => {
console.error(e);
process.exit(1);
});
}
function printSuccessMessage() {

View File

@@ -16,11 +16,11 @@ import { ParseGraphQLServer } from './GraphQL/ParseGraphQLServer';
// Factory function
const _ParseServer = function (options: ParseServerOptions) {
const server = new ParseServer(options);
return server.app;
return server;
};
// Mount the create liveQueryServer
_ParseServer.createLiveQueryServer = ParseServer.createLiveQueryServer;
_ParseServer.start = ParseServer.start;
_ParseServer.startApp = ParseServer.startApp;
const S3Adapter = useExternal('S3Adapter', '@parse/s3-files-adapter');
const GCSAdapter = useExternal('GCSAdapter', '@parse/gcs-files-adapter');

View File

@@ -158,9 +158,18 @@ export function handleParseHeaders(req, res, next) {
}
const clientIp = getClientIp(req);
const config = Config.get(info.appId, mount);
if (config.state && config.state !== 'ok') {
res.status(500);
res.json({
code: Parse.Error.INTERNAL_SERVER_ERROR,
error: `Invalid server state: ${config.state}`,
});
return;
}
info.app = AppCache.get(info.appId);
req.config = Config.get(info.appId, mount);
req.config = config;
req.config.headers = req.headers || {};
req.config.ip = clientIp;
req.info = info;