Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Parse Server doesn't shutdown gracefully #9634

Merged
merged 11 commits into from
Mar 27, 2025
8 changes: 3 additions & 5 deletions spec/EnableExpressErrorHandler.spec.js
Original file line number Diff line number Diff line change
@@ -3,11 +3,9 @@ const request = require('../lib/request');
describe('Enable express error handler', () => {
it('should call the default handler in case of error, like updating a non existing object', async done => {
spyOn(console, 'error');
const parseServer = await reconfigureServer(
Object.assign({}, defaultConfiguration, {
enableExpressErrorHandler: true,
})
);
const parseServer = await reconfigureServer({
enableExpressErrorHandler: true,
});
parseServer.app.use(function (err, req, res, next) {
expect(err.message).toBe('Object not found.');
next(err);
2 changes: 0 additions & 2 deletions spec/ParseAPI.spec.js
Original file line number Diff line number Diff line change
@@ -33,9 +33,7 @@ describe('miscellaneous', () => {
expect(results.length).toEqual(1);
expect(results[0]['foo']).toEqual('bar');
});
});

describe('miscellaneous', function () {
it('create a GameScore object', function (done) {
const obj = new Parse.Object('GameScore');
obj.set('score', 1337);
6 changes: 1 addition & 5 deletions spec/ParseConfigKey.spec.js
Original file line number Diff line number Diff line change
@@ -12,7 +12,6 @@ describe('Config Keys', () => {

it('recognizes invalid keys in root', async () => {
await expectAsync(reconfigureServer({
...defaultConfiguration,
invalidKey: 1,
})).toBeResolved();
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 () => {
await expectAsync(reconfigureServer({
...defaultConfiguration,
pages: {
customUrls: {
invalidKey: 1,
@@ -37,7 +35,6 @@ describe('Config Keys', () => {

it('recognizes invalid keys in liveQueryServerOptions', async () => {
await expectAsync(reconfigureServer({
...defaultConfiguration,
liveQueryServerOptions: {
invalidKey: 1,
MasterKey: 1,
@@ -50,7 +47,6 @@ describe('Config Keys', () => {

it('recognizes invalid keys in rateLimit', async () => {
await expectAsync(reconfigureServer({
...defaultConfiguration,
rateLimit: [
{ invalidKey: 1 },
{ RequestPath: 1 },
@@ -64,7 +60,7 @@ describe('Config Keys', () => {
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({
...defaultConfiguration,
})).toBeResolved();
47 changes: 33 additions & 14 deletions spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
@@ -431,17 +431,32 @@ describe('ParseGraphQLServer', () => {
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();
httpServer = http.createServer(expressApp);
expressApp.use('/parse', parseServer.app);
expressApp.use('/parse', _parseServer.app);
parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, {
port: 1338,
});
parseGraphQLServer = new ParseGraphQLServer(_parseServer, {
graphQLPath: '/graphql',
playgroundPath: '/playground',
subscriptionsPath: '/subscriptions',
});
parseGraphQLServer.applyGraphQL(expressApp);
parseGraphQLServer.applyPlayground(expressApp);
parseGraphQLServer.createSubscriptions(httpServer);
await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve));
}

beforeEach(async () => {
await createGQLFromParseServer(parseServer);

const subscriptionClient = new SubscriptionClient(
'ws://localhost:13377/subscriptions',
@@ -753,10 +768,6 @@ describe('ParseGraphQLServer', () => {
}
});

afterAll(async () => {
await resetGraphQLCache();
});

it('should have Node interface', async () => {
const schemaTypes = (
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 () => {
await reconfigureServer({ objectIdSize: 20 });
const parseServer = await reconfigureServer({ objectIdSize: 20 });
await createGQLFromParseServer(parseServer);
const obj = new Parse.Object('SomeClass');
await obj.save({ name: 'aname', type: 'robot' });
const result = await apolloClient.query({
@@ -5328,7 +5340,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({
maxLimit: 10,
});

await createGQLFromParseServer(parseServer);
const promises = [];
for (let i = 0; i < 100; i++) {
const obj = new Parse.Object('SomeClass');
@@ -6841,7 +6853,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse',
});

await createGQLFromParseServer(parseServer);
const body = new FormData();
body.append(
'operations',
@@ -7049,6 +7061,7 @@ describe('ParseGraphQLServer', () => {
challengeAdapter,
},
});
await createGQLFromParseServer(parseServer);
const clientMutationId = uuidv4();

const result = await apolloClient.mutate({
@@ -7095,6 +7108,7 @@ describe('ParseGraphQLServer', () => {
challengeAdapter,
},
});
await createGQLFromParseServer(parseServer);
const clientMutationId = uuidv4();
const userSchema = new Parse.Schema('_User');
userSchema.addString('someField');
@@ -7169,7 +7183,7 @@ describe('ParseGraphQLServer', () => {
},
},
});

await createGQLFromParseServer(parseServer);
userSchema.addString('someField');
userSchema.addPointer('aPointer', '_User');
await userSchema.update();
@@ -7239,7 +7253,7 @@ describe('ParseGraphQLServer', () => {
challengeAdapter,
},
});

await createGQLFromParseServer(parseServer);
const user = new Parse.User();
await user.save({ username: 'username', password: 'password' });

@@ -7310,6 +7324,7 @@ describe('ParseGraphQLServer', () => {
challengeAdapter,
},
});
await createGQLFromParseServer(parseServer);
const clientMutationId = uuidv4();
const user = new Parse.User();
user.setUsername('user1');
@@ -7441,6 +7456,7 @@ describe('ParseGraphQLServer', () => {
emailAdapter: emailAdapter,
publicServerURL: 'http://test.test',
});
await createGQLFromParseServer(parseServer);
const user = new Parse.User();
user.setUsername('user1');
user.setPassword('user1');
@@ -7488,6 +7504,7 @@ describe('ParseGraphQLServer', () => {
},
},
});
await createGQLFromParseServer(parseServer);
const user = new Parse.User();
user.setUsername('user1');
user.setPassword('user1');
@@ -7550,6 +7567,7 @@ describe('ParseGraphQLServer', () => {
emailAdapter: emailAdapter,
publicServerURL: 'http://test.test',
});
await createGQLFromParseServer(parseServer);
const user = new Parse.User();
user.setUsername('user1');
user.setPassword('user1');
@@ -9306,7 +9324,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse',
});

await createGQLFromParseServer(parseServer);
const body = new FormData();
body.append(
'operations',
@@ -9339,7 +9357,6 @@ describe('ParseGraphQLServer', () => {
headers,
body,
});

expect(res.status).toEqual(200);

const result = JSON.parse(await res.text());
@@ -9553,6 +9570,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse',
});
await createGQLFromParseServer(parseServer);
const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('SomeClassWithRequiredFile', {
someField: { type: 'File', required: true },
@@ -9617,6 +9635,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse',
});
await createGQLFromParseServer(parseServer);
const schema = new Parse.Schema('SomeClass');
schema.addFile('someFileField');
schema.addPointer('somePointerField', 'SomeClass');
@@ -9725,7 +9744,7 @@ describe('ParseGraphQLServer', () => {
parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse',
});

await createGQLFromParseServer(parseServer);
const body = new FormData();
body.append(
'operations',
76 changes: 75 additions & 1 deletion spec/ParseLiveQuery.spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use strict';
const http = require('http');
const Auth = require('../lib/Auth');
const UserController = require('../lib/Controllers/UserController').UserController;
const Config = require('../lib/Config');
const ParseServer = require('../lib/index').ParseServer;
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 = () => {
throw 'you are not authorized';
};
@@ -1181,6 +1183,78 @@ describe('ParseLiveQuery', function () {
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 () => {
await reconfigureServer({
liveQuery: {
Loading