Files
shater/protocol/group/reachability_lx.go
T
Leadaxe ace08f7aca feat(spec-020): event-driven reachability cache (replace per-tick walk)
Reachability is now recomputed ONLY when the active routing tree changes, not
every idle tick. Per user direction: events decide WHO is reachable; the timer
only checks WHEN (last-activity comparison).

- adapter.ReachabilityInvalidator: narrow interface (not folded into the large
  adapter.Router), registered into ctx in box.go, pulled by groups via
  service.FromContext — no route<-group import.
- Router: reachMu/reachCache/reachDirty. InvalidateReachability() is a lock-free
  atomic store (safe under any group lock — no lock-order cycle). reachableOutbounds()
  recomputes the walk OUTSIDE the cache lock (the walk calls into groups that hold
  their own locks), clears dirty BEFORE the walk so a concurrent event re-dirties
  for next tick rather than being lost, publishes under RWMutex. Starts dirty so
  the first tick (and every reload = fresh Router) computes.
- 4 invalidation sources: selector switch (selector.go after selected.Store),
  legacy urltest auto-switch (urltest.go performUpdateCheck), and a balancer
  onChange hook fired from setSlots — one hook covers all pool-rebuild call sites.
- idle tick: now one cached-map lookup + atomic idle compare per endpoint, no walk.

Design independently verified against source (no data race, no import cycle, no
deadlock — walk runs outside the lock). Builds + go vet + race-build clean.
2026-06-30 22:16:49 +03:00

24 lines
784 B
Go

// lx:begin idle-suspend
package group
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/service"
)
// invalidateReachability tells the router (SPEC 020) that the active routing tree
// changed, so its cached reachable set must be recomputed. Called from the group
// event points — a selector switch, a urltest auto-switch, a pool rebuild. Pulls
// the invalidator out of the context the same way groups pull other services, so
// protocol/group never imports route. A nil invalidator (feature/route absent) is
// a no-op.
func invalidateReachability(ctx context.Context) {
if invalidator := service.FromContext[adapter.ReachabilityInvalidator](ctx); invalidator != nil {
invalidator.InvalidateReachability()
}
}
// lx:end idle-suspend