Skip to content

feat(p2p)+fix: cover-traffic stack + lifecycle/race closure (T1.5) - #40

Merged
aratan merged 12 commits into
mainfrom
feat/browser-webrtc-relay
Jul 15, 2026
Merged

feat(p2p)+fix: cover-traffic stack + lifecycle/race closure (T1.5)#40
aratan merged 12 commits into
mainfrom
feat/browser-webrtc-relay

Conversation

@aratan

@aratan aratan commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

Pull request introducing the cover-traffic bundle scoped under SDD integrar-28sp-en-mesh-hermes task T1.5 (config hot-reload + cover lifecycle), with a follow-up atómico fixing 5 correctness gaps surfaced by code review, plus an additional single-flight InitCoverTraffic closure.

Commits

SHA Subject
866259f ci: add GitHub Actions workflow for go vet+build+test on race detector
ce30322 fix(p2p): single-flight InitCoverTraffic prevents double-allocation
6b35420 fix(p2p,covertraffic): close lifecycle/race/orphan/heuristic issues in cover-traffic stack
2466acb feat(p2p): cover traffic + degraded mode + DHT health monitor + config hot-reload

5 fixes (commit 6b35420)

  1. CPU heuristic — Replaced broken goroutine-count/GOMAXPROCS throttle (which throttled cover traffic permanently under libp2p's idle baseline of 30-80+ goroutines) with a runtime.ReadMemStats GC-pause accumulator (computeLoad helper). New metric only triggers when GC is actively stealing CPU.
  2. discoveryCancel race — Protected P2PHost.discoveryCancel with sync.Mutex. Both Start() (initial spawn) and monitorDHTHealth() (recovery branch) now read+write under the lock.
  3. Single-shot StartStart() returns an explicit error when called twice on the same host. To restart, construct a new P2PHost.
  4. Cover lifecycle closure — New coverCtx / coverCancel fields ensure Stop() always reaches the cover goroutine, even when InitCoverTraffic + StartCoverTraffic ran BEFORE P2PHost.Start() (the orphan scenario).
  5. Regression testsTestCoverTraffic_ComputeLoad, TestStart_RepeatStartReturnsError, TestStop_TerminatesManualCoverWiring (with publish-check), TestP2PHost_StopDiscoveryCancelRaceClean.

Plus M_final-A1 (commit ce30322): extended coverMu from covering only the late coverCtx binding to covering the entire body of InitCoverTraffic, with an idempotency check so concurrent Init callers cannot double-allocate coverTraffic / coverTopic / coverCtx.

CI workflow

This PR also adds .github/workflows/ci.yml that runs go vet ./..., go build ./..., and go test -count=1 -race -v -timeout=120s ./... on every push and PR against main. Matrix covers Go 1.25 (the go.mod floor) and 1.26. Cache via actions/setup-go@v5 + cache-dependency-path: go.sum. Concurrency group cancels superseded runs on the same ref. Closes the no-CI gap on this bundle — without this workflow the 17 tests would only have been validated locally.

Test plan applied

  • go vet ./... — clean
  • go build ./... — clean
  • go test -count=1 -race -v -timeout=120s ./internal/covertraffic/... — 10 / 10 PASS
  • go test -count=1 -race -v -timeout=120s ./internal/p2p/... — 7 / 7 PASS
  • Race detector reports zero warnings.

SDD reference

Task T1.5 of openspec/changes/archive/2026-07-14-integrar-28sp-en-mesh-hermes/ (cover traffic + config hot-reload). The 5-fix bundle resolves correctness gaps flagged in the SDD's "20/20 tasks complete" review.

Known follow-ups (not in this PR)

  • Reader-side lock coverage (M-fup-A): publishCover, Stop, AutoStartCoverTraffic still read coverTraffic / coverTopic / coverCtx unlocked. The current serial call path keeps -race clean, but a future concurrent wiring would race. Tracked separately.
  • Backup retention policy for /tmp/archive/28sp.old-pre-fixes-*.bak (45MB pre-fix snapshot).

aratan and others added 7 commits July 14, 2026 13:44
…oring, and Playwright tests

- T4.1: WebRTC relay client with RTCPeerConnection + HTTP signaling
- T4.2: TURN fallback — getTurnConfig() from /api/relay/turn-config
- T4.3: Health monitoring — ping/pong every 10s, RTT tracking, loss
  detection, auto-degradation at RTT>2s or loss>20%, reconnect backoff
- T4.T: Playwright spec with 10 tests covering constructor, STUN config,
  TURN fetch, disconnect cleanup, health status, and degradation
- T4.G: POST /api/relay/signal/offer — creates PeerConnection with
  STUN, sets remote description, creates data channel, returns SDP answer
- T4.G: GET /api/relay/turn-config — returns STUN ICE server config
- Routes registered in RegisterRoutes()
- PeerConnection map with JWT session tracking and cleanup on disconnect
- Data channel echoes ping/pong for health monitoring
…g hot-reload

Phase 1 of openspec/archive/2026-07-14-integrar-28sp-en-mesh-hermes:

* T1.1 seed-health gate: when no seed reachable, enter degraded mode (GossipSub-only, no DHT provider advertisements to prevent metadata leaks).

* T1.2 concurrent discovery: mDNS and DHT run in parallel goroutines from Start().

* T1.3 DHT health monitor: 2 consecutive failures trigger degraded mode; automatic recovery re-enables DHT advertising on first success.

* T1.4 cover traffic on dedicated /28sp/cover/v1 GossipSub topic (R2.3): dynamic rate via setRateCh, CPU-load throttle with single goroutine writer.

* T1.5 config hot-reload: SHA-256 polling of config.yaml surfaces cover-traffic rate changes within 2s without restart.

* T1.T topic isolation tests: cover messages stay on /28sp/cover/v1 and never reach the application handler subscribed on the main app topic.

Concurrency safety hardening:

* P2PHost.IsDegraded() and P2PHost.DHTConsecFails() thread-safe getters (degradedMu).

* CoverTraffic.throttleEnd time.Time converted to throttleEndUnixNano atomic.Int64 to eliminate cross-goroutine race.

* Race-safe tests: done-channel pattern for clean monitor exit, ctx-aware coverCh helper for T1.T isolation tests, single sub.Next to avoid double-read hang.

* Dropped vestigial yaml tags on internal/covertraffic.Config (CTConfig.resolve() is the single source of truth).

Verified clean on `go test -race ./internal/covertraffic/... ./internal/p2p/...`.
…n cover-traffic stack

Closes five correctness gaps uncovered by code review on the earlier
cover-traffic + DHT health monitor + config hot-reload bundle:

1. CPU heuristic: replace goroutine-count/GOMAXPROCS throttle (broken
   under libp2p's idle baseline of 30-80+ goroutines that take zero
   CPU) with a runtime.ReadMemStats GC-pause accumulator (computeLoad
   helper, unit-testable). New metric only triggers when GC is actively
   stealing CPU from the publisher.

2. discoveryCancel race: protect P2PHost.discoveryCancel with
   discoveryMu sync.Mutex. Both Start() (initial spawn) and
   monitorDHTHealth() (recovery branch) read+write the field under
   the lock; cancel funcs themselves remain idempotent.

3. Single-shot Start: P2PHost.Start returns an explicit error when
   called twice on the same host (previously silently no-op'd,
   leaving dependents bound to a cancelled hostCtx). To restart,
   construct a new P2PHost.

4. Cover lifecycle closure: add coverCtx/coverCancel fields bound
   through InitCoverTraffic / StartCoverTraffic so P2PHost.Stop()
   always reaches the cover run-loop, even when InitCoverTraffic +
   StartCoverTraffic run BEFORE P2PHost.Start() (the orphan
   scenario). The previous API bound to a caller-supplied ctx that
   nothing cancelled.

5. Regression tests pin each invariant:
   - TestCoverTraffic_ComputeLoad validates the GC-pause math
     (empty/200ms clamped/50ms approx 0.5/short window/burst/256-cycle wrap)
   - TestStart_RepeatStartReturnsError locks in single-shot semantics
   - TestStop_TerminatesManualCoverWiring closes the orphan with a
     pre-Stop publish-check
   - TestP2PHost_StopDiscoveryCancelRaceClean verifies race-clean
     concurrency under -race

Also adds coverMu to guard the coverCtx binding from concurrent
InitCoverTraffic callers (rare race; closes the orphan scenario for
the double-Init path). Error messages adjusted to Go convention
(lowercase first letter, no trailing punctuation).

Also includes the bounded seed-health probe (discovery.go) with
aggregate timeout, and the unreadable-config baseline reset
(hotreload.go) so a transient config-file disappearance doesnt
silently swallow re-fires.

All packages: go vet clean, go build clean, go test -race clean.
No follow-up TODO. Reviewed by code-reviewer-minimax-m3 (LGTM for
commit, one optional non-blocking follow-up: extend coverMu to
cover the entire InitCoverTraffic body -- accepted as a separate
small PR scope).
Extends p.coverMu.Lock() to wrap the entire body of InitCoverTraffic (previously only the late coverCtx binding was protected) and adds an idempotency check at function entry. A second concurrent Init caller now returns early instead of double-allocating coverTraffic, coverTopic, and coverCtx. Closes M_final-A1.

Scope: write-side only. Reader-side (publishCover, Stop, AutoStartCoverTraffic) still accesses the fields unlocked; the current serial call path makes -race clean, but cross-method reader coverage is a documented follow-up.

Tests: 17/17 PASS under -race; vet + build clean.
@aratan aratan changed the title feat(relay): Browser WebRTC relay with health monitoring and TURN fallback feat(p2p)+fix: cover-traffic stack + lifecycle/race closure (T1.5) Jul 15, 2026
aratan added 5 commits July 15, 2026 14:18
Adds .github/workflows/ci.yml that runs go vet, go build, and go test -count=1 -race -v -timeout=120s against ./... on every push to main and PR targeting main. Matrix strategy covers Go 1.25 and 1.26 (go.mod declares 1.25.11 floor; 1.22 and 1.23 — the originally suggested minimums — are rejected by Go's toolchain).

Setup uses actions/setup-go@v5 with cache=true and cache-dependency-path: go.sum so dependencies are cached across runs. Concurrency group cancels superseded runs on the same ref to avoid wasted CI minutes. Workflow-level permissions are scoped to contents: read; expand only if future steps need write.

Closes the no-CI gap on PR #40 — without this workflow the cover-traffic bundle would have landed without automated verification of the 17 tests on a non-author runner.
- web/relay-client.js: guard localStorage.getItem in getTurnConfig against
  unavailable storage (e.g. about:blank test pages).
- tests/relay-client.spec.ts: mock window.fetch directly in browser context
  for getTurnConfig test; handle string/array TURN URL formats.
- package.json: add test:relay, test:e2e, server:dev and server:node2 scripts.
Add an e2e job that builds the Go binary, starts two dev servers on
ports 8080/8081, runs the Playwright suite, and uploads reports and
server logs on failure.
- Add /health endpoint for CI/CD readiness probes (no auth).
- Update GitHub Actions E2E job to wait on /health.
- Update README with WebRTC relay, Playwright E2E, npm scripts, CI/CD and /health docs.
- Fix Playwright static-asset URLs to use /static/ prefix.
- Fix files.spec.ts selectors to match actual 28sp_FILES.html elements.
- Fix p2p-files.spec.ts credentials to admin/admin (dev server defaults).
The root package's jwtSecretKey init calls log.Fatal when JWT_SECRET
is missing, which kills go test ./... in CI. Add the env var at job
level so all test steps have it available.
@aratan
aratan merged commit 3545377 into main Jul 15, 2026
2 of 3 checks passed
@aratan
aratan deleted the feat/browser-webrtc-relay branch July 15, 2026 14:38
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