Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 64 additions & 16 deletions src/engines/BandwidthEngine/BandwidthEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,27 @@ import {
type AuthorizationOptions
} from '../../utils/authorization';

const MAX_RETRIES = 20;
const MAX_RETRIES = 3;

class HttpError extends Error {
constructor(
readonly status: number,
statusText: string,
readonly retryAfter: string | null
) {
super(statusText);
}
}

const getRetryDelay = (retryAfter: string | null): number => {
if (!retryAfter) return 5000;

const seconds = Number(retryAfter);
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;

const date = Date.parse(retryAfter);
return Number.isNaN(date) ? 5000 : Math.max(0, date - Date.now());
};

const ESTIMATED_HEADER_FRACTION = 0.005; // ~.5% of packet header / payload size. used when transferSize is not available.

Expand Down Expand Up @@ -230,19 +250,23 @@ class BandwidthMeasurementEngine implements Engine {
set onFinished(f: (results: BandwidthEngineResults) => void) {
this.#onFinished = f;
}
#onConnectionError: (error: string) => void = () => {}; // Invoked when unable to get a response from the API
set onConnectionError(f: (error: string) => void) {
#onConnectionError: (error: string, status?: number) => void = () => {}; // Invoked when unable to get a response from the API
set onConnectionError(f: (error: string, status?: number) => void) {
this.#onConnectionError = f;
}

// Public methods
pause(): void {
if (this.#retryTimeout !== undefined) {
clearTimeout(this.#retryTimeout);
this.#retryTimeout = undefined;
}
this.#cancelCurrentMeasurement(`pause()`);
this.#setRunning(false);
}

play(): void {
if (!this.#running) {
if (!this.#failed && !this.#running) {
this.#setRunning(true);
this.#nextMeasurement();
}
Expand All @@ -254,11 +278,13 @@ class BandwidthMeasurementEngine implements Engine {
#uploadApi: string;

#running: boolean = false;
#failed: boolean = false;
#finished: Record<string, boolean> = { down: false, up: false };
#results: BandwidthEngineResults = { down: {}, up: {} };
#measIdx: number = 0;
#counter: number = 0;
#retries: number = 0;
#retryTimeout: ReturnType<typeof setTimeout> | undefined;
#minDuration: number = -Infinity; // of current measurement
#throttleMs: number = 0;
#estimatedServerTime: number = 0;
Expand Down Expand Up @@ -397,7 +423,11 @@ class BandwidthMeasurementEngine implements Engine {
url
);

if (this.#retries === 0) {
if (
this.#retries === 0 ||
!this.#currentAbortController ||
this.#currentAbortController.signal.aborted
) {
// abort existing abort controller
this.#currentAbortController?.abort('restarting engine');

Expand All @@ -406,9 +436,9 @@ class BandwidthMeasurementEngine implements Engine {
if (this.abortRequestDuration) {
const abortTimeout = setTimeout(() => {
const errorMessage = `${isDown ? 'Download' : 'Upload'} measurement of ${numBytes} bytes aborted. Measurement exceeded bandwidthAbortRequestDuration (${this.abortRequestDuration}ms)`;
this.#cancelCurrentMeasurement(errorMessage);
this.#retries = 0;
this.#setRunning(false);
this.pause();
this.#failed = true;
this.#onConnectionError(errorMessage);
}, this.abortRequestDuration);
this.#currentAbortController.signal.addEventListener('abort', () =>
Expand All @@ -424,7 +454,11 @@ class BandwidthMeasurementEngine implements Engine {
})
.then(r => {
if (r.ok) return r;
throw Error(r.statusText);
throw new HttpError(
r.status,
r.statusText,
r.headers.get('retry-after')
);
})
.then(r => {
this.getServerTime && (serverTime = this.getServerTime(r));
Expand Down Expand Up @@ -567,15 +601,29 @@ class BandwidthMeasurementEngine implements Engine {
}
console.warn(`Error fetching ${url}: ${error}`);
Comment thread
devandrepascoa marked this conversation as resolved.

if (this.#retries++ < MAX_RETRIES) {
this.#nextMeasurement(); // keep trying
} else {
this.#retries = 0;
this.#setRunning(false);
this.#onConnectionError(
`Connection failed to ${url}. Gave up after ${MAX_RETRIES} retries.`
);
if (
Comment thread
devandrepascoa marked this conversation as resolved.
error instanceof HttpError &&
error.status === 429 &&
this.#retries++ < MAX_RETRIES
) {
this.#retryTimeout = setTimeout(() => {
this.#retryTimeout = undefined;
if (this.#running && !this.#failed) this.#nextMeasurement();
}, getRetryDelay(error.retryAfter));
return;
}

const status = error instanceof HttpError ? error.status : undefined;
const message =
status === 429
? `Connection failed to ${url}. Gave up after ${MAX_RETRIES} retries.`
: status === undefined
? `Connection failed to ${url}.`
: `Request failed with ${status}: ${url}`;
this.#retries = 0;
this.pause();
this.#failed = true;
this.#onConnectionError(message, status);
});
}

Expand Down
12 changes: 10 additions & 2 deletions src/engines/BandwidthEngine/ParallelLatency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,19 @@ class BandwidthWithParallelLatencyEngine extends BandwidthEngine {
};
}

set onConnectionError(onConnectionError: (error: string) => void) {
super.onConnectionError = (...args: [string]) => {
set onConnectionError(
onConnectionError: (error: string, status?: number) => void
) {
super.onConnectionError = (...args: [string, number?]) => {
Comment thread
devandrepascoa marked this conversation as resolved.
this.#latencyEngine && this.#latencyEngine.pause();
onConnectionError(...args);
};
if (this.#latencyEngine) {
this.#latencyEngine.onConnectionError = (...args: [string, number?]) => {
this.pause();
onConnectionError(...args);
};
}
}

// Internal state
Expand Down
24 changes: 13 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ class MeasurementEngine {
this.#onFinish = f;
}

#onError: (message: string) => void = () => {};
#onError: (message: string, status?: number) => void = () => {};

set onError(f: (message: string) => void) {
set onError(f: (message: string, status?: number) => void) {
Comment thread
devandrepascoa marked this conversation as resolved.
this.#onError = f;
}

Expand All @@ -201,7 +201,7 @@ class MeasurementEngine {
* resumes the current phase.
*/
play(): void {
if (!this.#running) {
if (!this.#failed && !this.#running) {
// Clear timings before running the engine
performance.clearResourceTimings();

Expand Down Expand Up @@ -244,6 +244,7 @@ class MeasurementEngine {

#running: boolean = false;
#finished: boolean = false;
#failed: boolean = false;

// Internal methods
#setRunning(running: boolean): void {
Expand Down Expand Up @@ -297,6 +298,7 @@ class MeasurementEngine {

this.#setRunning(false);
this.#setFinished(false);
this.#failed = false;

this.#results.clear();
this.#accumulatedRuntimeMs = 0;
Expand Down Expand Up @@ -561,12 +563,12 @@ class MeasurementEngine {
this.#running && this.#next();
};

engine.onConnectionError = (e: unknown) => {
this.#serverTimeDelta = (engine as BandwidthEngine).serverTimeDelta;
engine.onConnectionError = (e: unknown, status?: number) => {
msmResults.error = e;
this.#failed = true;
this.pause();
this.onResultsChange({ type });
this.#onError(`Connection error while measuring latency: ${e}`);
this.#next();
this.#onError(String(e), status);
};

(engine as Engine & { play: () => void }).play!();
Expand Down Expand Up @@ -716,12 +718,12 @@ class MeasurementEngine {
this.#running && this.#next();
};

engine.onConnectionError = (e: unknown) => {
this.#serverTimeDelta = (engine as BandwidthEngine).serverTimeDelta;
engine.onConnectionError = (e: unknown, status?: number) => {
msmResults.error = e;
this.#failed = true;
this.pause();
this.onResultsChange({ type });
this.#onError(`Connection error while measuring ${type}: ${e}`);
this.#next();
this.#onError(String(e), status);
};

(engine as Engine & { play: () => void }).play!();
Expand Down
Loading