-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Add idempotency #6748
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
Merged
Merged
Add idempotency #6748
Changes from all commits
Commits
Show all changes
60 commits
Select commit
Hold shift + click to select a range
f257ba5
added idempotency router and middleware
mtrezza 2785a91
added idempotency rules for routes classes, functions, jobs, installa…
mtrezza 579abe5
fixed typo
mtrezza 3f51df7
ignore requests without header
mtrezza cdd4eb0
removed unused var
mtrezza c6a7c45
enabled feature only for MongoDB
mtrezza 4557fe9
changed code comment
mtrezza e3f40d1
fixed inconsistend storage adapter specification
mtrezza 827e0a8
Trigger notification
mtrezza d6c15a1
Travis CI trigger
mtrezza 472a84c
Travis CI trigger
mtrezza bd8333f
Travis CI trigger
mtrezza 7b0e93d
rebuilt option definitions
mtrezza e36a49f
fixed incorrect import path
mtrezza b2204b0
added new request ID header to allowed headers
mtrezza 772f942
fixed typescript typos
mtrezza ae928f3
add new system class to spec helper
mtrezza a6232c0
fixed typescript typos
mtrezza d3e7ee9
re-added postgres conn parameter
mtrezza 0d58fef
removed postgres conn parameter
mtrezza 9c62563
fixed incorrect schema for index creation
mtrezza 884fd35
temporarily disabling index creation to fix postgres issue
mtrezza 33fc25c
temporarily disabling index creation to fix postgres issue
mtrezza bf32dd1
temporarily disabling index creation to fix postgres issue
mtrezza 46f19b0
temporarily disabling index creation to fix postgres issue
mtrezza e14138e
temporarily disabling index creation to fix postgres issue
mtrezza f80730e
temporarily disabling index creation to fix postgres issue
mtrezza f02b3fd
temporarily disabling index creation to fix postgres issue
mtrezza 1e3ad15
trying to fix postgres issue
mtrezza 6175f41
fixed incorrect auth when writing to _Idempotency
mtrezza c7e2d19
trying to fix postgres issue
mtrezza e6536b0
Travis CI trigger
mtrezza 8f670d3
added test cases
mtrezza ae384b9
removed number grouping
mtrezza 8c06add
fixed test description
mtrezza 7b31767
trying to fix postgres issue
mtrezza 40a74f0
added Github readme docs
mtrezza 87c6432
added change log
mtrezza 5c6d30f
refactored tests; fixed some typos
mtrezza 8ee5e3e
fixed test case
mtrezza c0fb4ea
fixed default TTL value
mtrezza d409ea1
Travis CI Trigger
mtrezza 3ec141b
Travis CI Trigger
mtrezza 53d623a
Travis CI Trigger
mtrezza 4717663
added test case to increase coverage
mtrezza b69ca44
Trigger Travis CI
mtrezza 18b3186
changed configuration syntax to use regex; added test cases
mtrezza abc5c7e
removed unused vars
mtrezza 66fe783
removed IdempotencyRouter
mtrezza 38fc952
Trigger Travis CI
mtrezza 46107b7
updated docs
mtrezza 7bf5a6a
updated docs
mtrezza bf60c8c
updated docs
mtrezza a28685e
updated docs
mtrezza a661a3e
update docs
mtrezza b408ac5
Trigger Travis CI
mtrezza 220236f
fixed coverage
mtrezza 6c03028
removed code comments
mtrezza 401e584
Merge remote-tracking branch 'upstream/master' into add-idempotency
mtrezza bb2409a
Merge remote-tracking branch 'upstream/master' into add-idempotency
mtrezza File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
'use strict'; | ||
const Config = require('../lib/Config'); | ||
const Definitions = require('../lib/Options/Definitions'); | ||
const request = require('../lib/request'); | ||
const rest = require('../lib/rest'); | ||
const auth = require('../lib/Auth'); | ||
const uuid = require('uuid'); | ||
|
||
describe_only_db('mongo')('Idempotency', () => { | ||
// Parameters | ||
/** Enable TTL expiration simulated by removing entry instead of waiting for MongoDB TTL monitor which | ||
runs only every 60s, so it can take up to 119s until entry removal - ain't nobody got time for that */ | ||
const SIMULATE_TTL = true; | ||
// Helpers | ||
async function deleteRequestEntry(reqId) { | ||
const config = Config.get(Parse.applicationId); | ||
const res = await rest.find( | ||
config, | ||
auth.master(config), | ||
'_Idempotency', | ||
{ reqId: reqId }, | ||
{ limit: 1 } | ||
); | ||
await rest.del( | ||
config, | ||
auth.master(config), | ||
'_Idempotency', | ||
res.results[0].objectId); | ||
} | ||
async function setup(options) { | ||
await reconfigureServer({ | ||
appId: Parse.applicationId, | ||
masterKey: Parse.masterKey, | ||
serverURL: Parse.serverURL, | ||
idempotencyOptions: options, | ||
}); | ||
} | ||
// Setups | ||
beforeEach(async () => { | ||
if (SIMULATE_TTL) { jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000; } | ||
await setup({ | ||
paths: [ | ||
"functions/.*", | ||
"jobs/.*", | ||
"classes/.*", | ||
"users", | ||
"installations" | ||
], | ||
ttl: 30, | ||
}); | ||
}); | ||
// Tests | ||
it('should enforce idempotency for cloud code function', async () => { | ||
let counter = 0; | ||
Parse.Cloud.define('myFunction', () => { | ||
counter++; | ||
}); | ||
const params = { | ||
method: 'POST', | ||
url: 'http://localhost:8378/1/functions/myFunction', | ||
headers: { | ||
'X-Parse-Application-Id': Parse.applicationId, | ||
'X-Parse-Master-Key': Parse.masterKey, | ||
'X-Parse-Request-Id': 'abc-123' | ||
} | ||
}; | ||
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(30); | ||
await request(params); | ||
await request(params).then(fail, e => { | ||
expect(e.status).toEqual(400); | ||
expect(e.data.error).toEqual("Duplicate request"); | ||
}); | ||
expect(counter).toBe(1); | ||
}); | ||
|
||
it('should delete request entry after TTL', async () => { | ||
let counter = 0; | ||
Parse.Cloud.define('myFunction', () => { | ||
counter++; | ||
}); | ||
const params = { | ||
method: 'POST', | ||
url: 'http://localhost:8378/1/functions/myFunction', | ||
headers: { | ||
'X-Parse-Application-Id': Parse.applicationId, | ||
'X-Parse-Master-Key': Parse.masterKey, | ||
'X-Parse-Request-Id': 'abc-123' | ||
} | ||
}; | ||
await expectAsync(request(params)).toBeResolved(); | ||
if (SIMULATE_TTL) { | ||
await deleteRequestEntry('abc-123'); | ||
} else { | ||
await new Promise(resolve => setTimeout(resolve, 130000)); | ||
} | ||
await expectAsync(request(params)).toBeResolved(); | ||
expect(counter).toBe(2); | ||
}); | ||
|
||
it('should enforce idempotency for cloud code jobs', async () => { | ||
let counter = 0; | ||
Parse.Cloud.job('myJob', () => { | ||
counter++; | ||
}); | ||
const params = { | ||
method: 'POST', | ||
url: 'http://localhost:8378/1/jobs/myJob', | ||
headers: { | ||
'X-Parse-Application-Id': Parse.applicationId, | ||
'X-Parse-Master-Key': Parse.masterKey, | ||
'X-Parse-Request-Id': 'abc-123' | ||
} | ||
}; | ||
await expectAsync(request(params)).toBeResolved(); | ||
await request(params).then(fail, e => { | ||
expect(e.status).toEqual(400); | ||
expect(e.data.error).toEqual("Duplicate request"); | ||
}); | ||
expect(counter).toBe(1); | ||
}); | ||
|
||
it('should enforce idempotency for class object creation', async () => { | ||
let counter = 0; | ||
Parse.Cloud.afterSave('MyClass', () => { | ||
counter++; | ||
}); | ||
const params = { | ||
method: 'POST', | ||
url: 'http://localhost:8378/1/classes/MyClass', | ||
headers: { | ||
'X-Parse-Application-Id': Parse.applicationId, | ||
'X-Parse-Master-Key': Parse.masterKey, | ||
'X-Parse-Request-Id': 'abc-123' | ||
} | ||
}; | ||
await expectAsync(request(params)).toBeResolved(); | ||
await request(params).then(fail, e => { | ||
expect(e.status).toEqual(400); | ||
expect(e.data.error).toEqual("Duplicate request"); | ||
}); | ||
expect(counter).toBe(1); | ||
}); | ||
|
||
it('should enforce idempotency for user object creation', async () => { | ||
let counter = 0; | ||
Parse.Cloud.afterSave('_User', () => { | ||
counter++; | ||
}); | ||
const params = { | ||
method: 'POST', | ||
url: 'http://localhost:8378/1/users', | ||
body: { | ||
username: "user", | ||
password: "pass" | ||
}, | ||
headers: { | ||
'X-Parse-Application-Id': Parse.applicationId, | ||
'X-Parse-Master-Key': Parse.masterKey, | ||
'X-Parse-Request-Id': 'abc-123' | ||
} | ||
}; | ||
await expectAsync(request(params)).toBeResolved(); | ||
await request(params).then(fail, e => { | ||
expect(e.status).toEqual(400); | ||
expect(e.data.error).toEqual("Duplicate request"); | ||
}); | ||
expect(counter).toBe(1); | ||
}); | ||
|
||
it('should enforce idempotency for installation object creation', async () => { | ||
let counter = 0; | ||
Parse.Cloud.afterSave('_Installation', () => { | ||
counter++; | ||
}); | ||
const params = { | ||
method: 'POST', | ||
url: 'http://localhost:8378/1/installations', | ||
body: { | ||
installationId: "1", | ||
deviceType: "ios" | ||
}, | ||
headers: { | ||
'X-Parse-Application-Id': Parse.applicationId, | ||
'X-Parse-Master-Key': Parse.masterKey, | ||
'X-Parse-Request-Id': 'abc-123' | ||
} | ||
}; | ||
await expectAsync(request(params)).toBeResolved(); | ||
await request(params).then(fail, e => { | ||
expect(e.status).toEqual(400); | ||
expect(e.data.error).toEqual("Duplicate request"); | ||
}); | ||
expect(counter).toBe(1); | ||
}); | ||
|
||
it('should not interfere with calls of different request ID', async () => { | ||
let counter = 0; | ||
Parse.Cloud.afterSave('MyClass', () => { | ||
counter++; | ||
}); | ||
const promises = [...Array(100).keys()].map(() => { | ||
const params = { | ||
method: 'POST', | ||
url: 'http://localhost:8378/1/classes/MyClass', | ||
headers: { | ||
'X-Parse-Application-Id': Parse.applicationId, | ||
'X-Parse-Master-Key': Parse.masterKey, | ||
'X-Parse-Request-Id': uuid.v4() | ||
} | ||
}; | ||
return request(params); | ||
}); | ||
await expectAsync(Promise.all(promises)).toBeResolved(); | ||
expect(counter).toBe(100); | ||
}); | ||
|
||
it('should re-throw any other error unchanged when writing request entry fails for any other reason', async () => { | ||
spyOn(rest, 'create').and.rejectWith(new Parse.Error(0, "some other error")); | ||
Parse.Cloud.define('myFunction', () => {}); | ||
const params = { | ||
method: 'POST', | ||
url: 'http://localhost:8378/1/functions/myFunction', | ||
headers: { | ||
'X-Parse-Application-Id': Parse.applicationId, | ||
'X-Parse-Master-Key': Parse.masterKey, | ||
'X-Parse-Request-Id': 'abc-123' | ||
} | ||
}; | ||
await request(params).then(fail, e => { | ||
expect(e.status).toEqual(400); | ||
expect(e.data.error).toEqual("some other error"); | ||
}); | ||
}); | ||
|
||
it('should use default configuration when none is set', async () => { | ||
await setup({}); | ||
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(Definitions.IdempotencyOptions.ttl.default); | ||
expect(Config.get(Parse.applicationId).idempotencyOptions.paths).toBe(Definitions.IdempotencyOptions.paths.default); | ||
}); | ||
|
||
it('should throw on invalid configuration', async () => { | ||
await expectAsync(setup({ paths: 1 })).toBeRejected(); | ||
await expectAsync(setup({ ttl: 'a' })).toBeRejected(); | ||
await expectAsync(setup({ ttl: 0 })).toBeRejected(); | ||
await expectAsync(setup({ ttl: -1 })).toBeRejected(); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.