fix: Parse Server doesn't shutdown gracefully (#9634)

This commit is contained in:
Diamond Lewis
2025-03-27 15:38:51 -05:00
committed by GitHub
parent f55de2b342
commit aed918d310
19 changed files with 308 additions and 240 deletions

View File

@@ -3,11 +3,9 @@ const request = require('../lib/request');
describe('Enable express error handler', () => { describe('Enable express error handler', () => {
it('should call the default handler in case of error, like updating a non existing object', async done => { it('should call the default handler in case of error, like updating a non existing object', async done => {
spyOn(console, 'error'); spyOn(console, 'error');
const parseServer = await reconfigureServer( const parseServer = await reconfigureServer({
Object.assign({}, defaultConfiguration, {
enableExpressErrorHandler: true, enableExpressErrorHandler: true,
}) });
);
parseServer.app.use(function (err, req, res, next) { parseServer.app.use(function (err, req, res, next) {
expect(err.message).toBe('Object not found.'); expect(err.message).toBe('Object not found.');
next(err); next(err);

View File

@@ -33,9 +33,7 @@ describe('miscellaneous', () => {
expect(results.length).toEqual(1); expect(results.length).toEqual(1);
expect(results[0]['foo']).toEqual('bar'); expect(results[0]['foo']).toEqual('bar');
}); });
});
describe('miscellaneous', function () {
it('create a GameScore object', function (done) { it('create a GameScore object', function (done) {
const obj = new Parse.Object('GameScore'); const obj = new Parse.Object('GameScore');
obj.set('score', 1337); obj.set('score', 1337);

View File

@@ -12,7 +12,6 @@ describe('Config Keys', () => {
it('recognizes invalid keys in root', async () => { it('recognizes invalid keys in root', async () => {
await expectAsync(reconfigureServer({ await expectAsync(reconfigureServer({
...defaultConfiguration,
invalidKey: 1, invalidKey: 1,
})).toBeResolved(); })).toBeResolved();
const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], ''); const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '');
@@ -21,7 +20,6 @@ describe('Config Keys', () => {
it('recognizes invalid keys in pages.customUrls', async () => { it('recognizes invalid keys in pages.customUrls', async () => {
await expectAsync(reconfigureServer({ await expectAsync(reconfigureServer({
...defaultConfiguration,
pages: { pages: {
customUrls: { customUrls: {
invalidKey: 1, invalidKey: 1,
@@ -37,7 +35,6 @@ describe('Config Keys', () => {
it('recognizes invalid keys in liveQueryServerOptions', async () => { it('recognizes invalid keys in liveQueryServerOptions', async () => {
await expectAsync(reconfigureServer({ await expectAsync(reconfigureServer({
...defaultConfiguration,
liveQueryServerOptions: { liveQueryServerOptions: {
invalidKey: 1, invalidKey: 1,
MasterKey: 1, MasterKey: 1,
@@ -50,7 +47,6 @@ describe('Config Keys', () => {
it('recognizes invalid keys in rateLimit', async () => { it('recognizes invalid keys in rateLimit', async () => {
await expectAsync(reconfigureServer({ await expectAsync(reconfigureServer({
...defaultConfiguration,
rateLimit: [ rateLimit: [
{ invalidKey: 1 }, { invalidKey: 1 },
{ RequestPath: 1 }, { RequestPath: 1 },
@@ -64,7 +60,7 @@ describe('Config Keys', () => {
expect(error).toMatch('rateLimit\\[2\\]\\.RequestTimeWindow'); expect(error).toMatch('rateLimit\\[2\\]\\.RequestTimeWindow');
}); });
it('recognizes valid keys in default configuration', async () => { it_only_db('mongo')('recognizes valid keys in default configuration', async () => {
await expectAsync(reconfigureServer({ await expectAsync(reconfigureServer({
...defaultConfiguration, ...defaultConfiguration,
})).toBeResolved(); })).toBeResolved();

View File

@@ -431,17 +431,32 @@ describe('ParseGraphQLServer', () => {
objects.push(object1, object2, object3, object4); objects.push(object1, object2, object3, object4);
} }
beforeEach(async () => { async function createGQLFromParseServer(_parseServer) {
if (parseLiveQueryServer) {
await parseLiveQueryServer.server.close();
}
if (httpServer) {
await httpServer.close();
}
const expressApp = express(); const expressApp = express();
httpServer = http.createServer(expressApp); httpServer = http.createServer(expressApp);
expressApp.use('/parse', parseServer.app); expressApp.use('/parse', _parseServer.app);
parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, { parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, {
port: 1338, port: 1338,
}); });
parseGraphQLServer = new ParseGraphQLServer(_parseServer, {
graphQLPath: '/graphql',
playgroundPath: '/playground',
subscriptionsPath: '/subscriptions',
});
parseGraphQLServer.applyGraphQL(expressApp); parseGraphQLServer.applyGraphQL(expressApp);
parseGraphQLServer.applyPlayground(expressApp); parseGraphQLServer.applyPlayground(expressApp);
parseGraphQLServer.createSubscriptions(httpServer); parseGraphQLServer.createSubscriptions(httpServer);
await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve)); await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve));
}
beforeEach(async () => {
await createGQLFromParseServer(parseServer);
const subscriptionClient = new SubscriptionClient( const subscriptionClient = new SubscriptionClient(
'ws://localhost:13377/subscriptions', 'ws://localhost:13377/subscriptions',
@@ -753,10 +768,6 @@ describe('ParseGraphQLServer', () => {
} }
}); });
afterAll(async () => {
await resetGraphQLCache();
});
it('should have Node interface', async () => { it('should have Node interface', async () => {
const schemaTypes = ( const schemaTypes = (
await apolloClient.query({ await apolloClient.query({
@@ -2821,7 +2832,8 @@ describe('ParseGraphQLServer', () => {
} }
}); });
it('Id inputs should work either with global id or object id with objectId higher than 19', async () => { it('Id inputs should work either with global id or object id with objectId higher than 19', async () => {
await reconfigureServer({ objectIdSize: 20 }); const parseServer = await reconfigureServer({ objectIdSize: 20 });
await createGQLFromParseServer(parseServer);
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
await obj.save({ name: 'aname', type: 'robot' }); await obj.save({ name: 'aname', type: 'robot' });
const result = await apolloClient.query({ const result = await apolloClient.query({
@@ -5328,7 +5340,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({ parseServer = await global.reconfigureServer({
maxLimit: 10, maxLimit: 10,
}); });
await createGQLFromParseServer(parseServer);
const promises = []; const promises = [];
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
@@ -6841,7 +6853,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({ parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse', publicServerURL: 'http://localhost:13377/parse',
}); });
await createGQLFromParseServer(parseServer);
const body = new FormData(); const body = new FormData();
body.append( body.append(
'operations', 'operations',
@@ -7049,6 +7061,7 @@ describe('ParseGraphQLServer', () => {
challengeAdapter, challengeAdapter,
}, },
}); });
await createGQLFromParseServer(parseServer);
const clientMutationId = uuidv4(); const clientMutationId = uuidv4();
const result = await apolloClient.mutate({ const result = await apolloClient.mutate({
@@ -7095,6 +7108,7 @@ describe('ParseGraphQLServer', () => {
challengeAdapter, challengeAdapter,
}, },
}); });
await createGQLFromParseServer(parseServer);
const clientMutationId = uuidv4(); const clientMutationId = uuidv4();
const userSchema = new Parse.Schema('_User'); const userSchema = new Parse.Schema('_User');
userSchema.addString('someField'); userSchema.addString('someField');
@@ -7169,7 +7183,7 @@ describe('ParseGraphQLServer', () => {
}, },
}, },
}); });
await createGQLFromParseServer(parseServer);
userSchema.addString('someField'); userSchema.addString('someField');
userSchema.addPointer('aPointer', '_User'); userSchema.addPointer('aPointer', '_User');
await userSchema.update(); await userSchema.update();
@@ -7239,7 +7253,7 @@ describe('ParseGraphQLServer', () => {
challengeAdapter, challengeAdapter,
}, },
}); });
await createGQLFromParseServer(parseServer);
const user = new Parse.User(); const user = new Parse.User();
await user.save({ username: 'username', password: 'password' }); await user.save({ username: 'username', password: 'password' });
@@ -7310,6 +7324,7 @@ describe('ParseGraphQLServer', () => {
challengeAdapter, challengeAdapter,
}, },
}); });
await createGQLFromParseServer(parseServer);
const clientMutationId = uuidv4(); const clientMutationId = uuidv4();
const user = new Parse.User(); const user = new Parse.User();
user.setUsername('user1'); user.setUsername('user1');
@@ -7441,6 +7456,7 @@ describe('ParseGraphQLServer', () => {
emailAdapter: emailAdapter, emailAdapter: emailAdapter,
publicServerURL: 'http://test.test', publicServerURL: 'http://test.test',
}); });
await createGQLFromParseServer(parseServer);
const user = new Parse.User(); const user = new Parse.User();
user.setUsername('user1'); user.setUsername('user1');
user.setPassword('user1'); user.setPassword('user1');
@@ -7488,6 +7504,7 @@ describe('ParseGraphQLServer', () => {
}, },
}, },
}); });
await createGQLFromParseServer(parseServer);
const user = new Parse.User(); const user = new Parse.User();
user.setUsername('user1'); user.setUsername('user1');
user.setPassword('user1'); user.setPassword('user1');
@@ -7550,6 +7567,7 @@ describe('ParseGraphQLServer', () => {
emailAdapter: emailAdapter, emailAdapter: emailAdapter,
publicServerURL: 'http://test.test', publicServerURL: 'http://test.test',
}); });
await createGQLFromParseServer(parseServer);
const user = new Parse.User(); const user = new Parse.User();
user.setUsername('user1'); user.setUsername('user1');
user.setPassword('user1'); user.setPassword('user1');
@@ -9306,7 +9324,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({ parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse', publicServerURL: 'http://localhost:13377/parse',
}); });
await createGQLFromParseServer(parseServer);
const body = new FormData(); const body = new FormData();
body.append( body.append(
'operations', 'operations',
@@ -9339,7 +9357,6 @@ describe('ParseGraphQLServer', () => {
headers, headers,
body, body,
}); });
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
const result = JSON.parse(await res.text()); const result = JSON.parse(await res.text());
@@ -9553,6 +9570,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({ parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse', publicServerURL: 'http://localhost:13377/parse',
}); });
await createGQLFromParseServer(parseServer);
const schemaController = await parseServer.config.databaseController.loadSchema(); const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('SomeClassWithRequiredFile', { await schemaController.addClassIfNotExists('SomeClassWithRequiredFile', {
someField: { type: 'File', required: true }, someField: { type: 'File', required: true },
@@ -9617,6 +9635,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({ parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse', publicServerURL: 'http://localhost:13377/parse',
}); });
await createGQLFromParseServer(parseServer);
const schema = new Parse.Schema('SomeClass'); const schema = new Parse.Schema('SomeClass');
schema.addFile('someFileField'); schema.addFile('someFileField');
schema.addPointer('somePointerField', 'SomeClass'); schema.addPointer('somePointerField', 'SomeClass');
@@ -9725,7 +9744,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({ parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse', publicServerURL: 'http://localhost:13377/parse',
}); });
await createGQLFromParseServer(parseServer);
const body = new FormData(); const body = new FormData();
body.append( body.append(
'operations', 'operations',

View File

@@ -1,10 +1,12 @@
'use strict'; 'use strict';
const http = require('http');
const Auth = require('../lib/Auth'); const Auth = require('../lib/Auth');
const UserController = require('../lib/Controllers/UserController').UserController; const UserController = require('../lib/Controllers/UserController').UserController;
const Config = require('../lib/Config'); const Config = require('../lib/Config');
const ParseServer = require('../lib/index').ParseServer; const ParseServer = require('../lib/index').ParseServer;
const triggers = require('../lib/triggers'); const triggers = require('../lib/triggers');
const { resolvingPromise, sleep } = require('../lib/TestUtils'); const { resolvingPromise, sleep, getConnectionsCount } = require('../lib/TestUtils');
const request = require('../lib/request');
const validatorFail = () => { const validatorFail = () => {
throw 'you are not authorized'; throw 'you are not authorized';
}; };
@@ -1181,6 +1183,78 @@ describe('ParseLiveQuery', function () {
await new Promise(resolve => server.server.close(resolve)); await new Promise(resolve => server.server.close(resolve));
}); });
it_id('45655b74-716f-4fa1-a058-67eb21f3c3db')(it)('does shutdown separate liveQuery server', async () => {
await reconfigureServer({ appId: 'test_app_id' });
let close = false;
const config = {
appId: 'hello_test',
masterKey: 'world',
port: 1345,
mountPath: '/1',
serverURL: 'http://localhost:1345/1',
liveQuery: {
classNames: ['Yolo'],
},
startLiveQueryServer: true,
verbose: false,
silent: true,
liveQueryServerOptions: {
port: 1346,
},
serverCloseComplete: () => {
close = true;
},
};
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
config.databaseAdapter = new databaseAdapter.constructor({
uri: databaseURI,
collectionPrefix: 'test_',
});
config.filesAdapter = defaultConfiguration.filesAdapter;
}
const parseServer = await ParseServer.startApp(config);
expect(parseServer.liveQueryServer).toBeDefined();
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
// Open a connection to the liveQuery server
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
client.serverURL = 'ws://localhost:1346/1';
const query = await new Parse.Query('Yolo').subscribe();
// Open a connection to the parse server
const health = await request({
method: 'GET',
url: `http://localhost:1345/1/health`,
json: true,
headers: {
'X-Parse-Application-Id': 'hello_test',
'X-Parse-Master-Key': 'world',
'Content-Type': 'application/json',
},
agent: new http.Agent({ keepAlive: true }),
}).then(res => res.data);
expect(health.status).toBe('ok');
let parseConnectionCount = await getConnectionsCount(parseServer.server);
let liveQueryConnectionCount = await getConnectionsCount(parseServer.liveQueryServer.server);
expect(parseConnectionCount > 0).toBe(true);
expect(liveQueryConnectionCount > 0).toBe(true);
await Promise.all([
parseServer.handleShutdown(),
new Promise(resolve => query.on('close', resolve)),
]);
expect(close).toBe(true);
await new Promise(resolve => setTimeout(resolve, 100));
expect(parseServer.liveQueryServer.server.address()).toBeNull();
expect(parseServer.liveQueryServer.subscriber.isOpen).toBeFalse();
parseConnectionCount = await getConnectionsCount(parseServer.server);
liveQueryConnectionCount = await getConnectionsCount(parseServer.liveQueryServer.server);
expect(parseConnectionCount).toBe(0);
expect(liveQueryConnectionCount).toBe(0);
});
it('prevent afterSave trigger if not exists', async () => { it('prevent afterSave trigger if not exists', async () => {
await reconfigureServer({ await reconfigureServer({
liveQuery: { liveQuery: {

View File

@@ -3,11 +3,8 @@
const Config = require('../lib/Config'); const Config = require('../lib/Config');
const Parse = require('parse/node'); const Parse = require('parse/node');
const request = require('../lib/request'); const request = require('../lib/request');
let databaseAdapter;
const fullTextHelper = async () => { const fullTextHelper = async () => {
const config = Config.get('test');
databaseAdapter = config.database.adapter;
const subjects = [ const subjects = [
'coffee', 'coffee',
'Coffee Shopping', 'Coffee Shopping',
@@ -18,12 +15,6 @@ const fullTextHelper = async () => {
'coffee and cream', 'coffee and cream',
'Cafe con Leche', 'Cafe con Leche',
]; ];
await reconfigureServer({
appId: 'test',
restAPIKey: 'test',
publicServerURL: 'http://localhost:8378/1',
databaseAdapter,
});
await Parse.Object.saveAll( await Parse.Object.saveAll(
subjects.map(subject => new Parse.Object('TestObject').set({ subject, comment: subject })) subjects.map(subject => new Parse.Object('TestObject').set({ subject, comment: subject }))
); );
@@ -101,7 +92,7 @@ describe('Parse.Query Full Text Search testing', () => {
body: { where, _method: 'GET' }, body: { where, _method: 'GET' },
headers: { headers: {
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'test', 'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });
@@ -189,7 +180,7 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () =
url: 'http://localhost:8378/1/schemas/TestObject', url: 'http://localhost:8378/1/schemas/TestObject',
headers: { headers: {
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'test', 'X-Parse-REST-API-Key': 'rest',
'X-Parse-Master-Key': 'test', 'X-Parse-Master-Key': 'test',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@@ -220,7 +211,7 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () =
body: { where, _method: 'GET' }, body: { where, _method: 'GET' },
headers: { headers: {
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'test', 'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });
@@ -288,7 +279,7 @@ describe_only_db('postgres')('[postgres] Parse.Query Full Text Search testing',
body: { where, _method: 'GET' }, body: { where, _method: 'GET' },
headers: { headers: {
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'test', 'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });
@@ -322,7 +313,7 @@ describe_only_db('postgres')('[postgres] Parse.Query Full Text Search testing',
body: { where, _method: 'GET' }, body: { where, _method: 'GET' },
headers: { headers: {
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'test', 'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });

View File

@@ -1,9 +1,6 @@
'use strict'; 'use strict';
/* Tests for ParseServer.js */ /* Tests for ParseServer.js */
const express = require('express'); const express = require('express');
const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default;
const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter')
.default;
const ParseServer = require('../lib/ParseServer').default; const ParseServer = require('../lib/ParseServer').default;
const path = require('path'); const path = require('path');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
@@ -45,49 +42,6 @@ describe('Server Url Checks', () => {
); );
}); });
xit('handleShutdown, close connection', done => {
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
let databaseAdapter;
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
databaseAdapter = new PostgresStorageAdapter({
uri: process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI,
collectionPrefix: 'test_',
});
} else {
databaseAdapter = new MongoStorageAdapter({
uri: mongoURI,
collectionPrefix: 'test_',
});
}
let close = false;
const newConfiguration = Object.assign({}, defaultConfiguration, {
databaseAdapter,
serverStartComplete: () => {
let promise = Promise.resolve();
if (process.env.PARSE_SERVER_TEST_DB !== 'postgres') {
promise = parseServer.config.filesController.adapter._connect();
}
promise.then(() => {
parseServer.handleShutdown();
parseServer.server.close(err => {
if (err) {
done.fail('Close Server Error');
}
reconfigureServer({}).then(() => {
expect(close).toBe(true);
done();
});
});
});
},
serverCloseComplete: () => {
close = true;
},
});
const parseServer = ParseServer.startApp(newConfiguration);
});
it('does not have unhandled promise rejection in the case of load error', done => { it('does not have unhandled promise rejection in the case of load error', done => {
const parseServerProcess = spawn(path.resolve(__dirname, './support/FailingServer.js')); const parseServerProcess = spawn(path.resolve(__dirname, './support/FailingServer.js'));
let stdout; let stdout;

View File

@@ -282,7 +282,6 @@ describe('ParseServerRESTController', () => {
}); });
it('should generate separate session for each call', async () => { it('should generate separate session for each call', async () => {
await reconfigureServer();
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
await myObject.save({ key: 'stringField' }); await myObject.save({ key: 'stringField' });
await myObject.destroy(); await myObject.destroy();

View File

@@ -182,6 +182,9 @@ describe('PushController', () => {
return ['ios', 'android']; return ['ios', 'android'];
}, },
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
const payload = { const payload = {
data: { data: {
alert: 'Hello World!', alert: 'Hello World!',
@@ -212,9 +215,6 @@ describe('PushController', () => {
const auth = { const auth = {
isMaster: true, isMaster: true,
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
await Parse.Object.saveAll(installations); await Parse.Object.saveAll(installations);
const pushStatusId = await sendPush(payload, {}, config, auth); const pushStatusId = await sendPush(payload, {}, config, auth);
await pushCompleted(pushStatusId); await pushCompleted(pushStatusId);
@@ -247,6 +247,9 @@ describe('PushController', () => {
return ['ios', 'android']; return ['ios', 'android'];
}, },
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
const payload = { const payload = {
data: { data: {
alert: 'Hello World!', alert: 'Hello World!',
@@ -277,9 +280,6 @@ describe('PushController', () => {
const auth = { const auth = {
isMaster: true, isMaster: true,
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
await Parse.Object.saveAll(installations); await Parse.Object.saveAll(installations);
const pushStatusId = await sendPush(payload, {}, config, auth); const pushStatusId = await sendPush(payload, {}, config, auth);
await pushCompleted(pushStatusId); await pushCompleted(pushStatusId);
@@ -309,7 +309,9 @@ describe('PushController', () => {
return ['ios']; return ['ios'];
}, },
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
const payload = { const payload = {
data: { data: {
alert: 'Hello World!', alert: 'Hello World!',
@@ -331,9 +333,6 @@ describe('PushController', () => {
const auth = { const auth = {
isMaster: true, isMaster: true,
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
await Parse.Object.saveAll(installations); await Parse.Object.saveAll(installations);
const pushStatusId = await sendPush(payload, {}, config, auth); const pushStatusId = await sendPush(payload, {}, config, auth);
await pushCompleted(pushStatusId); await pushCompleted(pushStatusId);
@@ -382,14 +381,13 @@ describe('PushController', () => {
return ['ios']; return ['ios'];
}, },
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
const config = Config.get(Parse.applicationId); const config = Config.get(Parse.applicationId);
const auth = { const auth = {
isMaster: true, isMaster: true,
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
await Parse.Object.saveAll(installations); await Parse.Object.saveAll(installations);
const objectIds = installations.map(installation => { const objectIds = installations.map(installation => {
return installation.id; return installation.id;
@@ -445,14 +443,13 @@ describe('PushController', () => {
return ['ios']; return ['ios'];
}, },
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
const config = Config.get(Parse.applicationId); const config = Config.get(Parse.applicationId);
const auth = { const auth = {
isMaster: true, isMaster: true,
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
await Parse.Object.saveAll(installations); await Parse.Object.saveAll(installations);
const pushStatusId = await sendPush(payload, {}, config, auth); const pushStatusId = await sendPush(payload, {}, config, auth);
await pushCompleted(pushStatusId); await pushCompleted(pushStatusId);
@@ -548,16 +545,15 @@ describe('PushController', () => {
return ['ios']; return ['ios'];
}, },
}; };
const config = Config.get(Parse.applicationId);
const auth = {
isMaster: true,
};
await installation.save(); await installation.save();
await reconfigureServer({ await reconfigureServer({
serverURL: 'http://localhost:8378/', // server with borked URL serverURL: 'http://localhost:8378/', // server with borked URL
push: { adapter: pushAdapter }, push: { adapter: pushAdapter },
}); });
const config = Config.get(Parse.applicationId);
const auth = {
isMaster: true,
};
const pushStatusId = await sendPush(payload, {}, config, auth); const pushStatusId = await sendPush(payload, {}, config, auth);
// it is enqueued so it can take time // it is enqueued so it can take time
await jasmine.timeout(1000); await jasmine.timeout(1000);
@@ -580,6 +576,9 @@ describe('PushController', () => {
return ['ios']; return ['ios'];
}, },
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
// $ins is invalid query // $ins is invalid query
const where = { const where = {
channels: { channels: {
@@ -596,9 +595,6 @@ describe('PushController', () => {
isMaster: true, isMaster: true,
}; };
const pushController = new PushController(); const pushController = new PushController();
await reconfigureServer({
push: { adapter: pushAdapter },
});
const config = Config.get(Parse.applicationId); const config = Config.get(Parse.applicationId);
try { try {
await pushController.sendPush(payload, where, config, auth); await pushController.sendPush(payload, where, config, auth);
@@ -631,6 +627,9 @@ describe('PushController', () => {
return ['ios']; return ['ios'];
}, },
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
const config = Config.get(Parse.applicationId); const config = Config.get(Parse.applicationId);
const auth = { const auth = {
isMaster: true, isMaster: true,
@@ -641,9 +640,6 @@ describe('PushController', () => {
$in: ['device_token_0', 'device_token_1', 'device_token_2'], $in: ['device_token_0', 'device_token_1', 'device_token_2'],
}, },
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
const installations = []; const installations = [];
while (installations.length != 5) { while (installations.length != 5) {
const installation = new Parse.Object('_Installation'); const installation = new Parse.Object('_Installation');
@@ -678,7 +674,9 @@ describe('PushController', () => {
return ['ios']; return ['ios'];
}, },
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
const config = Config.get(Parse.applicationId); const config = Config.get(Parse.applicationId);
const auth = { const auth = {
isMaster: true, isMaster: true,
@@ -686,9 +684,6 @@ describe('PushController', () => {
const where = { const where = {
deviceType: 'ios', deviceType: 'ios',
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
const installations = []; const installations = [];
while (installations.length != 5) { while (installations.length != 5) {
const installation = new Parse.Object('_Installation'); const installation = new Parse.Object('_Installation');
@@ -762,10 +757,6 @@ describe('PushController', () => {
}); });
it('should not schedule push when not configured', async () => { it('should not schedule push when not configured', async () => {
const config = Config.get(Parse.applicationId);
const auth = {
isMaster: true,
};
const pushAdapter = { const pushAdapter = {
send: function (body, installations) { send: function (body, installations) {
return successfulTransmissions(body, installations); return successfulTransmissions(body, installations);
@@ -774,7 +765,13 @@ describe('PushController', () => {
return ['ios']; return ['ios'];
}, },
}; };
await reconfigureServer({
push: { adapter: pushAdapter },
});
const config = Config.get(Parse.applicationId);
const auth = {
isMaster: true,
};
const pushController = new PushController(); const pushController = new PushController();
const payload = { const payload = {
data: { data: {
@@ -793,10 +790,6 @@ describe('PushController', () => {
installation.set('deviceType', 'ios'); installation.set('deviceType', 'ios');
installations.push(installation); installations.push(installation);
} }
await reconfigureServer({
push: { adapter: pushAdapter },
});
await Parse.Object.saveAll(installations); await Parse.Object.saveAll(installations);
await pushController.sendPush(payload, {}, config, auth); await pushController.sendPush(payload, {}, config, auth);
await jasmine.timeout(1000); await jasmine.timeout(1000);
@@ -986,6 +979,10 @@ describe('PushController', () => {
return ['ios']; return ['ios'];
}, },
}; };
spyOn(pushAdapter, 'send').and.callThrough();
await reconfigureServer({
push: { adapter: pushAdapter },
});
const config = Config.get(Parse.applicationId); const config = Config.get(Parse.applicationId);
const auth = { const auth = {
isMaster: true, isMaster: true,
@@ -1007,10 +1004,6 @@ describe('PushController', () => {
installations[1].set('localeIdentifier', 'fr-FR'); installations[1].set('localeIdentifier', 'fr-FR');
installations[2].set('localeIdentifier', 'en-US'); installations[2].set('localeIdentifier', 'en-US');
spyOn(pushAdapter, 'send').and.callThrough();
await reconfigureServer({
push: { adapter: pushAdapter },
});
await Parse.Object.saveAll(installations); await Parse.Object.saveAll(installations);
const pushStatusId = await sendPush(payload, where, config, auth); const pushStatusId = await sendPush(payload, where, config, auth);
await pushCompleted(pushStatusId); await pushCompleted(pushStatusId);
@@ -1039,7 +1032,10 @@ describe('PushController', () => {
return ['ios']; return ['ios'];
}, },
}; };
spyOn(pushAdapter, 'send').and.callThrough();
await reconfigureServer({
push: { adapter: pushAdapter },
});
const config = Config.get(Parse.applicationId); const config = Config.get(Parse.applicationId);
const auth = { const auth = {
isMaster: true, isMaster: true,
@@ -1060,10 +1056,6 @@ describe('PushController', () => {
installation.set('deviceType', 'ios'); installation.set('deviceType', 'ios');
installations.push(installation); installations.push(installation);
} }
spyOn(pushAdapter, 'send').and.callThrough();
await reconfigureServer({
push: { adapter: pushAdapter },
});
await Parse.Object.saveAll(installations); await Parse.Object.saveAll(installations);
// Create an audience // Create an audience

View File

@@ -5,10 +5,8 @@ describe('Schema Performance', function () {
let config; let config;
beforeEach(async () => { beforeEach(async () => {
await reconfigureServer();
config = Config.get('test'); config = Config.get('test');
config.schemaCache.clear();
const databaseAdapter = config.database.adapter;
await reconfigureServer({ databaseAdapter });
getAllSpy = spyOn(databaseAdapter, 'getAllClasses').and.callThrough(); getAllSpy = spyOn(databaseAdapter, 'getAllClasses').and.callThrough();
}); });

View File

@@ -67,18 +67,22 @@ describe('Security Check Groups', () => {
it('checks succeed correctly', async () => { it('checks succeed correctly', async () => {
const config = Config.get(Parse.applicationId); const config = Config.get(Parse.applicationId);
const uri = config.database.adapter._uri;
config.database.adapter._uri = 'protocol://user:aMoreSecur3Passwor7!@example.com'; config.database.adapter._uri = 'protocol://user:aMoreSecur3Passwor7!@example.com';
const group = new CheckGroupDatabase(); const group = new CheckGroupDatabase();
await group.run(); await group.run();
expect(group.checks()[0].checkState()).toBe(CheckState.success); expect(group.checks()[0].checkState()).toBe(CheckState.success);
config.database.adapter._uri = uri;
}); });
it('checks fail correctly', async () => { it('checks fail correctly', async () => {
const config = Config.get(Parse.applicationId); const config = Config.get(Parse.applicationId);
const uri = config.database.adapter._uri;
config.database.adapter._uri = 'protocol://user:insecure@example.com'; config.database.adapter._uri = 'protocol://user:insecure@example.com';
const group = new CheckGroupDatabase(); const group = new CheckGroupDatabase();
await group.run(); await group.run();
expect(group.checks()[0].checkState()).toBe(CheckState.fail); expect(group.checks()[0].checkState()).toBe(CheckState.fail);
config.database.adapter._uri = uri;
}); });
}); });
}); });

View File

@@ -366,7 +366,6 @@ describe('batch', () => {
}); });
it('should generate separate session for each call', async () => { it('should generate separate session for each call', async () => {
await reconfigureServer();
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
await myObject.save({ key: 'stringField' }); await myObject.save({ key: 'stringField' });
await myObject.destroy(); await myObject.destroy();

View File

@@ -36,6 +36,7 @@ module.exports = [
describe_only_db: "readonly", describe_only_db: "readonly",
fdescribe_only_db: "readonly", fdescribe_only_db: "readonly",
describe_only: "readonly", describe_only: "readonly",
fdescribe_only: "readonly",
on_db: "readonly", on_db: "readonly",
defaultConfiguration: "readonly", defaultConfiguration: "readonly",
range: "readonly", range: "readonly",

View File

@@ -1,9 +1,11 @@
'use strict'; 'use strict';
const dns = require('dns'); const dns = require('dns');
const semver = require('semver'); const semver = require('semver');
const Parse = require('parse/node');
const CurrentSpecReporter = require('./support/CurrentSpecReporter.js'); const CurrentSpecReporter = require('./support/CurrentSpecReporter.js');
const { SpecReporter } = require('jasmine-spec-reporter'); const { SpecReporter } = require('jasmine-spec-reporter');
const SchemaCache = require('../lib/Adapters/Cache/SchemaCache').default; const SchemaCache = require('../lib/Adapters/Cache/SchemaCache').default;
const { sleep, Connections } = require('../lib/TestUtils');
// Ensure localhost resolves to ipv4 address first on node v17+ // Ensure localhost resolves to ipv4 address first on node v17+
if (dns.setDefaultResultOrder) { if (dns.setDefaultResultOrder) {
@@ -53,7 +55,6 @@ const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
let databaseAdapter; let databaseAdapter;
let databaseURI; let databaseURI;
// need to bind for mocking mocha
if (process.env.PARSE_SERVER_DATABASE_ADAPTER) { if (process.env.PARSE_SERVER_DATABASE_ADAPTER) {
databaseAdapter = JSON.parse(process.env.PARSE_SERVER_DATABASE_ADAPTER); databaseAdapter = JSON.parse(process.env.PARSE_SERVER_DATABASE_ADAPTER);
@@ -73,7 +74,7 @@ if (process.env.PARSE_SERVER_DATABASE_ADAPTER) {
} }
const port = 8378; const port = 8378;
const serverURL = `http://localhost:${port}/1`;
let filesAdapter; let filesAdapter;
on_db( on_db(
@@ -99,7 +100,7 @@ if (process.env.PARSE_SERVER_LOG_LEVEL) {
// Default server configuration for tests. // Default server configuration for tests.
const defaultConfiguration = { const defaultConfiguration = {
filesAdapter, filesAdapter,
serverURL: 'http://localhost:' + port + '/1', serverURL,
databaseAdapter, databaseAdapter,
appId: 'test', appId: 'test',
javascriptKey: 'test', javascriptKey: 'test',
@@ -153,34 +154,38 @@ if (silent) {
}; };
} }
if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
defaultConfiguration.cacheAdapter = new RedisCacheAdapter();
}
const openConnections = {};
const destroyAliveConnections = function () {
for (const socketId in openConnections) {
try {
openConnections[socketId].destroy();
delete openConnections[socketId];
} catch (e) {
/* */
}
}
};
// Set up a default API server for testing with default configuration. // Set up a default API server for testing with default configuration.
let parseServer; let parseServer;
let didChangeConfiguration = false; let didChangeConfiguration = false;
const openConnections = new Connections();
const shutdownServer = async (_parseServer) => {
await _parseServer.handleShutdown();
// Connection close events are not immediate on node 10+, so wait a bit
await sleep(0);
expect(openConnections.count() > 0).toBeFalsy(`There were ${openConnections.count()} open connections to the server left after the test finished`);
parseServer = undefined;
};
// Allows testing specific configurations of Parse Server // Allows testing specific configurations of Parse Server
const reconfigureServer = async (changedConfiguration = {}) => { const reconfigureServer = async (changedConfiguration = {}) => {
if (parseServer) { if (parseServer) {
destroyAliveConnections(); await shutdownServer(parseServer);
await new Promise(resolve => parseServer.server.close(resolve));
parseServer = undefined;
return reconfigureServer(changedConfiguration); return reconfigureServer(changedConfiguration);
} }
didChangeConfiguration = Object.keys(changedConfiguration).length !== 0; didChangeConfiguration = Object.keys(changedConfiguration).length !== 0;
databaseAdapter = new databaseAdapter.constructor({
uri: databaseURI,
collectionPrefix: 'test_',
});
defaultConfiguration.databaseAdapter = databaseAdapter;
global.databaseAdapter = databaseAdapter;
if (filesAdapter instanceof GridFSBucketAdapter) {
defaultConfiguration.filesAdapter = new GridFSBucketAdapter(mongoURI);
}
if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
defaultConfiguration.cacheAdapter = new RedisCacheAdapter();
}
const newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { const newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, {
mountPath: '/1', mountPath: '/1',
port, port,
@@ -192,39 +197,19 @@ const reconfigureServer = async (changedConfiguration = {}) => {
console.error(err); console.error(err);
fail('should not call next'); fail('should not call next');
}); });
parseServer.liveQueryServer?.server?.on('connection', connection => { openConnections.track(parseServer.server);
const key = `${connection.remoteAddress}:${connection.remotePort}`; if (parseServer.liveQueryServer?.server && parseServer.liveQueryServer.server !== parseServer.server) {
openConnections[key] = connection; openConnections.track(parseServer.liveQueryServer.server);
connection.on('close', () => { }
delete openConnections[key];
});
});
parseServer.server.on('connection', connection => {
const key = `${connection.remoteAddress}:${connection.remotePort}`;
openConnections[key] = connection;
connection.on('close', () => {
delete openConnections[key];
});
});
return parseServer; return parseServer;
}; };
// Set up a Parse client to talk to our test API server
const Parse = require('parse/node');
Parse.serverURL = 'http://localhost:' + port + '/1';
beforeAll(async () => { beforeAll(async () => {
try {
Parse.User.enableUnsafeCurrentUser();
} catch (error) {
if (error !== 'You need to call Parse.initialize before using Parse.') {
throw error;
}
}
await reconfigureServer(); await reconfigureServer();
Parse.initialize('test', 'test', 'test'); Parse.initialize('test', 'test', 'test');
Parse.serverURL = 'http://localhost:' + port + '/1'; Parse.serverURL = serverURL;
Parse.User.enableUnsafeCurrentUser();
Parse.CoreManager.set('REQUEST_ATTEMPT_LIMIT', 1);
}); });
global.afterEachFn = async () => { global.afterEachFn = async () => {
@@ -253,19 +238,7 @@ global.afterEachFn = async () => {
}, },
}); });
}); });
await Parse.User.logOut().catch(() => {}); await Parse.User.logOut().catch(() => {});
// Connection close events are not immediate on node 10+, so wait a bit
await new Promise(resolve => setTimeout(resolve, 0));
// After logout operations
if (Object.keys(openConnections).length > 1) {
console.warn(
`There were ${Object.keys(openConnections).length} open connections to the server left after the test finished`
);
}
await TestUtils.destroyAllDataPermanently(true); await TestUtils.destroyAllDataPermanently(true);
SchemaCache.clear(); SchemaCache.clear();
@@ -454,6 +427,7 @@ global.mockCustomAuthenticator = mockCustomAuthenticator;
global.mockFacebookAuthenticator = mockFacebookAuthenticator; global.mockFacebookAuthenticator = mockFacebookAuthenticator;
global.databaseAdapter = databaseAdapter; global.databaseAdapter = databaseAdapter;
global.databaseURI = databaseURI; global.databaseURI = databaseURI;
global.shutdownServer = shutdownServer;
global.jfail = function (err) { global.jfail = function (err) {
fail(JSON.stringify(err)); fail(JSON.stringify(err));
}; };
@@ -610,6 +584,14 @@ global.describe_only = validator => {
} }
}; };
global.fdescribe_only = validator => {
if (validator()) {
return fdescribe;
} else {
return xdescribe;
}
};
const libraryCache = {}; const libraryCache = {};
jasmine.mockLibrary = function (library, name, mock) { jasmine.mockLibrary = function (library, name, mock) {
const original = require(library)[name]; const original = require(library)[name];

View File

@@ -62,6 +62,7 @@ describe('server', () => {
}); });
it('fails if database is unreachable', async () => { it('fails if database is unreachable', async () => {
spyOn(console, 'error').and.callFake(() => {});
const server = new ParseServer.default({ const server = new ParseServer.default({
...defaultConfiguration, ...defaultConfiguration,
databaseAdapter: new MongoStorageAdapter({ databaseAdapter: new MongoStorageAdapter({
@@ -145,7 +146,7 @@ describe('server', () => {
}, },
publicServerURL: 'http://localhost:8378/1', publicServerURL: 'http://localhost:8378/1',
}; };
expectAsync(reconfigureServer(options)).toBeRejected('MockMailAdapterConstructor'); await expectAsync(reconfigureServer(options)).toBeRejected('MockMailAdapterConstructor');
}); });
}); });

View File

@@ -20,6 +20,8 @@ const flakyTests = [
"UserController sendVerificationEmail parseFrameURL provided uses parseFrameURL and includes the destination in the link parameter", "UserController sendVerificationEmail parseFrameURL provided uses parseFrameURL and includes the destination in the link parameter",
// Expected undefined to be defined // Expected undefined to be defined
"Email Verification Token Expiration: sets the _email_verify_token_expires_at and _email_verify_token fields after user SignUp", "Email Verification Token Expiration: sets the _email_verify_token_expires_at and _email_verify_token fields after user SignUp",
// Expected 0 to be 1.
"Email Verification Token Expiration: should send a new verification email when a resend is requested and the user is UNVERIFIED",
]; ];
/** The minimum execution time in seconds for a test to be considered slow. */ /** The minimum execution time in seconds for a test to be considered slow. */

View File

@@ -97,15 +97,23 @@ class ParseLiveQueryServer {
if (this.subscriber.isOpen) { if (this.subscriber.isOpen) {
await Promise.all([ await Promise.all([
...[...this.clients.values()].map(client => client.parseWebSocket.ws.close()), ...[...this.clients.values()].map(client => client.parseWebSocket.ws.close()),
this.parseWebSocketServer.close(), this.parseWebSocketServer.close?.(),
...Array.from(this.subscriber.subscriptions.keys()).map(key => ...Array.from(this.subscriber.subscriptions?.keys() || []).map(key =>
this.subscriber.unsubscribe(key) this.subscriber.unsubscribe(key)
), ),
this.subscriber.close?.(), this.subscriber.close?.(),
]); ]);
} }
if (typeof this.subscriber.quit === 'function') {
try {
await this.subscriber.quit();
} catch (err) {
logger.error('PubSubAdapter error on shutdown', { error: err });
}
} else {
this.subscriber.isOpen = false; this.subscriber.isOpen = false;
} }
}
_createSubscribers() { _createSubscribers() {
const messageRecieved = (channel, messageStr) => { const messageRecieved = (channel, messageStr) => {

View File

@@ -45,10 +45,14 @@ import CheckRunner from './Security/CheckRunner';
import Deprecator from './Deprecator/Deprecator'; import Deprecator from './Deprecator/Deprecator';
import { DefinedSchemas } from './SchemaMigrations/DefinedSchemas'; import { DefinedSchemas } from './SchemaMigrations/DefinedSchemas';
import OptionsDefinitions from './Options/Definitions'; import OptionsDefinitions from './Options/Definitions';
import { resolvingPromise, Connections } from './TestUtils';
// Mutate the Parse object to add the Cloud Code handlers // Mutate the Parse object to add the Cloud Code handlers
addParseCloud(); addParseCloud();
// Track connections to destroy them on shutdown
const connections = new Connections();
// ParseServer works like a constructor of an express app. // ParseServer works like a constructor of an express app.
// https://parseplatform.org/parse-server/api/master/ParseServerOptions.html // https://parseplatform.org/parse-server/api/master/ParseServerOptions.html
class ParseServer { class ParseServer {
@@ -214,8 +218,39 @@ class ParseServer {
return this._app; return this._app;
} }
handleShutdown() { /**
* Stops the parse server, cancels any ongoing requests and closes all connections.
*
* Currently, express doesn't shut down immediately after receiving SIGINT/SIGTERM
* if it has client connections that haven't timed out.
* (This is a known issue with node - https://github.com/nodejs/node/issues/2642)
*
* @returns {Promise<void>} a promise that resolves when the server is stopped
*/
async handleShutdown() {
const serverClosePromise = resolvingPromise();
const liveQueryServerClosePromise = resolvingPromise();
const promises = []; const promises = [];
this.server.close((error) => {
/* istanbul ignore next */
if (error) {
// eslint-disable-next-line no-console
console.error('Error while closing parse server', error);
}
serverClosePromise.resolve();
});
if (this.liveQueryServer?.server?.close && this.liveQueryServer.server !== this.server) {
this.liveQueryServer.server.close((error) => {
/* istanbul ignore next */
if (error) {
// eslint-disable-next-line no-console
console.error('Error while closing live query server', error);
}
liveQueryServerClosePromise.resolve();
});
} else {
liveQueryServerClosePromise.resolve();
}
const { adapter: databaseAdapter } = this.config.databaseController; const { adapter: databaseAdapter } = this.config.databaseController;
if (databaseAdapter && typeof databaseAdapter.handleShutdown === 'function') { if (databaseAdapter && typeof databaseAdapter.handleShutdown === 'function') {
promises.push(databaseAdapter.handleShutdown()); promises.push(databaseAdapter.handleShutdown());
@@ -228,17 +263,15 @@ class ParseServer {
if (cacheAdapter && typeof cacheAdapter.handleShutdown === 'function') { if (cacheAdapter && typeof cacheAdapter.handleShutdown === 'function') {
promises.push(cacheAdapter.handleShutdown()); promises.push(cacheAdapter.handleShutdown());
} }
if (this.liveQueryServer?.server?.close) {
promises.push(new Promise(resolve => this.liveQueryServer.server.close(resolve)));
}
if (this.liveQueryServer) { if (this.liveQueryServer) {
promises.push(this.liveQueryServer.shutdown()); promises.push(this.liveQueryServer.shutdown());
} }
return (promises.length > 0 ? Promise.all(promises) : Promise.resolve()).then(() => { await Promise.all(promises);
connections.destroyAll();
await Promise.all([serverClosePromise, liveQueryServerClosePromise]);
if (this.config.serverCloseComplete) { if (this.config.serverCloseComplete) {
this.config.serverCloseComplete(); this.config.serverCloseComplete();
} }
});
} }
/** /**
@@ -419,6 +452,7 @@ class ParseServer {
}); });
}); });
this.server = server; this.server = server;
connections.track(server);
if (options.startLiveQueryServer || options.liveQueryServerOptions) { if (options.startLiveQueryServer || options.liveQueryServerOptions) {
this.liveQueryServer = await ParseServer.createLiveQueryServer( this.liveQueryServer = await ParseServer.createLiveQueryServer(
@@ -426,6 +460,9 @@ class ParseServer {
options.liveQueryServerOptions, options.liveQueryServerOptions,
options options
); );
if (this.liveQueryServer.server !== this.server) {
connections.track(this.liveQueryServer.server);
}
} }
if (options.trustProxy) { if (options.trustProxy) {
app.set('trust proxy', options.trustProxy); app.set('trust proxy', options.trustProxy);
@@ -600,32 +637,8 @@ function injectDefaults(options: ParseServerOptions) {
// Those can't be tested as it requires a subprocess // Those can't be tested as it requires a subprocess
/* istanbul ignore next */ /* istanbul ignore next */
function configureListeners(parseServer) { function configureListeners(parseServer) {
const server = parseServer.server;
const sockets = {};
/* Currently, express doesn't shut down immediately after receiving SIGINT/SIGTERM if it has client connections that haven't timed out. (This is a known issue with node - https://github.com/nodejs/node/issues/2642)
This function, along with `destroyAliveConnections()`, intend to fix this behavior such that parse server will close all open connections and initiate the shutdown process as soon as it receives a SIGINT/SIGTERM signal. */
server.on('connection', socket => {
const socketId = socket.remoteAddress + ':' + socket.remotePort;
sockets[socketId] = socket;
socket.on('close', () => {
delete sockets[socketId];
});
});
const destroyAliveConnections = function () {
for (const socketId in sockets) {
try {
sockets[socketId].destroy();
} catch (e) {
/* */
}
}
};
const handleShutdown = function () { const handleShutdown = function () {
process.stdout.write('Termination signal received. Shutting down.'); process.stdout.write('Termination signal received. Shutting down.');
destroyAliveConnections();
server.close();
parseServer.handleShutdown(); parseServer.handleShutdown();
}; };
process.on('SIGTERM', handleShutdown); process.on('SIGTERM', handleShutdown);

View File

@@ -42,3 +42,42 @@ export function resolvingPromise() {
export function sleep(ms) { export function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
export function getConnectionsCount(server) {
return new Promise((resolve, reject) => {
server.getConnections((err, count) => {
/* istanbul ignore next */
if (err) {
reject(err);
} else {
resolve(count);
}
});
});
};
export class Connections {
constructor() {
this.sockets = new Set();
}
track(server) {
server.on('connection', socket => {
this.sockets.add(socket);
socket.on('close', () => {
this.sockets.delete(socket);
});
});
}
destroyAll() {
for (const socket of this.sockets.values()) {
socket.destroy();
}
this.sockets.clear();
}
count() {
return this.sockets.size;
}
}