Fix flaky test with transactions (#7187)

* Fix flaky test with transactions

* Add CHANGELOG entry

* Fix the other transactions related tests that became flaky because now Parse Server tries to submit the transaction multilpe times in the case of TransientError

* Remove fit from tests
This commit is contained in:
Antonio Davi Macedo Coelho de Castro
2021-02-18 10:18:54 -08:00
committed by GitHub
parent 9a9fc5fa5f
commit a430d6f7b7
6 changed files with 158 additions and 100 deletions

View File

@@ -20,6 +20,7 @@ ___
- NEW: LiveQuery support for $and, $nor, $containedBy, $geoWithin, $geoIntersects queries [#7113](https://github.com/parse-community/parse-server/pull/7113). Thanks to [dplewis](https://github.com/dplewis) - NEW: LiveQuery support for $and, $nor, $containedBy, $geoWithin, $geoIntersects queries [#7113](https://github.com/parse-community/parse-server/pull/7113). Thanks to [dplewis](https://github.com/dplewis)
- NEW: Supporting patterns in LiveQuery server's config parameter `classNames` [#7131](https://github.com/parse-community/parse-server/pull/7131). Thanks to [Nes-si](https://github.com/Nes-si) - NEW: Supporting patterns in LiveQuery server's config parameter `classNames` [#7131](https://github.com/parse-community/parse-server/pull/7131). Thanks to [Nes-si](https://github.com/Nes-si)
- NEW: `requireAnyUserRoles` and `requireAllUserRoles` for Parse Cloud validator. [#7097](https://github.com/parse-community/parse-server/pull/7097). Thanks to [dblythy](https://github.com/dblythy) - NEW: `requireAnyUserRoles` and `requireAllUserRoles` for Parse Cloud validator. [#7097](https://github.com/parse-community/parse-server/pull/7097). Thanks to [dblythy](https://github.com/dblythy)
- IMPROVE: Retry transactions on MongoDB when it fails due to transient error [#7187](https://github.com/parse-community/parse-server/pull/7187). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo).
- IMPROVE: Bump tests to use Mongo 4.4.4 [#7184](https://github.com/parse-community/parse-server/pull/7184). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo). - IMPROVE: Bump tests to use Mongo 4.4.4 [#7184](https://github.com/parse-community/parse-server/pull/7184). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo).
- IMPROVE: Added new account lockout policy option `accountLockout.unlockOnPasswordReset` to automatically unlock account on password reset. [#7146](https://github.com/parse-community/parse-server/pull/7146). Thanks to [Manuel Trezza](https://github.com/mtrezza). - IMPROVE: Added new account lockout policy option `accountLockout.unlockOnPasswordReset` to automatically unlock account on password reset. [#7146](https://github.com/parse-community/parse-server/pull/7146). Thanks to [Manuel Trezza](https://github.com/mtrezza).
- IMPROVE: Parse Server is from now on continuously tested against all recent MongoDB versions that have not reached their end-of-life support date. Added MongoDB compatibility table to Parse Server docs. [7161](https://github.com/parse-community/parse-server/pull/7161). Thanks to [Manuel Trezza](https://github.com/mtrezza). - IMPROVE: Parse Server is from now on continuously tested against all recent MongoDB versions that have not reached their end-of-life support date. Added MongoDB compatibility table to Parse Server docs. [7161](https://github.com/parse-community/parse-server/pull/7161). Thanks to [Manuel Trezza](https://github.com/mtrezza).

View File

@@ -197,7 +197,7 @@ describe('ParseServerRESTController', () => {
.then(() => { .then(() => {
spyOn(databaseAdapter, 'createObject').and.callThrough(); spyOn(databaseAdapter, 'createObject').and.callThrough();
RESTController.request('POST', 'batch', { return RESTController.request('POST', 'batch', {
requests: [ requests: [
{ {
method: 'POST', method: 'POST',
@@ -218,11 +218,14 @@ describe('ParseServerRESTController', () => {
expect(response[1].success.objectId).toBeDefined(); expect(response[1].success.objectId).toBeDefined();
expect(response[1].success.createdAt).toBeDefined(); expect(response[1].success.createdAt).toBeDefined();
const query = new Parse.Query('MyObject'); const query = new Parse.Query('MyObject');
query.find().then(results => { return query.find().then(results => {
expect(databaseAdapter.createObject.calls.count()).toBe(2); expect(databaseAdapter.createObject.calls.count() % 2).toBe(0);
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toBe( expect(databaseAdapter.createObject.calls.count() > 0).toEqual(true);
databaseAdapter.createObject.calls.argsFor(1)[3] for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) {
expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe(
databaseAdapter.createObject.calls.argsFor(i + 1)[3]
); );
}
expect(results.map(result => result.get('key')).sort()).toEqual([ expect(results.map(result => result.get('key')).sort()).toEqual([
'value1', 'value1',
'value2', 'value2',
@@ -230,7 +233,8 @@ describe('ParseServerRESTController', () => {
done(); done();
}); });
}); });
}); })
.catch(done.fail);
}); });
it('should not save anything when one operation fails in a transaction', done => { it('should not save anything when one operation fails in a transaction', done => {
@@ -513,18 +517,18 @@ describe('ParseServerRESTController', () => {
const results3 = await query3.find(); const results3 = await query3.find();
expect(results3.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); expect(results3.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
expect(databaseAdapter.createObject.calls.count()).toBe(13); expect(databaseAdapter.createObject.calls.count() >= 13).toEqual(true);
let transactionalSession; let transactionalSession;
let transactionalSession2; let transactionalSession2;
let myObjectDBCalls = 0; let myObjectDBCalls = 0;
let myObject2DBCalls = 0; let myObject2DBCalls = 0;
let myObject3DBCalls = 0; let myObject3DBCalls = 0;
for (let i = 0; i < 13; i++) { for (let i = 0; i < databaseAdapter.createObject.calls.count(); i++) {
const args = databaseAdapter.createObject.calls.argsFor(i); const args = databaseAdapter.createObject.calls.argsFor(i);
switch (args[0]) { switch (args[0]) {
case 'MyObject': case 'MyObject':
myObjectDBCalls++; myObjectDBCalls++;
if (!transactionalSession) { if (!transactionalSession || (myObjectDBCalls - 1) % 2 === 0) {
transactionalSession = args[3]; transactionalSession = args[3];
} else { } else {
expect(transactionalSession).toBe(args[3]); expect(transactionalSession).toBe(args[3]);
@@ -535,7 +539,7 @@ describe('ParseServerRESTController', () => {
break; break;
case 'MyObject2': case 'MyObject2':
myObject2DBCalls++; myObject2DBCalls++;
if (!transactionalSession2) { if (!transactionalSession2 || (myObject2DBCalls - 1) % 9 === 0) {
transactionalSession2 = args[3]; transactionalSession2 = args[3];
} else { } else {
expect(transactionalSession2).toBe(args[3]); expect(transactionalSession2).toBe(args[3]);
@@ -550,9 +554,12 @@ describe('ParseServerRESTController', () => {
break; break;
} }
} }
expect(myObjectDBCalls).toEqual(2); expect(myObjectDBCalls % 2).toEqual(0);
expect(myObject2DBCalls).toEqual(9); expect(myObjectDBCalls > 0).toEqual(true);
expect(myObject3DBCalls).toEqual(2); expect(myObject2DBCalls % 9).toEqual(0);
expect(myObject2DBCalls > 0).toEqual(true);
expect(myObject3DBCalls % 2).toEqual(0);
expect(myObject3DBCalls > 0).toEqual(true);
}); });
}); });
} }

View File

@@ -228,10 +228,13 @@ describe('batch', () => {
expect(response.data[1].success.createdAt).toBeDefined(); expect(response.data[1].success.createdAt).toBeDefined();
const query = new Parse.Query('MyObject'); const query = new Parse.Query('MyObject');
query.find().then(results => { query.find().then(results => {
expect(databaseAdapter.createObject.calls.count()).toBe(2); expect(databaseAdapter.createObject.calls.count() % 2).toBe(0);
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toBe( expect(databaseAdapter.createObject.calls.count() > 0).toEqual(true);
databaseAdapter.createObject.calls.argsFor(1)[3] for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) {
expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe(
databaseAdapter.createObject.calls.argsFor(i + 1)[3]
); );
}
expect(results.map(result => result.get('key')).sort()).toEqual([ expect(results.map(result => result.get('key')).sort()).toEqual([
'value1', 'value1',
'value2', 'value2',
@@ -542,18 +545,18 @@ describe('batch', () => {
const results3 = await query3.find(); const results3 = await query3.find();
expect(results3.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); expect(results3.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
expect(databaseAdapter.createObject.calls.count()).toBe(13); expect(databaseAdapter.createObject.calls.count() >= 13).toEqual(true);
let transactionalSession; let transactionalSession;
let transactionalSession2; let transactionalSession2;
let myObjectDBCalls = 0; let myObjectDBCalls = 0;
let myObject2DBCalls = 0; let myObject2DBCalls = 0;
let myObject3DBCalls = 0; let myObject3DBCalls = 0;
for (let i = 0; i < 13; i++) { for (let i = 0; i < databaseAdapter.createObject.calls.count(); i++) {
const args = databaseAdapter.createObject.calls.argsFor(i); const args = databaseAdapter.createObject.calls.argsFor(i);
switch (args[0]) { switch (args[0]) {
case 'MyObject': case 'MyObject':
myObjectDBCalls++; myObjectDBCalls++;
if (!transactionalSession) { if (!transactionalSession || (myObjectDBCalls - 1) % 2 === 0) {
transactionalSession = args[3]; transactionalSession = args[3];
} else { } else {
expect(transactionalSession).toBe(args[3]); expect(transactionalSession).toBe(args[3]);
@@ -564,7 +567,7 @@ describe('batch', () => {
break; break;
case 'MyObject2': case 'MyObject2':
myObject2DBCalls++; myObject2DBCalls++;
if (!transactionalSession2) { if (!transactionalSession2 || (myObject2DBCalls - 1) % 9 === 0) {
transactionalSession2 = args[3]; transactionalSession2 = args[3];
} else { } else {
expect(transactionalSession2).toBe(args[3]); expect(transactionalSession2).toBe(args[3]);
@@ -579,9 +582,12 @@ describe('batch', () => {
break; break;
} }
} }
expect(myObjectDBCalls).toEqual(2); expect(myObjectDBCalls % 2).toEqual(0);
expect(myObject2DBCalls).toEqual(9); expect(myObjectDBCalls > 0).toEqual(true);
expect(myObject3DBCalls).toEqual(2); expect(myObject2DBCalls % 9).toEqual(0);
expect(myObject2DBCalls > 0).toEqual(true);
expect(myObject3DBCalls % 2).toEqual(0);
expect(myObject3DBCalls > 0).toEqual(true);
}); });
}); });
} }

View File

@@ -1046,9 +1046,20 @@ export class MongoStorageAdapter implements StorageAdapter {
} }
commitTransactionalSession(transactionalSection: any): Promise<void> { commitTransactionalSession(transactionalSection: any): Promise<void> {
return transactionalSection.commitTransaction().then(() => { const commit = retries => {
return transactionalSection
.commitTransaction()
.catch(error => {
if (error && error.hasErrorLabel('TransientTransactionError') && retries > 0) {
return commit(retries - 1);
}
throw error;
})
.then(() => {
transactionalSection.endSession(); transactionalSection.endSession();
}); });
};
return commit(5);
} }
abortTransactionalSession(transactionalSection: any): Promise<void> { abortTransactionalSession(transactionalSection: any): Promise<void> {

View File

@@ -48,6 +48,7 @@ function ParseServerRESTController(applicationId, router) {
} }
if (path === '/batch') { if (path === '/batch') {
const batch = transactionRetries => {
let initialPromise = Promise.resolve(); let initialPromise = Promise.resolve();
if (data.transaction === true) { if (data.transaction === true) {
initialPromise = config.database.createTransactionalSession(); initialPromise = config.database.createTransactionalSession();
@@ -70,7 +71,8 @@ function ParseServerRESTController(applicationId, router) {
} }
); );
}); });
return Promise.all(promises).then(result => { return Promise.all(promises)
.then(result => {
if (data.transaction === true) { if (data.transaction === true) {
if (result.find(resultItem => typeof resultItem.error === 'object')) { if (result.find(resultItem => typeof resultItem.error === 'object')) {
return config.database.abortTransactionalSession().then(() => { return config.database.abortTransactionalSession().then(() => {
@@ -84,8 +86,22 @@ function ParseServerRESTController(applicationId, router) {
} else { } else {
return result; return result;
} }
})
.catch(error => {
if (
error &&
error.find(
errorItem => typeof errorItem.error === 'object' && errorItem.error.code === 251
) &&
transactionRetries > 0
) {
return batch(transactionRetries - 1);
}
throw error;
}); });
}); });
};
return batch(5);
} }
let query; let query;

View File

@@ -83,6 +83,7 @@ function handleBatch(router, req) {
req.config.publicServerURL req.config.publicServerURL
); );
const batch = transactionRetries => {
let initialPromise = Promise.resolve(); let initialPromise = Promise.resolve();
if (req.body.transaction === true) { if (req.body.transaction === true) {
initialPromise = req.config.database.createTransactionalSession(); initialPromise = req.config.database.createTransactionalSession();
@@ -110,7 +111,8 @@ function handleBatch(router, req) {
); );
}); });
return Promise.all(promises).then(results => { return Promise.all(promises)
.then(results => {
if (req.body.transaction === true) { if (req.body.transaction === true) {
if (results.find(result => typeof result.error === 'object')) { if (results.find(result => typeof result.error === 'object')) {
return req.config.database.abortTransactionalSession().then(() => { return req.config.database.abortTransactionalSession().then(() => {
@@ -124,8 +126,23 @@ function handleBatch(router, req) {
} else { } else {
return { response: results }; return { response: results };
} }
})
.catch(error => {
if (
error &&
error.response &&
error.response.find(
errorItem => typeof errorItem.error === 'object' && errorItem.error.code === 251
) &&
transactionRetries > 0
) {
return batch(transactionRetries - 1);
}
throw error;
}); });
}); });
};
return batch(5);
} }
module.exports = { module.exports = {