fix(orb): back off per-failure relay retries to avoid a fleet-wide storm (#1950) - #1977
Conversation
…orm (#1950) retryFailedRelays already caps per-tick work and per-row attempts, but a row was eligible on every ~2-min tick regardless of when it last failed. During a sustained container outage every self-host re-POSTs its whole failed backlog every 2 min — a synchronized storm on the central Orb while it is degraded. Skip a row whose last_attempt_at is under RELAY_RETRY_BACKOFF_MINUTES (5) old; never-attempted rows (NULL) stay immediately eligible so a transient blip still recovers next tick. No schema change. The backoff bound is a modifier param so the pg-dialect rewrite keeps it portable across SQLite and Postgres.
|
Tip 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Gittensory review result - approve/merge recommendedReview updated: 2026-07-01 04:30:22 UTC
✅ Suggested Action - Approve/Merge
Review summary Nits — 4 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1977 +/- ##
=======================================
Coverage 95.57% 95.57%
=======================================
Files 218 218
Lines 24257 24258 +1
Branches 8795 8795
=======================================
+ Hits 23184 23185 +1
Misses 436 436
Partials 637 637
🚀 New features to boost your workflow:
|
Summary
retryFailedRelays(theretry-orb-relaycron, ~every 2 min) already bounds its per-tick work (RELAY_RETRY_BATCH_SIZE) and per-row attempts (5 / 1h TTL), but a row was eligible on every tick regardless of when it last failed. During a sustained outage of a brokered container, every self-host in the fleet re-POSTs its whole failed-relay backlog every 2 minutes — a synchronized outbound storm against the central Orb exactly when it is already degraded.This adds a per-failure backoff: a row whose
last_attempt_atis underRELAY_RETRY_BACKOFF_MINUTES(5) old is skipped until a later tick, so a down container is re-attempted at most ~every 5 min instead of every 2. Never-attempted rows (last_attempt_at IS NULL) stay immediately eligible, so a transient blip still recovers on the very next tick. No schema change — the store already hasattempts+last_attempt_at.Portability: the backoff bound is a modifier parameter (
datetime('now', ?)), which the self-host PG dialect rewrites tonow() + (?)::interval(linesrc/selfhost/pg-dialect.ts:44), so the placeholder count/order is unchanged across SQLite (D1) and Postgres.Advances #1950 (rec #15 from the #1936 audit) and meets its acceptance criteria: per-instance POST rate decays during a sustained outage, and the backlog still drains within a bounded number of ticks after recovery.
Scope
CONTRIBUTING.md; nosite//CNAME.Validation
git diff --checknpm run typechecknpm run test:coverage— the changedretryFailedRelaysselect is exercised by the existingorb-relayintegration suite; added two regression tests covering both sides of the backoff (a row still inside the window is skipped and not re-POSTed; a row past the window is retried and its attempts increment). Thelast_attempt_at IS NULLimmediate-eligibility path stays covered by the existing store→retry tests.npm run test:cinpm audit --audit-level=moderateIf any required check was skipped, explain why:
ui:openapi/cf-typegen/ migration regen: reuses the existingorb_relay_failures.last_attempt_atcolumn; no API/schema/binding/DB change.Safety
Notes
(1 << attempts)inside thedatetime()modifier, whose parentheses break the pg-dialect rewrite regex). Fixed backoff already satisfies the "rate decays" acceptance criterion; exponential can be a follow-up if the fleet needs a longer tail.