-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Basically rewrite after testing update-conflict
- Loading branch information
1 parent
d8d60bd
commit 974d567
Showing
8 changed files
with
216 additions
and
135 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains 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 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,70 @@ | ||
import { AxiosError, AxiosRequestConfig } from 'axios'; | ||
import isRetryAllowed from 'is-retry-allowed'; | ||
import { UserPayload } from '../services/user-payload'; | ||
import { ChtApi } from './cht-api'; | ||
|
||
const RETRY_COUNT = 4; | ||
|
||
export const axiosRetryConfig = { | ||
retries: RETRY_COUNT, | ||
retryDelay: () => 1000, | ||
retryCondition: (err: AxiosError) => { | ||
const status = err.response?.status; | ||
return (!status || status >= 500) && isRetryAllowed(err); | ||
}, | ||
onRetry: (retryCount: number, error: AxiosError, requestConfig: AxiosRequestConfig) => { | ||
console.log(`${requestConfig.url} failure. Retrying (${retryCount})`); | ||
}, | ||
}; | ||
|
||
export async function retryOnUpdateConflict<T>(funcWithPut: () => Promise<T>): Promise<T> { | ||
for (let retryCount = 0; retryCount < RETRY_COUNT; retryCount++) { | ||
try { | ||
return await funcWithPut(); | ||
} catch (err : any) { | ||
const statusCode = err.response?.status; | ||
if (statusCode === 409) { | ||
console.log(`Retrying on update-conflict (${retryCount})`); | ||
continue; | ||
} | ||
|
||
throw err; | ||
} | ||
} | ||
|
||
throw Error('update-conflict 409 persisted'); | ||
} | ||
|
||
export async function createUserWithRetries(userPayload: UserPayload, chtApi: ChtApi): Promise<{ username: string; password: string }> { | ||
for (let retryCount = 0; retryCount < RETRY_COUNT; ++retryCount) { | ||
try { | ||
await chtApi.createUser(userPayload); | ||
return userPayload; | ||
} catch (err: any) { | ||
if (axiosRetryConfig.retryCondition(err)) { | ||
continue; | ||
} | ||
|
||
if (err.response?.status !== 400) { | ||
throw err; | ||
} | ||
|
||
const translationKey = err.response?.data?.error?.translationKey; | ||
console.error('createUser retry because', translationKey); | ||
if (translationKey === 'username.taken') { | ||
userPayload.makeUsernameMoreComplex(); | ||
continue; | ||
} | ||
|
||
const RETRY_PASSWORD_TRANSLATIONS = ['password.length.minimum', 'password.weak']; | ||
if (RETRY_PASSWORD_TRANSLATIONS.includes(translationKey)) { | ||
userPayload.regeneratePassword(); | ||
continue; | ||
} | ||
|
||
throw err; | ||
} | ||
} | ||
|
||
throw new Error('could not create user ' + userPayload.contact); | ||
} |
This file contains 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 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 was deleted.
Oops, something went wrong.
This file contains 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,125 @@ | ||
import Chai from 'chai'; | ||
import sinon from 'sinon'; | ||
|
||
import * as RetryLogic from '../../src/lib/retry-logic'; | ||
import { UserPayload } from '../../src/services/user-payload'; | ||
import { ChtApi } from '../../src/lib/cht-api'; | ||
import { mockSimpleContactType } from '../mocks'; | ||
|
||
import chaiAsPromised from 'chai-as-promised'; | ||
Chai.use(chaiAsPromised); | ||
|
||
const { expect } = Chai; | ||
|
||
const RetryScenarios = [ | ||
{ desc: '503', axiosError: { response: { status: 503 } }, retry: 'axios' }, | ||
{ desc: 'axios timeout', axiosError: { code: 'ECONNABORTED' }, retry: 'axios' }, | ||
{ desc: 'service timeout', axiosError: { code: 'ECONNRESET' }, retry: 'axios' }, | ||
|
||
{ desc: 'update conflict', axiosError: { code: 'ERR_BAD_REQUEST', response: { status: 409 } }, retry: 'update-conflict' }, | ||
|
||
{ | ||
desc: 'username taken is not retried', | ||
axiosError: { | ||
code: 'ERR_BAD_REQUEST', | ||
response: { | ||
status: 400, | ||
data: { error: { message: 'Username "chu" already taken.', translationKey: 'username.taken' } }, | ||
} | ||
}, | ||
retry: 'upload-manager' | ||
}, | ||
{ | ||
desc: 'password too short', | ||
axiosError: { | ||
code: 'ERR_BAD_REQUEST', | ||
response: { | ||
status: 400, | ||
data: { error: { message: 'The password must be at least 8 characters long.', translationKey: 'password.length.minimum' } }, | ||
} | ||
}, | ||
retry: 'upload-manager' | ||
}, | ||
{ | ||
desc: 'password too weak', | ||
axiosError: { | ||
code: 'ERR_BAD_REQUEST', | ||
response: { | ||
status: 400, | ||
data: { | ||
error: { | ||
message: 'The password is too easy to guess. Include a range of types of characters to increase the score.', | ||
translationKey: 'password.weak' | ||
} | ||
}, | ||
} | ||
}, | ||
retry: 'upload-manager' | ||
}, | ||
]; | ||
|
||
export const UploadManagerRetryScenario = RetryScenarios[RetryScenarios.length - 1]; | ||
const UpdateConflictScenario = RetryScenarios.find(s => s.retry === 'update-conflict'); | ||
|
||
describe('lib/retry-logic', () => { | ||
describe('axiosRetryConfig', () => { | ||
for (const scenario of RetryScenarios) { | ||
it(scenario.desc, () => { | ||
const doRetry = RetryLogic.axiosRetryConfig.retryCondition(scenario.axiosError as any); | ||
expect(doRetry).to.eq(scenario.retry === 'axios'); | ||
}); | ||
} | ||
}); | ||
|
||
describe('retryOnUpdateConflict', () => { | ||
for (const scenario of RetryScenarios) { | ||
it(scenario.desc, async () => { | ||
const output = 'foo'; | ||
const testFunction = sinon.stub() | ||
.rejects(scenario.axiosError) | ||
.onSecondCall().resolves(output); | ||
const execute = RetryLogic.retryOnUpdateConflict<string>(testFunction); | ||
const expectRetry = scenario.retry === 'update-conflict'; | ||
if (expectRetry) { | ||
await expect(execute).to.eventually.eq(output); | ||
expect(testFunction.callCount).to.eq(expectRetry ? 2 : 1); | ||
} else { | ||
await expect(execute).to.eventually.be.rejectedWith(scenario.axiosError); | ||
} | ||
}); | ||
} | ||
|
||
it ('throws after persistent conflict', async () => { | ||
const testFunction = sinon.stub().rejects(UpdateConflictScenario?.axiosError); | ||
const execute = RetryLogic.retryOnUpdateConflict<string>(testFunction); | ||
await expect(execute).to.eventually.be.rejectedWith('persisted'); | ||
expect(testFunction.callCount).to.eq(4); | ||
}); | ||
}); | ||
|
||
describe('createUserWithRetries', () => { | ||
for (const scenario of RetryScenarios) { | ||
it(scenario.desc, async() => { | ||
const chtApi = { | ||
createUser: sinon.stub().throws(scenario.axiosError), | ||
}; | ||
const place = { | ||
generateUsername: sinon.stub().returns('username'), | ||
type: mockSimpleContactType('string', 'bar'), | ||
contact: { properties: {} }, | ||
}; | ||
const userPayload = new UserPayload(place as Place, 'place_id', 'contact_id'); | ||
|
||
const execute = RetryLogic.createUserWithRetries(userPayload as UserPayload, chtApi as ChtApi); | ||
const expectRetry = ['upload-manager', 'axios'].includes(scenario.retry); | ||
if (expectRetry) { | ||
await expect(execute).to.eventually.be.rejectedWith('could not create user'); | ||
expect(chtApi.createUser.callCount).to.eq(4); | ||
} else { | ||
await expect(execute).to.eventually.be.rejectedWith(scenario.axiosError); | ||
expect(chtApi.createUser.callCount).to.eq(1); | ||
} | ||
}); | ||
} | ||
}); | ||
}); |
Oops, something went wrong.