fix: retry interval

This commit is contained in:
nate 2026-04-08 13:37:24 +04:00
parent 587442d034
commit 9425fb2454
5 changed files with 30 additions and 13 deletions

View File

@ -15,7 +15,7 @@ const MonitorBody = t.Object({
max_retries: t.Optional(t.Number({ minimum: 0, maximum: 10, default: 0, description: "Retry a failing check up to N times before declaring DOWN" })), max_retries: t.Optional(t.Number({ minimum: 0, maximum: 10, default: 0, description: "Retry a failing check up to N times before declaring DOWN" })),
retry_interval_s: t.Optional(t.Number({ minimum: 1, maximum: 600, default: 30, description: "Seconds between retries" })), retry_interval_s: t.Optional(t.Number({ minimum: 1, maximum: 600, default: 30, description: "Seconds between retries" })),
resend_interval: t.Optional(t.Number({ minimum: 0, maximum: 1000, default: 0, description: "Re-alert every Nth consecutive down beat. 0 = never resend." })), resend_interval: t.Optional(t.Number({ minimum: 0, maximum: 1000, default: 0, description: "Re-alert every Nth consecutive down beat. 0 = never resend." })),
cert_alert_days: t.Optional(t.Number({ minimum: 0, maximum: 365, default: 14, description: "Alert when TLS cert is within N days of expiry. 0 disables." })), cert_alert_days: t.Optional(t.Number({ minimum: 0, maximum: 365, default: 0, description: "Alert when TLS cert is within N days of expiry. 0 disables (default)." })),
query: t.Optional(t.Any({ description: "PingQL query — filter conditions for up/down" })), query: t.Optional(t.Any({ description: "PingQL query — filter conditions for up/down" })),
regions: t.Optional(t.Array(t.String(), { description: "Regions to run checks from. Empty array = all regions." })), regions: t.Optional(t.Array(t.String(), { description: "Regions to run checks from. Empty array = all regions." })),
channel_ids: t.Optional(t.Array(t.String(), { description: "Notification channel IDs to attach to this monitor." })), channel_ids: t.Optional(t.Array(t.String(), { description: "Notification channel IDs to attach to this monitor." })),
@ -58,6 +58,12 @@ export const monitors = new Elysia({ prefix: "/monitors" })
return { error: `Minimum interval for ${plan} plan is ${limits.minIntervalS}s` }; return { error: `Minimum interval for ${plan} plan is ${limits.minIntervalS}s` };
} }
const retryGap = body.retry_interval_s ?? Math.max(30, limits.minIntervalS);
if (retryGap < limits.minIntervalS) {
set.status = 400;
return { error: `Retry interval for ${plan} plan must be at least ${limits.minIntervalS}s` };
}
const regions = body.regions ?? []; const regions = body.regions ?? [];
if (regions.length > limits.maxRegions) { if (regions.length > limits.maxRegions) {
set.status = 400; set.status = 400;
@ -76,9 +82,9 @@ export const monitors = new Elysia({ prefix: "/monitors" })
${body.timeout_ms ?? 10000}, ${body.timeout_ms ?? 10000},
${interval}, ${interval},
${body.max_retries ?? 0}, ${body.max_retries ?? 0},
${body.retry_interval_s ?? 30}, ${retryGap},
${body.resend_interval ?? 0}, ${body.resend_interval ?? 0},
${body.cert_alert_days ?? 14}, ${body.cert_alert_days ?? 0},
${body.query ? sql.json(body.query) : null}, ${body.query ? sql.json(body.query) : null},
${sql.array(regions)} ${sql.array(regions)}
) )
@ -112,6 +118,11 @@ export const monitors = new Elysia({ prefix: "/monitors" })
return { error: `Minimum interval for ${plan} plan is ${limits.minIntervalS}s` }; return { error: `Minimum interval for ${plan} plan is ${limits.minIntervalS}s` };
} }
if (body.retry_interval_s != null && body.retry_interval_s < limits.minIntervalS) {
set.status = 400;
return { error: `Retry interval for ${plan} plan must be at least ${limits.minIntervalS}s` };
}
if (body.regions && body.regions.length > limits.maxRegions) { if (body.regions && body.regions.length > limits.maxRegions) {
set.status = 400; set.status = 400;
return { error: `Free plan allows ${limits.maxRegions} region per monitor. Upgrade to use multi-region.` }; return { error: `Free plan allows ${limits.maxRegions} region per monitor. Upgrade to use multi-region.` };

View File

@ -72,7 +72,8 @@ export async function migrate(sql: any) {
await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS max_retries INTEGER NOT NULL DEFAULT 0`; await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS max_retries INTEGER NOT NULL DEFAULT 0`;
await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS retry_interval_s INTEGER NOT NULL DEFAULT 30`; await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS retry_interval_s INTEGER NOT NULL DEFAULT 30`;
await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS resend_interval INTEGER NOT NULL DEFAULT 0`; await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS resend_interval INTEGER NOT NULL DEFAULT 0`;
await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS cert_alert_days INTEGER NOT NULL DEFAULT 14`; await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS cert_alert_days INTEGER NOT NULL DEFAULT 0`;
await sql`ALTER TABLE monitors ALTER COLUMN cert_alert_days SET DEFAULT 0`;
await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS important BOOLEAN NOT NULL DEFAULT false`; await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS important BOOLEAN NOT NULL DEFAULT false`;
// Per-region transition state. region='' for unspecified/single-region monitors. // Per-region transition state. region='' for unspecified/single-region monitors.

View File

@ -525,7 +525,7 @@ export const dashboard = new Elysia()
max_retries: Number(b.max_retries) || 0, max_retries: Number(b.max_retries) || 0,
retry_interval_s: Number(b.retry_interval_s) || 30, retry_interval_s: Number(b.retry_interval_s) || 30,
resend_interval: Number(b.resend_interval) || 0, resend_interval: Number(b.resend_interval) || 0,
cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 14, cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 0,
channel_ids: Array.isArray(b.channel_ids) ? b.channel_ids : (b.channel_ids ? [b.channel_ids] : []), channel_ids: Array.isArray(b.channel_ids) ? b.channel_ids : (b.channel_ids ? [b.channel_ids] : []),
regions, regions,
request_headers: Object.keys(requestHeaders).length ? requestHeaders : null, request_headers: Object.keys(requestHeaders).length ? requestHeaders : null,
@ -567,7 +567,7 @@ export const dashboard = new Elysia()
max_retries: Number(b.max_retries) || 0, max_retries: Number(b.max_retries) || 0,
retry_interval_s: Number(b.retry_interval_s) || 30, retry_interval_s: Number(b.retry_interval_s) || 30,
resend_interval: Number(b.resend_interval) || 0, resend_interval: Number(b.resend_interval) || 0,
cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 14, cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 0,
channel_ids: Array.isArray(b.channel_ids) ? b.channel_ids : (b.channel_ids ? [b.channel_ids] : []), channel_ids: Array.isArray(b.channel_ids) ? b.channel_ids : (b.channel_ids ? [b.channel_ids] : []),
regions, regions,
request_headers: Object.keys(requestHeaders).length ? requestHeaders : null, request_headers: Object.keys(requestHeaders).length ? requestHeaders : null,

View File

@ -162,7 +162,7 @@
<span class="k">"max_retries"</span>: <span class="n">2</span>, <span class="c">// optional — retry N times before declaring DOWN. Default: 0</span> <span class="k">"max_retries"</span>: <span class="n">2</span>, <span class="c">// optional — retry N times before declaring DOWN. Default: 0</span>
<span class="k">"retry_interval_s"</span>: <span class="n">30</span>, <span class="c">// optional — seconds between retries. Default: 30</span> <span class="k">"retry_interval_s"</span>: <span class="n">30</span>, <span class="c">// optional — seconds between retries. Default: 30</span>
<span class="k">"resend_interval"</span>: <span class="n">10</span>, <span class="c">// optional — re-alert every Nth consecutive DOWN beat. 0 = never. Default: 0</span> <span class="k">"resend_interval"</span>: <span class="n">10</span>, <span class="c">// optional — re-alert every Nth consecutive DOWN beat. 0 = never. Default: 0</span>
<span class="k">"cert_alert_days"</span>: <span class="n">14</span>, <span class="c">// optional — alert when TLS cert is within N days of expiry. 0 disables. Default: 14</span> <span class="k">"cert_alert_days"</span>: <span class="n">0</span>, <span class="c">// optional — alert when TLS cert is within N days of expiry. 0 disables. Default: 0 (disabled)</span>
<span class="k">"channel_ids"</span>: [<span class="s">"&lt;uuid&gt;"</span>], <span class="c">// optional — notification channels to attach</span> <span class="k">"channel_ids"</span>: [<span class="s">"&lt;uuid&gt;"</span>], <span class="c">// optional — notification channels to attach</span>
<span class="k">"query"</span>: { ... } <span class="c">// optional — see Query Language below</span> <span class="k">"query"</span>: { ... } <span class="c">// optional — see Query Language below</span>
}</pre> }</pre>
@ -181,7 +181,7 @@
<tr><td>max_retries</td><td>number</td><td>Retry a failing check this many times before posting a DOWN result. Default: 0. Max: 10. See <a href="#reliability">Reliability</a>.</td></tr> <tr><td>max_retries</td><td>number</td><td>Retry a failing check this many times before posting a DOWN result. Default: 0. Max: 10. See <a href="#reliability">Reliability</a>.</td></tr>
<tr><td>retry_interval_s</td><td>number</td><td>Seconds between retries. Default: 30.</td></tr> <tr><td>retry_interval_s</td><td>number</td><td>Seconds between retries. Default: 30.</td></tr>
<tr><td>resend_interval</td><td>number</td><td>If a monitor stays DOWN, re-fire a notification every Nth consecutive down beat. 0 disables resend. Default: 0.</td></tr> <tr><td>resend_interval</td><td>number</td><td>If a monitor stays DOWN, re-fire a notification every Nth consecutive down beat. 0 disables resend. Default: 0.</td></tr>
<tr><td>cert_alert_days</td><td>number</td><td>Fire a separate <code>cert</code> notification when the TLS certificate is within N days of expiring. 0 disables. Default: 14.</td></tr> <tr><td>cert_alert_days</td><td>number</td><td>Fire a separate <code>cert</code> notification when the TLS certificate is within N days of expiring. 0 disables. Default: 0 (disabled).</td></tr>
<tr><td>channel_ids</td><td>string[]</td><td>Notification channel IDs to attach. See <a href="#notifications">Notifications</a>.</td></tr> <tr><td>channel_ids</td><td>string[]</td><td>Notification channel IDs to attach. See <a href="#notifications">Notifications</a>.</td></tr>
<tr><td>query</td><td>object</td><td>Query conditions — see below</td></tr> <tr><td>query</td><td>object</td><td>Query conditions — see below</td></tr>
</tbody> </tbody>

View File

@ -94,12 +94,19 @@
<% }) %> <% }) %>
</select> </select>
</div> </div>
<%
const allRetryGaps = [['5','5 seconds'],['15','15 seconds'],['30','30 seconds'],['60','1 minute'],['120','2 minutes'],['300','5 minutes']];
const retryGaps = allRetryGaps.filter(([val]) => Number(val) >= minInterval);
const defaultRetryGap = String(Math.max(30, minInterval));
const curRetryRaw = Number(monitor.retry_interval_s);
const curRetry = (curRetryRaw >= minInterval) ? String(curRetryRaw) : defaultRetryGap;
%>
<div class="flex-1"> <div class="flex-1">
<label class="block text-sm text-gray-400 mb-1.5">Retry gap</label> <label class="block text-sm text-gray-400 mb-1.5">Retry gap</label>
<select id="<%= prefix %>retry-interval" name="retry_interval_s" <select id="<%= prefix %>retry-interval" name="retry_interval_s"
class="w-full <%= bg %> border <%= border %> rounded-lg px-4 py-2.5 text-gray-100 focus:outline-none focus:border-blue-500"> class="w-full <%= bg %> border <%= border %> rounded-lg px-4 py-2.5 text-gray-100 focus:outline-none focus:border-blue-500">
<% [['5','5 seconds'],['15','15 seconds'],['30','30 seconds'],['60','1 minute'],['120','2 minutes']].forEach(function([val, label]) { %> <% retryGaps.forEach(function([val, label]) { %>
<option value="<%= val %>" <%= String(monitor.retry_interval_s ?? '30') === val ? 'selected' : '' %>><%= label %></option> <option value="<%= val %>" <%= curRetry === val ? 'selected' : '' %>><%= label %></option>
<% }) %> <% }) %>
</select> </select>
</div> </div>
@ -116,9 +123,7 @@
<% <%
const certDaysRaw = Number(monitor.cert_alert_days); const certDaysRaw = Number(monitor.cert_alert_days);
const certDaysSel = certDaysRaw === 0 ? '0' const certDaysSel = (certDaysRaw >= 1 && certDaysRaw <= 9) ? String(certDaysRaw) : '0';
: (certDaysRaw >= 1 && certDaysRaw <= 9) ? String(certDaysRaw)
: '7';
%> %>
<div> <div>
<label class="block text-sm text-gray-400 mb-1.5">TLS cert expiry alert</label> <label class="block text-sm text-gray-400 mb-1.5">TLS cert expiry alert</label>