fix(upload): self-aborting watchdog + lower concurrency to end stalls
A folder upload with several large files could appear frozen for ~2 min: a few concurrent uploads stalled and the old 120s per-file timeout neither aborted the request (leaving zombie XHRs that exhaust the browser's per-host connection pool) nor recovered quickly. - uploadFileWithProgress now self-aborts on a stalled connection: the deadline resets on every upload-progress tick (a slow but *moving* transfer is fine), and once the body is sent the server gets a fixed window to respond; on a stall xhr.abort() frees the connection immediately — no zombie, no cascade. - Lower upload concurrency 4 -> 3 to reduce server contention from large concurrent uploads. - The outer per-file timeout is now just a generous backstop for a wedged delta worker / by-hash request. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -76,10 +76,28 @@ export function uploadFileWithProgress(
|
||||
xhr.open('POST', '/api/files/upload');
|
||||
xhr.withCredentials = true;
|
||||
for (const [k, v] of Object.entries(getCsrfHeaders())) xhr.setRequestHeader(k, v);
|
||||
|
||||
// Self-aborting watchdog so a stalled connection can never pin an upload
|
||||
// slot forever (and leave a zombie XHR holding one of the browser's few
|
||||
// per-host connections). While the body is uploading we reset the deadline
|
||||
// on every progress tick — a slow but *moving* transfer is fine; once the
|
||||
// body is fully sent we give the server a fixed window to respond. On a
|
||||
// stall we `xhr.abort()`, which frees the connection immediately.
|
||||
const SEND_STALL_MS = 30_000;
|
||||
const RESPONSE_MS = 60_000;
|
||||
let watchdog: ReturnType<typeof setTimeout>;
|
||||
const arm = (ms: number) => {
|
||||
clearTimeout(watchdog);
|
||||
watchdog = setTimeout(() => xhr.abort(), ms);
|
||||
};
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
onProgress(e.lengthComputable ? e.loaded / e.total : NaN);
|
||||
arm(SEND_STALL_MS);
|
||||
};
|
||||
xhr.upload.onload = () => arm(RESPONSE_MS); // body sent — wait for the server
|
||||
xhr.onload = () => {
|
||||
clearTimeout(watchdog);
|
||||
if (xhr.status >= 200 && xhr.status < 300) resolve();
|
||||
else {
|
||||
// Flag quota so a batch can stop early instead of retrying every file.
|
||||
@@ -88,7 +106,15 @@ export function uploadFileWithProgress(
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(new Error('upload failed: network error'));
|
||||
xhr.onerror = () => {
|
||||
clearTimeout(watchdog);
|
||||
reject(new Error('upload failed: network error'));
|
||||
};
|
||||
xhr.onabort = () => {
|
||||
clearTimeout(watchdog);
|
||||
reject(new Error('upload stalled — aborted'));
|
||||
};
|
||||
arm(SEND_STALL_MS);
|
||||
xhr.send(form);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -291,14 +291,14 @@
|
||||
|
||||
// Upload at most this many files concurrently. Bounded so one stuck file
|
||||
// blocks only its own lane (the others keep going) without overwhelming the
|
||||
// browser's per-host connection cap or spawning too many delta workers.
|
||||
const UPLOAD_CONCURRENCY = 4;
|
||||
// browser's per-host connection cap, spawning too many delta workers, or
|
||||
// over-contending the server with many large concurrent uploads.
|
||||
const UPLOAD_CONCURRENCY = 3;
|
||||
|
||||
/** Per-file deadline (ms): a tiny file that stops responding fails after 2
|
||||
* min; larger files get proportionally longer (20 KB/s floor) so a slow but
|
||||
* progressing transfer is never killed. Stops a stuck file pinning its lane
|
||||
* forever. */
|
||||
const uploadDeadlineMs = (file: File) => Math.max(120_000, (file.size / (20 * 1024)) * 1000);
|
||||
/** Outer backstop deadline (ms). The plain-upload path already self-aborts on
|
||||
* a stalled connection (see `uploadFileWithProgress`); this only catches a
|
||||
* wedged delta worker or by-hash request so a lane can never hang forever. */
|
||||
const FILE_BACKSTOP_MS = 4 * 60_000;
|
||||
|
||||
/** Reject after `ms` if `p` hasn't settled. */
|
||||
function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
|
||||
@@ -388,7 +388,7 @@
|
||||
},
|
||||
owned.get(file) ?? null
|
||||
),
|
||||
uploadDeadlineMs(file)
|
||||
FILE_BACKSTOP_MS
|
||||
);
|
||||
} catch (e) {
|
||||
failures++;
|
||||
|
||||
Reference in New Issue
Block a user