Files
meshcore-analyzer/cmd/server/node_reach_concurrency_test.go
T
nullrouten0andClaude Mythos 5.1 caa66356bd fix(api): cap concurrent cold reach scans at 2, answer 429 beyond that (#2127)
`GET /api/nodes/{pubkey}/reach` is unauthenticated and a cold-cache
request runs a full scan. `singleflight` only collapses identical keys,
so distinct `(pubkey, days)` requests each start a scan, and they share
the 4-connection SQLite pool with every other handler. A handful of
requests for different nodes can hold every reader and stall the whole
API.

**Fix:** a small semaphore allows two cold scans at once. A cold request
that finds both slots busy gets `429` with `Retry-After: 5` instead of
queueing. Cached answers are unaffected, and the two in-flight scans
still complete normally.

**Tests:** `node_reach_concurrency_test.go` — with both slots held a
cold request returns 429 with `Retry-After`; after release the same
request runs normally. Full server suite passes.

**Note:** our instance runs a fork that already bounds reach work
differently (an async job queue), so this exact change is not what we
run in production. The test suite is the verification here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
2026-10-08 16:26:15 +02:00

49 lines
1.5 KiB
Go

package main
import (
"net/http"
"net/http/httptest"
"testing"
)
// GET /api/nodes/{pubkey}/reach is unauthenticated and a cold-cache request
// starts a scan. Only reachMaxConcurrentBuilds scans may run at once; the
// next cold request gets 429 with Retry-After instead of queueing on the
// shared SQLite pool.
func TestNodeReachConcurrentBuildCap(t *testing.T) {
srv, router := setupTestServerWithAPIKey(t, "")
const key = "ab00000000000000000000000000000000000000000000000000000000000001"
get := func() *httptest.ResponseRecorder {
req := httptest.NewRequest("GET", "/api/nodes/"+key+"/reach?days=7", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
// Fill every build slot as if scans were in flight.
var releases []func()
for i := 0; i < reachMaxConcurrentBuilds; i++ {
rel, ok := srv.reachAcquireBuildSlot()
if !ok {
t.Fatalf("slot %d should be free", i)
}
releases = append(releases, rel)
}
if w := get(); w.Code != http.StatusTooManyRequests {
t.Fatalf("expected 429 while all build slots are busy, got %d (body: %s)", w.Code, w.Body.String())
} else if w.Header().Get("Retry-After") == "" {
t.Fatalf("429 must carry Retry-After")
}
// Free the slots: the same request now runs the scan. The key is not a
// known node, so the normal answer is 404 — the point is that it is no
// longer 429.
for _, rel := range releases {
rel()
}
if w := get(); w.Code == http.StatusTooManyRequests {
t.Fatalf("still 429 after slots were released (body: %s)", w.Body.String())
}
}