Skip to content

Commit 8b97c13

Browse files
authored
Batch transaction (#5849)
* Batch transaction boilerplate * Refactoring transaction boilerplate * Independent sessions test * Transactions - partial * Missing only one test * All tests passing for mongo db * Tests on Travis * Transactions on postgres * Fix travis to restart mongodb * Remove mongodb service and keep only mongodb runner * MongoDB service back * Initialize replicaset * Remove mongodb runner again * Again only with mongodb-runner and removing cache * Trying with pretest and posttest * WiredTiger * Pretest and posttest again * Removing inexistent scripts * wiredTiger * One more attempt * Trying another way to run mongodb-runner * Fixing tests * Include batch transaction on direct access * Add tests to direct access
1 parent fe18fe0 commit 8b97c13

15 files changed

+931
-106
lines changed

.travis.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
language: node_js
22
dist: trusty
33
services:
4-
- mongodb
54
- postgresql
65
- redis-server
76
- docker
@@ -19,13 +18,12 @@ branches:
1918
cache:
2019
directories:
2120
- "$HOME/.npm"
22-
- "$HOME/.mongodb/versions"
2321
stage: test
2422
env:
2523
global:
2624
- COVERAGE_OPTION='./node_modules/.bin/nyc'
2725
matrix:
28-
- MONGODB_VERSION=4.0.4
26+
- MONGODB_VERSION=4.0.4 MONGODB_TOPOLOGY=replicaset MONGODB_STORAGE_ENGINE=wiredTiger
2927
- MONGODB_VERSION=3.6.9
3028
- PARSE_SERVER_TEST_DB=postgres
3129
- PARSE_SERVER_TEST_CACHE=redis
@@ -42,7 +40,6 @@ before_script:
4240
- psql -c 'create database parse_server_postgres_adapter_test_database;' -U postgres
4341
- psql -c 'CREATE EXTENSION postgis;' -U postgres -d parse_server_postgres_adapter_test_database
4442
- psql -c 'CREATE EXTENSION postgis_topology;' -U postgres -d parse_server_postgres_adapter_test_database
45-
- silent=1 mongodb-runner --start
4643
- greenkeeper-lockfile-update
4744
script:
4845
- npm run lint

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,10 @@
9595
"lint": "flow && eslint --cache ./",
9696
"build": "babel src/ -d lib/ --copy-files",
9797
"watch": "babel --watch src/ -d lib/ --copy-files",
98-
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_STORAGE_ENGINE=mmapv1 TESTING=1 jasmine",
99-
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_STORAGE_ENGINE=mmapv1 TESTING=1 nyc jasmine",
98+
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} mongodb-runner start",
99+
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} TESTING=1 jasmine",
100+
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} mongodb-runner stop",
101+
"coverage": "npm run pretest && cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} TESTING=1 nyc jasmine && npm run posttest",
100102
"start": "node ./bin/parse-server",
101103
"prepare": "npm run build",
102104
"postinstall": "node -p 'require(\"./postinstall.js\")()'"

