feat: optional instant retry before marking a monitor down

Adds per-monitor `retries` and `retryDelayMs` options. When a check fails,
the monitor is re-checked up to `retries` extra times (waiting `retryDelayMs`
between attempts, default 5000ms) before being reported as down. This filters
out transient blips / false positives, and affects both the status page state
and downtime notifications.

Backward compatible: default `retries: 0` keeps the current behavior (no retry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pull/212/head
LsM Lucas Martins 2026-07-23 18:10:07 -03:00
parent a5670e51cb
commit eb449a335f
3 changed files with 68 additions and 42 deletions

View File

@ -45,6 +45,11 @@ export type MonitorTarget = {
responseForbiddenKeyword?: string responseForbiddenKeyword?: string
checkProxy?: string checkProxy?: string
checkProxyFallback?: boolean checkProxyFallback?: boolean
// [OPTIONAL] Instant retry on failure: number of extra attempts before reporting
// the monitor as down (default 0 = no retry). Filters out transient blips.
retries?: number
// [OPTIONAL] Delay in ms between a failed attempt and the retry (default 5000).
retryDelayMs?: number
} }
export type WorkerConfig<TEnv = Env> = { export type WorkerConfig<TEnv = Env> = {

View File

@ -74,6 +74,11 @@ const workerConfig: WorkerConfig = {
checkProxy: 'https://xxx.example.com OR worker://weur', checkProxy: 'https://xxx.example.com OR worker://weur',
// [OPTIONAL] if true, the check will fallback to local if the specified proxy is down // [OPTIONAL] if true, the check will fallback to local if the specified proxy is down
checkProxyFallback: true, checkProxyFallback: true,
// [OPTIONAL] instant retry on failure: number of extra attempts before reporting
// the monitor as down, to filter out transient blips (default 0 = no retry)
retries: 1,
// [OPTIONAL] delay in ms between a failed attempt and the retry (default 5000)
retryDelayMs: 5000,
}, },
// Example TCP Monitor // Example TCP Monitor
{ {

View File

@ -356,7 +356,16 @@ export async function getStatus(
export async function doMonitor(monitor: MonitorTarget, defaultLocation: string, env: Env) { export async function doMonitor(monitor: MonitorTarget, defaultLocation: string, env: Env) {
let checkLocation = defaultLocation let checkLocation = defaultLocation
let status let status: { ping: number; up: boolean; err: string } = { ping: 0, up: false, err: '' }
// Instant retry: if a check fails, re-check before reporting the monitor as down,
// to filter out transient blips (false positives). Controlled per-monitor via
// `retries` / `retryDelayMs` (default 0 retries = previous behavior, no retry).
const maxAttempts = 1 + (monitor.retries ?? 0)
const retryDelayMs = monitor.retryDelayMs ?? 5000
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
checkLocation = defaultLocation
if (monitor.checkProxy) { if (monitor.checkProxy) {
// Initiate a check using proxy (Geo-specific monitoring) // Initiate a check using proxy (Geo-specific monitoring)
@ -404,6 +413,13 @@ export async function doMonitor(monitor: MonitorTarget, defaultLocation: string,
status = await getStatus(monitor) status = await getStatus(monitor)
} }
if (status.up || attempt === maxAttempts) break
console.log(
`[${monitor.id}] Attempt ${attempt}/${maxAttempts} failed (${status.err}); retrying in ${retryDelayMs}ms...`
)
await new Promise((resolve) => setTimeout(resolve, retryDelayMs))
}
console.log(`[${monitor.id}] Check result from ${checkLocation}: up=${status.up}, ping=${status.ping}, err=${status.err}`) console.log(`[${monitor.id}] Check result from ${checkLocation}: up=${status.up}, ping=${status.ping}, err=${status.err}`)
return { return {