fix(notifications): harden stall monitor lifecycle
This commit is contained in:
@@ -6190,11 +6190,10 @@ export class DownloadManager extends EventEmitter {
|
||||
this.speedBytesPerPackage.clear();
|
||||
this.speedEventsHead = 0;
|
||||
this.lastGlobalProgressBytes = 0;
|
||||
this.lastGlobalProgressAt = nowMs();
|
||||
this.lastReconnectMarkAt = 0;
|
||||
this.consecutiveReconnects = 0;
|
||||
this.globalSpeedLimitQueue = Promise.resolve();
|
||||
this.globalSpeedLimitNextAt = 0;
|
||||
this.lastGlobalProgressAt = nowMs();
|
||||
this.lastReconnectMarkAt = 0;
|
||||
this.consecutiveReconnects = 0;
|
||||
this.resetGlobalSpeedLimitState();
|
||||
this.summary = null;
|
||||
this.nonResumableActive = 0;
|
||||
this.persistSoon();
|
||||
@@ -6304,11 +6303,10 @@ export class DownloadManager extends EventEmitter {
|
||||
this.speedBytesPerPackage.clear();
|
||||
this.speedEventsHead = 0;
|
||||
this.lastGlobalProgressBytes = 0;
|
||||
this.lastGlobalProgressAt = nowMs();
|
||||
this.lastReconnectMarkAt = 0;
|
||||
this.consecutiveReconnects = 0;
|
||||
this.globalSpeedLimitQueue = Promise.resolve();
|
||||
this.globalSpeedLimitNextAt = 0;
|
||||
this.lastGlobalProgressAt = nowMs();
|
||||
this.lastReconnectMarkAt = 0;
|
||||
this.consecutiveReconnects = 0;
|
||||
this.resetGlobalSpeedLimitState();
|
||||
this.summary = null;
|
||||
this.nonResumableActive = 0;
|
||||
this.persistSoon();
|
||||
@@ -6461,11 +6459,10 @@ export class DownloadManager extends EventEmitter {
|
||||
this.speedEvents = [];
|
||||
this.speedBytesLastWindow = 0;
|
||||
this.speedBytesPerPackage.clear();
|
||||
this.speedEventsHead = 0;
|
||||
this.lastGlobalProgressBytes = 0;
|
||||
this.lastGlobalProgressAt = nowMs();
|
||||
this.globalSpeedLimitQueue = Promise.resolve();
|
||||
this.globalSpeedLimitNextAt = 0;
|
||||
this.speedEventsHead = 0;
|
||||
this.lastGlobalProgressBytes = 0;
|
||||
this.lastGlobalProgressAt = nowMs();
|
||||
this.resetGlobalSpeedLimitState();
|
||||
this.summary = null;
|
||||
this.nonResumableActive = 0;
|
||||
this.persistSoon();
|
||||
@@ -12359,9 +12356,23 @@ export class DownloadManager extends EventEmitter {
|
||||
|
||||
private cachedSpeedLimitAt = 0;
|
||||
|
||||
private globalSpeedLimitQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
private globalSpeedLimitNextAt = 0;
|
||||
private globalSpeedLimitQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
private globalSpeedLimitNextAt = 0;
|
||||
|
||||
private globalSpeedLimitHealthNextAt = 0;
|
||||
|
||||
private globalSpeedLimitPending = 0;
|
||||
|
||||
private globalSpeedLimitGeneration = 0;
|
||||
|
||||
private resetGlobalSpeedLimitState(): void {
|
||||
this.globalSpeedLimitGeneration += 1;
|
||||
this.globalSpeedLimitQueue = Promise.resolve();
|
||||
this.globalSpeedLimitNextAt = 0;
|
||||
this.globalSpeedLimitHealthNextAt = 0;
|
||||
this.globalSpeedLimitPending = 0;
|
||||
}
|
||||
|
||||
private getEffectiveSpeedLimitKbps(): number {
|
||||
const now = nowMs();
|
||||
@@ -12407,62 +12418,79 @@ export class DownloadManager extends EventEmitter {
|
||||
|
||||
private async applyGlobalSpeedLimit(chunkBytes: number, bytesPerSecond: number, active?: ActiveTask): Promise<void> {
|
||||
const signal = active?.abortController.signal;
|
||||
const generation = this.globalSpeedLimitGeneration;
|
||||
const queuedAt = nowMs();
|
||||
const durationMs = Math.max(1, Math.ceil((chunkBytes / bytesPerSecond) * 1000));
|
||||
const healthReadyAt = Math.max(queuedAt, this.globalSpeedLimitNextAt, this.globalSpeedLimitHealthNextAt);
|
||||
this.globalSpeedLimitHealthNextAt = healthReadyAt + durationMs;
|
||||
this.globalSpeedLimitPending += 1;
|
||||
if (active && healthReadyAt > queuedAt) {
|
||||
active.blockedOnThrottleUntil = Math.max(active.blockedOnThrottleUntil || 0, healthReadyAt);
|
||||
}
|
||||
const task = this.globalSpeedLimitQueue
|
||||
.catch(() => undefined)
|
||||
.then(async () => {
|
||||
if (signal?.aborted) {
|
||||
throw new Error("aborted:speed_limit");
|
||||
}
|
||||
.catch(() => undefined)
|
||||
.then(async () => {
|
||||
if (generation !== this.globalSpeedLimitGeneration) {
|
||||
throw new Error("aborted:speed_limit_generation");
|
||||
}
|
||||
if (signal?.aborted) {
|
||||
throw new Error("aborted:speed_limit");
|
||||
}
|
||||
const now = nowMs();
|
||||
const waitMs = Math.max(0, this.globalSpeedLimitNextAt - now);
|
||||
if (waitMs > 0) {
|
||||
if (active) {
|
||||
active.blockedOnThrottleUntil = now + waitMs;
|
||||
}
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let timer: NodeJS.Timeout | null = setTimeout(() => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let timer: NodeJS.Timeout | null = setTimeout(() => {
|
||||
timer = null;
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
resolve();
|
||||
}, waitMs);
|
||||
|
||||
const onAbort = (): void => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
resolve();
|
||||
}, waitMs);
|
||||
|
||||
const onAbort = (): void => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
reject(new Error("aborted:speed_limit"));
|
||||
};
|
||||
|
||||
if (signal) {
|
||||
if (signal.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
if (active) {
|
||||
active.blockedOnThrottleUntil = 0;
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
reject(new Error("aborted:speed_limit"));
|
||||
};
|
||||
|
||||
if (signal) {
|
||||
if (signal.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (generation !== this.globalSpeedLimitGeneration) {
|
||||
throw new Error("aborted:speed_limit_generation");
|
||||
}
|
||||
if (signal?.aborted) {
|
||||
throw new Error("aborted:speed_limit");
|
||||
}
|
||||
|
||||
const startAt = Math.max(nowMs(), this.globalSpeedLimitNextAt);
|
||||
const durationMs = Math.max(1, Math.ceil((chunkBytes / bytesPerSecond) * 1000));
|
||||
this.globalSpeedLimitNextAt = startAt + durationMs;
|
||||
});
|
||||
|
||||
this.globalSpeedLimitQueue = task;
|
||||
await task;
|
||||
}
|
||||
throw new Error("aborted:speed_limit");
|
||||
}
|
||||
|
||||
const startAt = Math.max(nowMs(), this.globalSpeedLimitNextAt);
|
||||
this.globalSpeedLimitNextAt = startAt + durationMs;
|
||||
});
|
||||
|
||||
this.globalSpeedLimitQueue = task;
|
||||
try {
|
||||
await task;
|
||||
} finally {
|
||||
if (active && active.blockedOnThrottleUntil === healthReadyAt) {
|
||||
active.blockedOnThrottleUntil = 0;
|
||||
}
|
||||
if (generation === this.globalSpeedLimitGeneration) {
|
||||
this.globalSpeedLimitPending = Math.max(0, this.globalSpeedLimitPending - 1);
|
||||
if (this.globalSpeedLimitPending === 0) {
|
||||
this.globalSpeedLimitHealthNextAt = Math.max(nowMs(), this.globalSpeedLimitNextAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async applySpeedLimit(chunkBytes: number, localWindowBytes: number, localWindowStarted: number, active?: ActiveTask): Promise<void> {
|
||||
const signal = active?.abortController.signal;
|
||||
@@ -14092,11 +14120,10 @@ export class DownloadManager extends EventEmitter {
|
||||
this.claimedTargetPathByItem.clear();
|
||||
this.itemContributedBytes.clear();
|
||||
this.speedEvents = [];
|
||||
this.speedEventsHead = 0;
|
||||
this.speedBytesLastWindow = 0;
|
||||
this.speedBytesPerPackage.clear();
|
||||
this.globalSpeedLimitQueue = Promise.resolve();
|
||||
this.globalSpeedLimitNextAt = 0;
|
||||
this.speedEventsHead = 0;
|
||||
this.speedBytesLastWindow = 0;
|
||||
this.speedBytesPerPackage.clear();
|
||||
this.resetGlobalSpeedLimitState();
|
||||
this.nonResumableActive = 0;
|
||||
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
|
||||
this.lastGlobalProgressAt = nowMs();
|
||||
|
||||
Reference in New Issue
Block a user