spec/.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"jequal": true,
2626
"create": true,
2727
"arrayContains": true,
28-
"expectAsync": true
28+
"expectAsync": true,
29+
"databaseAdapter": true
2930
},
3031
"rules": {
3132
"no-console": [0],

spec/GridFSBucketStorageAdapter.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('GridFSBucket and GridStore interop', () => {
1818
beforeEach(async () => {
1919
const gsAdapter = new GridStoreAdapter(databaseURI);
2020
const db = await gsAdapter._connect();
21-
db.dropDatabase();
21+
await db.dropDatabase();
2222
});
2323

2424
it('a file created in GridStore should be available in GridFS', async () => {

spec/GridStoreAdapter.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe_only_db('mongo')('GridStoreAdapter', () => {
1313
const config = Config.get(Parse.applicationId);
1414
const gridStoreAdapter = new GridStoreAdapter(databaseURI);
1515
const db = await gridStoreAdapter._connect();
16-
db.dropDatabase();
16+
await db.dropDatabase();
1717
const filesController = new FilesController(
1818
gridStoreAdapter,
1919
Parse.applicationId,

spec/ParseServerRESTController.spec.js

Lines changed: 268 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const ParseServerRESTController = require('../lib/ParseServerRESTController')
22
.ParseServerRESTController;
33
const ParseServer = require('../lib/ParseServer').default;
44
const Parse = require('parse/node').Parse;
5+
const TestUtils = require('../lib/TestUtils');
56

67
let RESTController;
78

@@ -40,7 +41,7 @@ describe('ParseServerRESTController', () => {
4041
);
4142
});
4243

43-
it('should handle a POST batch', done => {
44+
it('should handle a POST batch without transaction', done => {
4445
RESTController.request('POST', 'batch', {
4546
requests: [
4647
{
@@ -69,6 +70,272 @@ describe('ParseServerRESTController', () => {
6970
);
7071
});
7172

73+
it('should handle a POST batch with transaction=false', done => {
74+
RESTController.request('POST', 'batch', {
75+
requests: [
76+
{
77+
method: 'GET',
78+
path: '/classes/MyObject',
79+
},
80+
{
81+
method: 'POST',
82+
path: '/classes/MyObject',
83+
body: { key: 'value' },
84+
},
85+
{
86+
method: 'GET',
87+
path: '/classes/MyObject',
88+
},
89+
],
90+
transaction: false,
91+
}).then(
92+
res => {
93+
expect(res.length).toBe(3);
94+
done();
95+
},
96+
err => {
97+
jfail(err);
98+
done();
99+
}
100+
);
101+
});
102+
103+
if (
104+
(process.env.MONGODB_VERSION === '4.0.4' &&
105+
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
106+
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger') ||
107+
process.env.PARSE_SERVER_TEST_DB === 'postgres'
108+
) {
109+
describe('transactions', () => {
110+
beforeAll(async () => {
111+
if (
112+
process.env.MONGODB_VERSION === '4.0.4' &&
113+
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
114+
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger'
115+
) {
116+
await reconfigureServer({
117+
databaseAdapter: undefined,
118+
databaseURI:
119+
'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset',
120+
});
121+
}
122+
});
123+
124+
beforeEach(async () => {
125+
await TestUtils.destroyAllDataPermanently(true);
126+
});
127+
128+
it('should handle a batch request with transaction = true', done => {
129+
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
130+
myObject
131+
.save()
132+
.then(() => {
133+
return myObject.destroy();
134+
})
135+
.then(() => {
136+
spyOn(databaseAdapter, 'createObject').and.callThrough();
137+
138+
RESTController.request('POST', 'batch', {
139+
requests: [
140+
{
141+
method: 'POST',
142+
path: '/1/classes/MyObject',
143+
body: { key: 'value1' },
144+
},
145+
{
146+
method: 'POST',
147+
path: '/1/classes/MyObject',
148+
body: { key: 'value2' },
149+
},
150+
],
151+
transaction: true,
152+
}).then(response => {
153+
expect(response.length).toEqual(2);
154+
expect(response[0].success.objectId).toBeDefined();
155+
expect(response[0].success.createdAt).toBeDefined();
156+
expect(response[1].success.objectId).toBeDefined();
157+
expect(response[1].success.createdAt).toBeDefined();
158+
const query = new Parse.Query('MyObject');
159+
query.find().then(results => {
160+
expect(databaseAdapter.createObject.calls.count()).toBe(2);
161+
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toBe(
162+
databaseAdapter.createObject.calls.argsFor(1)[3]
163+
);
164+
expect(results.map(result => result.get('key')).sort()).toEqual(
165+
['value1', 'value2']
166+
);
167+
done();
168+
});
169+
});
170+
});
171+
});
172+
173+
it('should not save anything when one operation fails in a transaction', done => {
174+
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
175+
myObject
176+
.save()
177+
.then(() => {
178+
return myObject.destroy();
179+
})
180+
.then(() => {
181+
RESTController.request('POST', 'batch', {
182+
requests: [
183+
{
184+
method: 'POST',
185+
path: '/1/classes/MyObject',
186+
body: { key: 'value1' },
187+
},
188+
{
189+
method: 'POST',
190+
path: '/1/classes/MyObject',
191+
body: { key: 10 },
192+
},
193+
],
194+
transaction: true,
195+
}).catch(error => {
196+
expect(error.message).toBeDefined();
197+
const query = new Parse.Query('MyObject');
198+
query.find().then(results => {
199+
expect(results.length).toBe(0);
200+
done();
201+
});
202+
});
203+
});
204+
});
205+
206+
it('should generate separate session for each call', async () => {
207+
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
208+
await myObject.save();
209+
await myObject.destroy();
210+
211+
const myObject2 = new Parse.Object('MyObject2'); // This is important because transaction only works on pre-existing collections
212+
await myObject2.save();
213+
await myObject2.destroy();
214+
215+
spyOn(databaseAdapter, 'createObject').and.callThrough();
216+
217+
let myObjectCalls = 0;
218+
Parse.Cloud.beforeSave('MyObject', async () => {
219+
myObjectCalls++;
220+
if (myObjectCalls === 2) {
221+
try {
222+
await RESTController.request('POST', 'batch', {
223+
requests: [
224+
{
225+
method: 'POST',
226+
path: '/1/classes/MyObject2',
227+
body: { key: 'value1' },
228+
},
229+
{
230+
method: 'POST',
231+
path: '/1/classes/MyObject2',
232+
body: { key: 10 },
233+
},
234+
],
235+
transaction: true,
236+
});
237+
fail('should fail');
238+
} catch (e) {
239+
expect(e).toBeDefined();
240+
}
241+
}
242+
});
243+
244+
const response = await RESTController.request('POST', 'batch', {
245+
requests: [
246+
{
247+
method: 'POST',
248+
path: '/1/classes/MyObject',
249+
body: { key: 'value1' },
250+
},
251+
{
252+
method: 'POST',
253+
path: '/1/classes/MyObject',
254+
body: { key: 'value2' },
255+
},
256+
],
257+
transaction: true,
258+
});
259+
260+
expect(response.length).toEqual(2);
261+
expect(response[0].success.objectId).toBeDefined();
262+
expect(response[0].success.createdAt).toBeDefined();
263+
expect(response[1].success.objectId).toBeDefined();
264+
expect(response[1].success.createdAt).toBeDefined();
265+
266+
await RESTController.request('POST', 'batch', {
267+
requests: [
268+
{
269+
method: 'POST',
270+
path: '/1/classes/MyObject3',
271+
body: { key: 'value1' },
272+
},
273+
{
274+
method: 'POST',
275+
path: '/1/classes/MyObject3',
276+
body: { key: 'value2' },
277+
},
278+
],
279+
});
280+
281+
const query = new Parse.Query('MyObject');
282+
const results = await query.find();
283+
expect(results.map(result => result.get('key')).sort()).toEqual([
284+
'value1',
285+
'value2',
286+
]);
287+
288+
const query2 = new Parse.Query('MyObject2');
289+
const results2 = await query2.find();
290+
expect(results2.length).toEqual(0);
291+
292+
const query3 = new Parse.Query('MyObject3');
293+
const results3 = await query3.find();
294+
expect(results3.map(result => result.get('key')).sort()).toEqual([
295+
'value1',
296+
'value2',
297+
]);
298+
299+
expect(databaseAdapter.createObject.calls.count()).toBe(5);
300+
let transactionalSession;
301+
let transactionalSession2;
302+
let myObjectDBCalls = 0;
303+
let myObject2DBCalls = 0;
304+
let myObject3DBCalls = 0;
305+
for (let i = 0; i < 5; i++) {
306+
const args = databaseAdapter.createObject.calls.argsFor(i);
307+
switch (args[0]) {
308+
case 'MyObject':
309+
myObjectDBCalls++;
310+
if (!transactionalSession) {
311+
transactionalSession = args[3];
312+
} else {
313+
expect(transactionalSession).toBe(args[3]);
314+
}
315+
if (transactionalSession2) {
316+
expect(transactionalSession2).not.toBe(args[3]);
317+
}
318+
break;
319+
case 'MyObject2':
320+
myObject2DBCalls++;
321+
transactionalSession2 = args[3];
322+
if (transactionalSession) {
323+
expect(transactionalSession).not.toBe(args[3]);
324+
}
325+
break;
326+
case 'MyObject3':
327+
myObject3DBCalls++;
328+
expect(args[3]).toEqual(null);
329+
break;
330+
}
331+
}
332+
expect(myObjectDBCalls).toEqual(2);
333+
expect(myObject2DBCalls).toEqual(1);
334+
expect(myObject3DBCalls).toEqual(2);
335+
});
336+
});
337+
}
338+
72339
it('should handle a POST request', done => {
73340
RESTController.request('POST', '/classes/MyObject', { key: 'value' })
74341
.then(() => {

0 commit comments

Comments
 (0)