Hetzner checks answer again; failed checks go to the console (closes #114)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The Hetzner speed-test servers close the connection without an answer when the URL has a query string, and every check added ?_cb= and the time, so all six showed unreachable. Checks now fetch each target's URL as written; cache: "no-store" still keeps the browser's cache out of the measurement. Each recorded check that fails writes one console.error line, and the same line to the debug log: the target's name and URL, the time, what failed and how long the request took. A target that answers after failed checks writes one console.info line. Checks the page does not record write nothing, so the debug log no longer lists failures in the first round or the recovery probe. Model: opus-5-5
This commit was merged in pull request #116.
This commit is contained in:
+51
-12
@@ -268,6 +268,8 @@ export class HostState {
|
||||
this.lastLatency = null;
|
||||
this.status = "pending"; // 'online' | 'offline' | 'error' | 'pending'
|
||||
this.pinned = pinned;
|
||||
// How many recorded checks in a row have failed, up to the last one.
|
||||
this.consecutiveFailures = 0;
|
||||
}
|
||||
|
||||
pushSample(timestamp, result) {
|
||||
@@ -281,6 +283,9 @@ export class HostState {
|
||||
if (result.error === "timeout") this.status = "error";
|
||||
else if (result.error) this.status = "offline";
|
||||
else this.status = "online";
|
||||
this.consecutiveFailures = result.error
|
||||
? this.consecutiveFailures + 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
pushPaused(timestamp) {
|
||||
@@ -534,7 +539,13 @@ class Reporter {
|
||||
|
||||
// Checks one target. The check times out after CONFIG.requestTimeout; the
|
||||
// caller can give it up sooner through the optional signal, which also ends
|
||||
// it as a timeout.
|
||||
// it as a timeout. A failed check's reason says what went wrong and after
|
||||
// how long; a check that answered has none.
|
||||
//
|
||||
// The URL is fetched as written, with nothing added to it: the Hetzner
|
||||
// speed-test servers close the connection without an answer when the URL
|
||||
// has a query string, and cache: "no-store" keeps the browser's cache out
|
||||
// of the measurement.
|
||||
export async function measureLatency(url, signal) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(
|
||||
@@ -543,13 +554,10 @@ export async function measureLatency(url, signal) {
|
||||
);
|
||||
signal?.addEventListener("abort", () => controller.abort());
|
||||
|
||||
const targetUrl = new URL(url);
|
||||
targetUrl.searchParams.set("_cb", Date.now().toString());
|
||||
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
await fetch(targetUrl.toString(), {
|
||||
await fetch(url, {
|
||||
method: "GET",
|
||||
mode: "no-cors",
|
||||
cache: "no-store",
|
||||
@@ -558,18 +566,28 @@ export async function measureLatency(url, signal) {
|
||||
const latency = Math.round(performance.now() - start);
|
||||
clearTimeout(timeoutId);
|
||||
if (latency > CONFIG.maxLatency) {
|
||||
log.error(`${url} timeout (${latency}ms > ${CONFIG.maxLatency}ms)`);
|
||||
return { latency: null, error: "timeout" };
|
||||
return {
|
||||
latency: null,
|
||||
error: "timeout",
|
||||
reason: `answered after ${latency} ms, over the ${CONFIG.maxLatency} ms limit`,
|
||||
};
|
||||
}
|
||||
return { latency, error: null };
|
||||
return { latency, error: null, reason: null };
|
||||
} catch (err) {
|
||||
const took = Math.round(performance.now() - start);
|
||||
clearTimeout(timeoutId);
|
||||
if (err.name === "AbortError") {
|
||||
log.error(`${url} timeout (aborted)`);
|
||||
return { latency: null, error: "timeout" };
|
||||
return {
|
||||
latency: null,
|
||||
error: "timeout",
|
||||
reason: `timed out after ${took} ms (limit ${CONFIG.requestTimeout} ms)`,
|
||||
};
|
||||
}
|
||||
log.error(`${url} unreachable`);
|
||||
return { latency: null, error: "unreachable" };
|
||||
return {
|
||||
latency: null,
|
||||
error: "unreachable",
|
||||
reason: `network error (${err.name}: ${err.message}) after ${took} ms`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1136,6 +1154,26 @@ function sortAndRebuildWAN(state) {
|
||||
|
||||
// --- Main Loop ---------------------------------------------------------------
|
||||
|
||||
// Writes one line to the browser console, and the same line to the debug
|
||||
// log, for a check the page is about to record: for every check that
|
||||
// failed, and for a check that answered after checks that failed. Called
|
||||
// before host.pushSample, while host.consecutiveFailures still counts the
|
||||
// checks before this one.
|
||||
function logCheck(host, result) {
|
||||
const target = `${host.name} ${host.url} at ${new Date().toISOString()}`;
|
||||
const failed = host.consecutiveFailures;
|
||||
if (result.error) {
|
||||
const line = `netwatch: check failed: ${target}: ${result.reason}`;
|
||||
console.error(line);
|
||||
log.error(line);
|
||||
} else if (failed > 0) {
|
||||
const checks = failed === 1 ? "check" : "checks";
|
||||
const line = `netwatch: target recovered: ${target}: answered after ${result.latency} ms, following ${failed} failed ${checks} in a row`;
|
||||
console.info(line);
|
||||
log.info(line);
|
||||
}
|
||||
}
|
||||
|
||||
export async function tick(state, signal, onOffline) {
|
||||
const ts = Date.now();
|
||||
|
||||
@@ -1169,6 +1207,7 @@ export async function tick(state, signal, onOffline) {
|
||||
if (state.paused || signal.aborted || state.tickCount === 0) {
|
||||
return;
|
||||
}
|
||||
logCheck(host, r);
|
||||
host.pushSample(ts, r);
|
||||
updateHostRow(host, state.allHosts.indexOf(host));
|
||||
log.debug(`${host.name}: ${r.error ? r.error : r.latency + "ms"}`);
|
||||
|
||||
Reference in New Issue
Block a user