Skip to content

fix(model): stop advertising providers on stale egress-health evidence - #433

Merged
Ryanmello07 merged 1 commit into
urnetwork:mainfrom
Ryanmello07:fix/egress-health-staleness
Aug 12, 2026
Merged

fix(model): stop advertising providers on stale egress-health evidence#433
Ryanmello07 merged 1 commit into
urnetwork:mainfrom
Ryanmello07:fix/egress-health-staleness

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

The bug

GetAllProviderEgressHealthCounts reads provider_egress_health with no age bound. It selects the whole table, loads measured_at into ProviderEgressHealthCounts — and nothing ever reads that field again. passesHealth checks only the ratio.

So a health measurement, once taken, is permanent. A provider measured healthy months ago still passes the gate today.

The sibling loader has always been bounded. GetAllProviderEgressCountryCodes filters WHERE observed_at >= $1 against ProviderEgressLocationMaxAge. Location evidence expires; health evidence never did — and health is the half that detects a blackhole.

Why nothing caught it

The re-probe schedule keys off the wrong table. Both passes of GetProviderEgressLocationDueSharded are driven by provider_egress_location, so a provider with a fresh location is never re-offered no matter how old its health tally is. Location is trusted for 7 days; health for 1. A provider probed once and then quietly going dark keeps its passing tally and its place in the list until its location ages out, days later.

It fails silently by construction: such a provider stays connected and keeps accepting client connections. Nothing downstream can tell it apart from a working one.

Measured, not theorised

On a live deployment:

health evidence age providers passing the gate
under 6h 36
6–24h 1,208
1–3 days 1,093
over 3 days 200

98.6% were advertised on evidence older than six hours. I took the twelve with the stalest evidence — all currently advertised — and probed them:

019fedb3-7baa…  ok=0/131
019fedb3-752e…  ok=0/131
019fedb3-7640…  ok=0/131
…  12 of 12

Total blackholes, in the public list, on the strength of a measurement taken days earlier.

The fix

1. Bound the evidence. GetAllProviderEgressHealthCounts filters on a new ProviderEgressHealthMaxAge. A provider whose measurement aged out is absent from the map and fails closed — identically to one never measured, which is the correct reading: both mean no current evidence this provider carries traffic.

2. Re-probe on health age. A third due-queue pass offers providers whose health has gone stale, keyed on provider_egress_health. Without it, half 1 is a slow drain rather than a fix — every gated provider would age out of the map and never be re-measured, and the list would empty.

24h for health against 7 days for location, because they decay differently. Where a provider egresses from is a property of its network and rarely changes. Whether it still carries traffic is a property of the moment and changes with no signal at all. The constant is a floor on how bad the list can get: the longest a provider can blackhole while still being advertised.

A defect the tests caught

My first version scoped pass 3 to "no fresh health row". That predicate is also true of a provider never measured, so it made every fixture in TestGetProviderEgressLocationDue due, including one probed an hour ago — re-probing on the absence of a row rather than on evidence going stale.

Pass 3 is now scoped to providers that have a health row which aged out. A provider with a location but no health row is left to passes 1 and 2: excluded from the list meanwhile (passesHealth fails closed on a missing row) and re-offered when its location goes stale — deferred, not stranded.

Passes 1 and 2 keep their predicates, ordering and index scans untouched.

Verification

Run against a live postgres:

  • All six due-queue tests pass, including two new ones: stale health is offered, never-measured is not.
  • TestGetProviderEgressLocationDueOffersStaleHealth was confirmed to fail when pass 3 is disabled (due = []), so it has teeth.
  • Health, provider-count, find-providers and client-score tests pass unchanged (125s of execution).
  • go build ./model/... ./api/... ./controller/..., go vet ./model/, gofmt -l all clean.

Deployed to beta ahead of this PR: 5,942 stale rows dropped out of the gate, the advertised set moved to tracking the fresh-and-passing population, and providers were observed expiring live — 1,207 → 1,186 as a cohort crossed 24h. Under the old code they would have stayed advertised indefinitely.

Related, not in this PR

The deployment where this was found had every provider going dark at exactly 24h — jwt.expiryDuration is 24h and those clients hold static tokens with no refresh. That's a client-side matter and is separately fixed in sdk (half-life rotation, 2026-08-06). This PR is the server-side defence: whatever the cause, a provider that has stopped carrying traffic should stop being advertised, and it now does.

The health gate decides whether a provider is published, and it read
provider_egress_health with no age bound at all. GetAllProviderEgressHealthCounts
selected the whole table, loaded measured_at into the struct, and never looked at
it again. A measurement taken once was therefore permanent: passesHealth kept
returning true for a provider that had long since stopped carrying traffic.

Nothing else caught it, because the re-probe schedule keyed off the WRONG table.
Both passes of the due queue are driven by provider_egress_location, so a
provider with a fresh location was never re-offered however old its health tally
was -- and location is trusted for 7 days against health's 1. A provider probed
once, then quietly going dark, kept its passing tally and its place in the list
for days.

Measured on a live deployment: 98.6% of gated providers were advertised on
evidence older than six hours, some of it three days old. Twelve were sampled
from the stalest cohort and probed -- every one answered ok=0/131. Total
blackholes, in the public list, on the strength of a measurement taken days
earlier. They still accept client connections, which is why nothing downstream
noticed.

Two halves, and both are needed:

  - GetAllProviderEgressHealthCounts bounds on ProviderEgressHealthMaxAge. A
    provider whose measurement aged out is absent from the map and fails closed,
    identically to one never measured -- both mean "no current evidence this
    provider carries traffic". GetAllProviderEgressCountryCodes has always
    bounded its half this way; this is the missing symmetry.

  - A third due-queue pass offers providers whose health has gone stale, keyed on
    provider_egress_health rather than provider_egress_location. Without it the
    bound above would be a slow drain rather than a fix: every gated provider
    would age out of the map and never be re-measured, and the list would empty.

24h for health against 7 days for location because they decay differently. Where
a provider egresses from is a property of its network. Whether it still carries
traffic is a property of the moment and changes with no signal at all -- a
provider that stops forwarding stays connected and keeps accepting clients. The
constant is a floor on how bad the list can get: the longest a provider can
blackhole while still being advertised.

Pass 3 is scoped to providers that HAVE a health row which aged out, not to
every provider lacking fresh health. The looser predicate is also true of a
provider never measured, and it made every fixture in
TestGetProviderEgressLocationDue due -- including one probed an hour ago -- by
re-probing on the absence of a row rather than on evidence going stale. A
provider with a location but no health row is left to passes 1 and 2: excluded
from the list meanwhile, and re-offered when its location goes stale, so it is
deferred rather than stranded.

Passes 1 and 2 keep their predicates, ordering and index scans untouched.

Verified against a live database: the six due-queue tests pass, including two
new ones pinning that stale health IS offered and never-measured is NOT. The
first was confirmed to fail when pass 3 is disabled. The health, provider-count,
find-providers and client-score tests pass unchanged.
@Ryanmello07
Ryanmello07 merged commit 84540ba into urnetwork:main Aug 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant