mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-05 16:26:15 +02:00
fix(downloads): improved handling for large file downloads and user-initiated cancellation (#632)
* fix(downloads): increase retry attempts and backoff for large file downloads * fix download retry config and abort handling * use abort reason to detect user-initiated cancels
This commit is contained in:
parent
215eba3156
commit
fa93ed51e2
|
|
@ -54,6 +54,11 @@ export class RunDownloadJob {
|
||||||
totalBytes: 0,
|
totalBytes: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track whether cancellation was explicitly requested by the user (via Redis signal
|
||||||
|
// or in-process AbortController). BullMQ lock mismatches can also abort the download
|
||||||
|
// stream, but those should be retried — only user-initiated cancels are unrecoverable.
|
||||||
|
let userCancelled = false
|
||||||
|
|
||||||
// Poll Redis for cancel signal every 2s — independent of progress events so cancellation
|
// Poll Redis for cancel signal every 2s — independent of progress events so cancellation
|
||||||
// works even when the stream is stalled and no onProgress ticks are firing.
|
// works even when the stream is stalled and no onProgress ticks are firing.
|
||||||
let cancelPollInterval: ReturnType<typeof setInterval> | null = setInterval(async () => {
|
let cancelPollInterval: ReturnType<typeof setInterval> | null = setInterval(async () => {
|
||||||
|
|
@ -61,7 +66,8 @@ export class RunDownloadJob {
|
||||||
const val = await cancelRedis.get(RunDownloadJob.cancelKey(job.id!))
|
const val = await cancelRedis.get(RunDownloadJob.cancelKey(job.id!))
|
||||||
if (val) {
|
if (val) {
|
||||||
await cancelRedis.del(RunDownloadJob.cancelKey(job.id!))
|
await cancelRedis.del(RunDownloadJob.cancelKey(job.id!))
|
||||||
abortController.abort()
|
userCancelled = true
|
||||||
|
abortController.abort('user-cancel')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Redis errors are non-fatal; in-process AbortController covers same-process cancels
|
// Redis errors are non-fatal; in-process AbortController covers same-process cancels
|
||||||
|
|
@ -176,8 +182,10 @@ export class RunDownloadJob {
|
||||||
filepath,
|
filepath,
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// If this was a cancellation abort, don't let BullMQ retry
|
// Only prevent retries for user-initiated cancellations. BullMQ lock mismatches
|
||||||
if (error?.message?.includes('aborted') || error?.message?.includes('cancelled')) {
|
// can also abort the stream, and those should be retried with backoff.
|
||||||
|
// Check both the flag (Redis poll) and abort reason (in-process cancel).
|
||||||
|
if (userCancelled || abortController.signal.reason === 'user-cancel') {
|
||||||
throw new UnrecoverableError(`Download cancelled: ${error.message}`)
|
throw new UnrecoverableError(`Download cancelled: ${error.message}`)
|
||||||
}
|
}
|
||||||
throw error
|
throw error
|
||||||
|
|
@ -228,8 +236,8 @@ export class RunDownloadJob {
|
||||||
try {
|
try {
|
||||||
const job = await queue.add(this.key, params, {
|
const job = await queue.add(this.key, params, {
|
||||||
jobId,
|
jobId,
|
||||||
attempts: 3,
|
attempts: 10,
|
||||||
backoff: { type: 'exponential', delay: 2000 },
|
backoff: { type: 'exponential', delay: 30000 },
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ export class DownloadService {
|
||||||
await RunDownloadJob.signalCancel(jobId)
|
await RunDownloadJob.signalCancel(jobId)
|
||||||
|
|
||||||
// Also try in-memory abort (works if worker is in same process)
|
// Also try in-memory abort (works if worker is in same process)
|
||||||
RunDownloadJob.abortControllers.get(jobId)?.abort()
|
RunDownloadJob.abortControllers.get(jobId)?.abort('user-cancel')
|
||||||
RunDownloadJob.abortControllers.delete(jobId)
|
RunDownloadJob.abortControllers.delete(jobId)
|
||||||
|
|
||||||
// Poll for terminal state (up to 4s at 250ms intervals) — cooperates with BullMQ's lifecycle
|
// Poll for terminal state (up to 4s at 250ms intervals) — cooperates with BullMQ's lifecycle
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user