Skip to content

Commit 3c8fd65

Browse files
cansofgreaseclaude
andcommitted
Fix a pinned server dragging its stand-ins to the wrong city
Pinning your own provider's server and switching on Best of 3 fetched the two extra servers from your own city instead of from beside the pin. On a link whose address registers in one city while the connection actually leaves through another, that put the stand-ins hundreds of kilometres away, and one of them usually won the round and got recorded in the pin's place. Ookla is asked where a server is by its id, and that answer quietly hands back the caller's own position whenever the server asked about belongs to the caller's provider - which is the usual reason anyone pins one. The answer carries a distance alongside it, and for that substituted reply the distance is exactly zero: a server claiming to sit where you are standing. Servers genuinely a few kilometres away report those few kilometres, so the zero is the tell. On a zero, the server is looked up a second way, by its provider's name, which the same reply reports correctly. That returns its real position and the stand-ins come from beside it. If the second lookup finds nothing, the untrustworthy position is dropped rather than used, and the run falls back to timing candidate cities and centring on the fastest - already what happens for a pin Ookla gives no position for at all. That case is logged and counted, because it changes which server the run ends up recording. A trustworthy answer costs nothing extra: the check reads a number already sitting on the reply, and no second lookup is made. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f78cd21 commit 3c8fd65

3 files changed

Lines changed: 479 additions & 2 deletions

File tree

