Skip to content

Commit

Permalink
refactor: Improve retry policy to improve success rate
Browse files Browse the repository at this point in the history
  • Loading branch information
ci010 committed Mar 29, 2024
1 parent 5b1a83b commit 2ca93d7
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 1 deletion.
2 changes: 1 addition & 1 deletion xmcl
91 changes: 91 additions & 0 deletions xmcl-runtime/network/pluginNetworkInterface.ts
Expand Up @@ -102,9 +102,100 @@ export const pluginNetworkInterface: LauncherAppPlugin = (app) => {
}), new JsonCacheStorage(cache))
setGlobalDispatcher(apiDispatcher)

function calculateRetryAfterHeader(retryAfter: number) {
const current = Date.now()
const diff = new Date(retryAfter).getTime() - current

return diff
}

const downloadAgentOptions = getDefaultAgentOptions({
maxTimeout: 60_000,
maxRetries: 30,
// @ts-ignore
retry: (err, { state, opts }, cb) => {
const { statusCode, code, headers } = err as any
const { method, retryOptions } = opts
const {
maxRetries,
timeout,
maxTimeout,
timeoutFactor,
statusCodes,
errorCodes,
methods,
} = retryOptions! as any
let { counter, currentTimeout } = state

currentTimeout =
currentTimeout != null && currentTimeout > 0 ? currentTimeout : timeout

// Any code that is not a Undici's originated and allowed to retry
if (
code &&
code !== 'UND_ERR_REQ_RETRY' &&
code !== 'UND_ERR_SOCKET' &&
!errorCodes!.includes(code)
) {
if (code !== 'UND_ERR_CONNECT_TIMEOUT') {
cb(err)
return
}
// Check if there are connection with the same origin
// If there are, this error is usually due to high traffic, and we should retry
// @ts-ignore
const clients = downloadProxy.agent[kClients] as Map<string, Pool>
if (!opts.origin) {
cb(err)
return
}
const pool = clients.get(typeof opts.origin === 'string' ? opts.origin : opts.origin.origin)
const stats = pool?.stats
if (!stats?.connected && !stats?.pending && !stats?.running && !stats?.queued && !stats?.free) {
cb(err)
return
}
}

// If a set of method are provided and the current method is not in the list
if (Array.isArray(methods) && !methods.includes(method)) {
cb(err)
return
}

// If a set of status code are provided and the current status code is not in the list
if (
statusCode != null &&
Array.isArray(statusCodes) &&
!statusCodes.includes(statusCode)
) {
cb(err)
return
}

// If we reached the max number of retries
if (counter > (maxRetries ?? 5)) {
cb(err)
return
}

let retryAfterHeader = headers?.['retry-after']
if (retryAfterHeader) {
retryAfterHeader = Number(retryAfterHeader)
retryAfterHeader = Number.isNaN(retryAfterHeader)
? calculateRetryAfterHeader(retryAfterHeader)
: retryAfterHeader * 1e3 // Retry-After is in seconds
}

const retryTimeout =
retryAfterHeader > 0
? Math.min(retryAfterHeader, (maxTimeout!))
: Math.min(currentTimeout * timeoutFactor! ** counter, maxTimeout!)

state.currentTimeout = retryTimeout

setTimeout(() => cb(null), retryTimeout)
},
})
const downloadProxy = new ProxyAgent({
controller: proxy,
Expand Down

0 comments on commit 2ca93d7

Please sign in to comment.