Reputation: 379
My mobile app uploads several files to the server in succession, often from remote areas with questionable connection strength. For this reason, I want to make a few attempts to send the file. I also want to move on and attempt the next one in the event of a failure, with all error messages displayed at the end of the export (ie "10 files uploaded, 3 failed...")
However, I'm having trouble figuring out the recursive retry pattern with promises. Here's what I have so far:
sendFile(params, retries = 3){
console.log("SENDING FILE: ", retries, "attempts remaining", params)
return new Promise((resolve, reject)=>{
if(retries > 0){
this._sendFile(params)
.then(()=>{
// Upload Success
console.log("Upload Success!")
resolve()
})
.catch((err)=>{
console.log("Upload Fail", err)
// Retry
this.sendFile(params, --retries)
})
}else{
console.log("Failed 3 times!!!")
//Record error
this.exportStatus.errors.push({
message:"A file failed to upload after 3 attempts.",
params: params
})
//Resolve and move on to next export
resolve()
}
})
}
_sendFile(params){
// Mobile - File Transfer
let options = {
fileKey: "file_transfer",
fileName: params.fileName,
httpMethod: "PUT",
chunkedMode: false,
headers: {
"x-user-email":this.settings.user.email,
"x-user-token":this.settings.user.authentication_token,
}
}
let fileTransfer = this.transfer.create()
let url = encodeURI(this.settings.api_endpoint + params.url)
return fileTransfer.upload(params.file, url, options, true)
}
When I raise an exception on the server, I'll see the "Failed 3 times!!!" error message, but the calling promise does not resolve for the rest of the export to move on. I believe this is because I'm created nested promises (ie creating a new promise with each retry). How can I have the original promise resolve after 3 retries?
Thanks!
Upvotes: 5
Views: 16101
Reputation: 2460
This is a minor improvement on the answer provided by DarkNeuron, that does not create a timeout before first attempt, and uses a constant configurable delay between retries.
const MAX_RETRIES_DEFAULT = 5
export async function promiseRetry<T>(
fn: () => Promise<T>,
retries = MAX_RETRIES_DEFAULT,
retryIntervalMillis: number,
previousError?: Error
): Promise<T> {
return !retries
? Promise.reject(previousError)
: fn().catch(async (error) => {
await new Promise((resolve) => setTimeout(resolve, retryIntervalMillis))
return promiseRetry(fn, retries - 1, retryIntervalMillis, error)
})
}
Upvotes: 2
Reputation: 51756
You could implement a wrapper for Promise()
that automatically chains retries for you, allowing you to refactor your code with whatever logic you need and not worry about handling retry logic simultaneously. Your usage could look something like this:
sendFile(params, retries = 3) {
return Promise.retry(retries, (resolve, reject) => {
this._sendFile(params).then(resolve, reject)
})
}
Below is how you could implement Promise.retry()
:
Object.defineProperty(Promise, 'retry', {
configurable: true,
writable: true,
value: function retry (retries, executor) {
console.log(`${retries} retries left!`)
if (typeof retries !== 'number') {
throw new TypeError('retries is not a number')
}
return new Promise(executor).catch(error => retries > 0
? Promise.retry(retries - 1, executor)
: Promise.reject(error)
)
}
})
Promise.retry(100, (resolve, reject) => {
// your sendFile core logic with proper
// calls to resolve and reject goes here
const rand = Math.random()
console.log(rand)
if (rand < 0.1) resolve(rand)
else reject(rand)
}).then(
value => console.log(`resolved: ${value}`),
error => console.log(`rejected: ${error}`)
)
If you're uncomfortable extending a native object (this would be the correct way to do so, since it is a configurable, non-enumerable and writable property), you can just implement it as a static function:
function retry (retries, executor) {
console.log(`${retries} retries left!`)
if (typeof retries !== 'number') {
throw new TypeError('retries is not a number')
}
return new Promise(executor).catch(error => retries > 0
? retry(retries - 1, executor)
: Promise.reject(error)
)
}
Upvotes: 3
Reputation: 8682
Here's a simple version for completeness sake:
export async function promiseRetry<T>(fn: () => Promise<T>, retries = 5, err?: any): Promise<T> {
await new Promise(resolve => setTimeout(resolve, (5 - retries) * 1000));
return !retries ? Promise.reject(err) : fn().catch(error => promiseRetry(fn, (retries - 1), error));
}
Has built-in retry logic, which can be commented out if not needed.
Usage:
const myVal = await promiseRetry(() => myPromiseFn())
Upvotes: 1
Reputation: 1175
A follow up on what Patrick Roberts was posted, an example of how to implement it in typescript:
type promiseExecutor<T> = (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void;
class RetryablePromise<T> extends Promise<T> {
static retry<T>(retries: number, executor: promiseExecutor<T>): Promise<T> {
return new RetryablePromise(executor).catch(error =>
retries > 0 ? RetryablePromise.retry(retries - 1, executor) : RetryablePromise.reject(error)
);
}
}
and usage is as follows:
RetryablePromise.retry(4, (resolve, reject) => console.log('run'));
Upvotes: 2
Reputation: 379
Here's what ended up working:
sendFile(params, retries = 3, promise = null){
console.log("SENDING FILE: ", retries, "attempts remaining", params)
if(retries > 0){
return this._sendFile(params)
.then(()=>{
// Upload Success
console.log("Upload Success!")
return Promise.resolve(true)
})
.catch((err)=>{
console.log("Upload Fail", err)
this.exportStatus.retries++
return this.sendFile(params, --retries) // <-- The important part
})
}else{
console.log("Failed 3 times!!!")
this.exportStatus.errors.push({
message:"A file failed to upload after 3 attempts.",
params: params
})
return Promise.resolve(false)
}
}
Upvotes: 5