internal/speedtest/ookla.go

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2121,11 +2121,12 @@ func (o *Ookla) RunReason(ctx context.Context, reason string) (Result, error) {
21212121
if id != "" && want > 1 {
21222122
// Resolve the pin first so the list can be centred on it. Passed into
21232123
// pickServers afterwards so this fetch is not repeated.
2124-
p, err := newOoklaClient(uc).FetchServerByIDContext(ctx, id)
2124+
p, err := fetchServerByID(ctx, uc, id)
21252125
if err != nil {
21262126
return Result{}, fmt.Errorf("fetch server %s: %w", id, err)
21272127
}
2128-
currentEndpoint(p) // a pinned server needs the same rewrite as a listed one
2128+
currentEndpoint(p) // a pinned server needs the same rewrite as a listed one
2129+
o.recentrePin(ctx, p) // the by-ID position can be OURS, not the server's
21292130
pinned = p
21302131
}
21312132
if lat, lon, label, ok := listCentre(id, want, pinned, searchedCity); ok {
@@ -2405,6 +2406,111 @@ func serverCoord(s *ookla.Server) (lat, lon float64, ok bool) {
24052406
return la, lo, true
24062407
}
24072408

2409+
// fetchServersByKeyword fetches the catalogue rows Ookla's search matches. A
2410+
// package var for the same reason fetchOriginServers is one: it is the only
2411+
// seam through which a test can put a catalogue in front of the recovery
2412+
// without a network.
2413+
var fetchServersByKeyword = func(ctx context.Context, keyword string) (ookla.Servers, error) {
2414+
return newOoklaClient(keywordConfig(keyword)).FetchServerListContext(ctx)
2415+
}
2416+
2417+
// keywordConfig carries the search keyword to the catalogue fetch. Split out so
2418+
// a test can hold the search string to the byte without a network: the seam
2419+
// above is stubbed in every test, so nothing else exercises this line.
2420+
func keywordConfig(keyword string) *ookla.UserConfig {
2421+
return &ookla.UserConfig{UserAgent: ookla.DefaultUserAgent, Keyword: keyword}
2422+
}
2423+
2424+
// pinCoordBudget bounds the one recovery fetch below, which runs inside the run
2425+
// deadline and before anything is measured. Measured round trips: 0.4s for a
2426+
// small ISP's catalogue, ~4.5s for the largest (the library echoes every row it
2427+
// returns, and Rogers returns 90).
2428+
const pinCoordBudget = 15 * time.Second
2429+
2430+
// recentrePin replaces a by-ID coordinate we cannot trust with the catalogue's
2431+
// own, and clears it when there is none to be had.
2432+
//
2433+
// api/ios-config.php splices the CALLER'S own ISP server into its reply wearing
2434+
// the CALLER'S coordinates, so a pin that IS our ISP's server comes back
2435+
// positioned where we are - measured, Montreal server 1993 reported Toronto.
2436+
// listCentre would then draw best-of's companions from our city, where they
2437+
// out-score the pin on ping-discounted throughput and get recorded in its
2438+
// place. The same reply's identity fields (ID, sponsor, name) stay trustworthy.
2439+
//
2440+
// The tell costs no request: the library derives Distance from that reply's OWN
2441+
// client coordinate, so the spliced row reads exactly 0 while a server 5 km
2442+
// away reads 5. A reply carrying no client element reads 0 too, and so does a
2443+
// server genuinely at our address - which is why 0 means "this coordinate is
2444+
// not trustworthy", never "this coordinate is a lie". Both benign cases cost
2445+
// one extra fetch and land on the same coordinate anyway.
2446+
//
2447+
// A cleared coordinate is the pin-with-no-coordinate case listCentre and
2448+
// shouldRaceCities already handle: the searched city, else a raced city.
2449+
func (o *Ookla) recentrePin(ctx context.Context, p *ookla.Server) {
2450+
if p.Distance != 0 {
2451+
return
2452+
}
2453+
lat, lon, err := sponsorCoord(ctx, p.Sponsor, p.ID)
2454+
if err != nil {
2455+
// Warn with a counter: the companions now come from a raced city rather
2456+
// than from beside the pin, and one of them can be recorded in its place.
2457+
stats.Inc("speed.pin_coord_unrecovered")
2458+
o.warnf("could not place the pinned speedtest server; its companions will be drawn "+
2459+
"from elsewhere and one of them may be recorded instead",
2460+
"server", serverLabel(p), "server_id", p.ID, "err", err)
2461+
}
2462+
// Cleared on failure as well as set on success. A zero distance means this
2463+
// coordinate is the caller's own, so keeping it is keeping the fault; an
2464+
// empty one makes listCentre decline and the run races cities, which at
2465+
// least measures where it centres.
2466+
p.Lat, p.Lon = lat, lon
2467+
}
2468+
2469+
// sponsorCoord looks one server's registered coordinate up in the ordinary
2470+
// catalogue, searched by its sponsor and matched by ID.
2471+
//
2472+
// The keyword is the sponsor VERBATIM. Ookla's search is a literal substring
2473+
// match, so trimming, case-folding or stripping accents turns a hit into
2474+
// "no server available or found" (measured: "Telefonica" and "China Telecom"
2475+
// both find nothing, where the registered strings do).
2476+
func sponsorCoord(ctx context.Context, sponsor, id string) (lat, lon string, err error) {
2477+
if sponsor == "" {
2478+
return "", "", errors.New("no sponsor to search the catalogue by")
2479+
}
2480+
ctx, cancel := context.WithTimeout(ctx, pinCoordBudget)
2481+
defer cancel()
2482+
// Abandoned, not awaited, when the context dies: the library one-shot-pings
2483+
// every row it fetched on a context of its own (speedtest-go server.go:305),
2484+
// so waiting for the call to return holds the run - and the scheduler's
2485+
// single-flight flag - open past an abort. raceOrigins abandons its fetches
2486+
// for the same reason.
2487+
type res struct {
2488+
rows ookla.Servers
2489+
err error
2490+
}
2491+
done := make(chan res, 1)
2492+
go func() {
2493+
r, e := fetchServersByKeyword(ctx, sponsor)
2494+
done <- res{r, e}
2495+
}()
2496+
var rows ookla.Servers
2497+
select {
2498+
case r := <-done:
2499+
rows, err = r.rows, r.err
2500+
case <-ctx.Done():
2501+
return "", "", ctx.Err()
2502+
}
2503+
if err != nil {
2504+
return "", "", err
2505+
}
2506+
for _, s := range rows {
2507+
if s.ID == id {
2508+
return s.Lat, s.Lon, nil
2509+
}
2510+
}
2511+
return "", "", fmt.Errorf("server %s absent from sponsor %q's catalogue", id, sponsor)
2512+
}
2513+
24082514
// listCentre decides what the fetched server list is centred on, which is the
24092515
// same thing as deciding where best-of-N draws its companion servers from. Kept
24102516
// as a plain function so the rule itself is tested, not a copy of it.
@@ -2957,6 +3063,13 @@ var fetchServerList = func(ctx context.Context, client *ookla.Speedtest) (ookla.
29573063
return guardedServers(ctx, currentEndpoints(servers)), err
29583064
}
29593065

3066+
// fetchServerByID indirects the pin's early resolve for the same reason, and it
3067+
// carries more than identity: what this returns decides where a pinned best-of
3068+
// run draws its companions from (see recentrePin).
3069+
var fetchServerByID = func(ctx context.Context, uc *ookla.UserConfig, id string) (*ookla.Server, error) {
3070+
return newOoklaClient(uc).FetchServerByIDContext(ctx, id)
3071+
}
3072+
29603073
// connFamilies records which IP families one measurement's transfer
29613074
// connections ACTUALLY used, so Result.IPFamily can be derived from a real
29623075
// recorded connection rather than guessed from configuration. Fed by the

0 commit comments

Comments
 (0)