Source File
remotemap.go
Belonging Package
github.com/tmc/go-iroh/internal/socket
package socketimport ()// RemoteMap is the registry of per-remote [RemoteStateActor]s, keyed by// [key.EndpointID]. It spawns an actor on first reference to a remote and// removes it when the actor idles out. It is the Go analog of the Rust RemoteMap// (iroh/src/socket/remote_map.rs).//// Actor insertion and idle-teardown deregistration are serialized by a single// mutex, and an actor only removes itself if it is still the actor registered// under its id. So an AddConnection arriving exactly as the 60s idle timeout// fires yields exactly one actor: the teardown either runs first (the next// reference spawns a fresh actor) or the AddConnection runs first (which resets// the actor's idle timer, so it does not tear down). See [RemoteMap.ResolveRemote]// / [RemoteMap.AddConnection] and the onExit closure in [RemoteMap.actor].//// RemoteMap is safe for concurrent use. Create one with [NewRemoteMap].type RemoteMap struct {ctx context.Contextselector PathSelectorresolve ResolveFuncidle time.Duration // actor idle timeout; ActorMaxIdleTimeout unless overridden for testsmetrics *Metricsmu sync.Mutexactors map[key.EndpointID]*RemoteStateActoronEvict func(id key.EndpointID, addrs []Addr)noHolepunch bool}// NewRemoteMap returns a RemoteMap whose actors live until ctx is cancelled or// they idle out. selector is the path selector shared by all actors (nil uses// [BiasedRttPathSelector]); resolve is the address-lookup hook (nil disables// lookup-driven resolution).func ( context.Context, PathSelector, ResolveFunc) *RemoteMap {return newRemoteMap(, , , ActorMaxIdleTimeout, nil)}// NewRemoteMapWithMetrics is like [NewRemoteMap], but records actor path// lifecycle counters in metrics.func ( context.Context, PathSelector, ResolveFunc, *Metrics) *RemoteMap {return newRemoteMap(, , , ActorMaxIdleTimeout, )}// newRemoteMap is the constructor with a configurable actor idle timeout, so// tests can drive the idle-teardown race without waiting the full minute.func newRemoteMap( context.Context, PathSelector, ResolveFunc, time.Duration, *Metrics) *RemoteMap {if == nil {= BiasedRttPathSelector{}}return &RemoteMap{ctx: ,selector: ,resolve: ,idle: ,metrics: ,actors: make(map[key.EndpointID]*RemoteStateActor),}}// actor returns the running actor for id, spawning one if none is registered.// The caller must hold m.mu. The spawned actor's onExit removes it from the map// under m.mu, but only if it is still the actor registered under id, so a// concurrently-spawned successor is never reaped.func ( *RemoteMap) ( key.EndpointID) *RemoteStateActor {if , := .actors[]; {return}var *RemoteStateActor:= func() {.mu.Lock()defer .mu.Unlock()// The captured a is assigned by the spawner under m.mu, and onExit// runs deferred in the actor goroutine, so under m.mu both the// variable and the actor-owned path state are race-free to read.:= .paths.Addrs():= .actors[]if == {delete(.actors, )= nil}// Evict the remote's mapped addresses unless a successor actor has// already been spawned for the same id (it shares them). Calling the// hook under m.mu keeps eviction ordered before any later spawn, so a// fresh actor always regenerates fresh mappings.if == nil && .onEvict != nil {.onEvict(, )}}= newRemoteStateActor(.ctx, , .selector, .resolve, .idle, .metrics, ).noHolepunch.Store(.noHolepunch).actors[] =return}// DisableHolepunch stops actors from initiating NAT traversal or direct-path// validation on their upgrade tick. Endpoints without IP transports set it:// there is no direct path to punch toward, and a traversal round initiated on// a relay-only connection stalls its in-flight relay streams. Set it before// the first remote is referenced.func ( *RemoteMap) () {.mu.Lock()defer .mu.Unlock().noHolepunch = true}// SetOnEvict sets f to be called when a remote's actor is reaped with no// successor, passing the remote's known path addresses. The endpoint uses it to// release the remote's mapped addresses (see [Socket.EvictRemote]), so the// mapped-address tables do not grow without bound under peer churn. f is called// with the map's internal mutex held and must not call back into the RemoteMap.// Set it before the first remote is referenced.func ( *RemoteMap) ( func( key.EndpointID, []Addr)) {.mu.Lock()defer .mu.Unlock().onEvict =}// Actor returns the running actor for id, spawning one if none exists.func ( *RemoteMap) ( key.EndpointID) *RemoteStateActor {.mu.Lock()defer .mu.Unlock()return .actor()}// AddNATTraversalAddresses reconciles local QNT candidates on currently-active// remote actors. It does not spawn actors and ignores per-actor errors, because// candidate updates must not make an established endpoint fail.func ( *RemoteMap) ( []netip.AddrPort) {.mu.Lock():= make([]*RemoteStateActor, 0, len(.actors))for , := range .actors {= append(, )}.mu.Unlock()for , := range {_ = .AddNATTraversalAddresses()}}// Len returns the number of registered actors. Intended for tests and metrics.func ( *RemoteMap) () int {.mu.Lock()defer .mu.Unlock()return len(.actors)}// RemoteInfo returns a snapshot for id if a running actor exists. It does not// spawn a new actor.func ( *RemoteMap) ( key.EndpointID) (RemoteInfo, bool) {.mu.Lock(), := .actors[].mu.Unlock()if ! {return RemoteInfo{}, false}select {case <-.donec():.dropIfStopped(, )return RemoteInfo{}, falsedefault:}return .RemoteInfo(), true}// AddConnection registers conn with the actor for remote, spawning the actor if// needed, and returns the connection's path-event channel. Registering a// connection resets the actor's idle timer, so this can race the idle teardown// safely (O12): if the actor is mid-teardown the send observes its done channel// and a fresh actor is spawned on the retry.func ( *RemoteMap) ( key.EndpointID, Connection) <-chan PathEvent {, := .AddConnectionActor(, )return}// AddConnectionActor is like [RemoteMap.AddConnection], but also returns the// actor that accepted conn.func ( *RemoteMap) ( key.EndpointID, Connection) (<-chan PathEvent, *RemoteStateActor) {for {.mu.Lock():= .actor().mu.Unlock()// If the actor stopped between selection and use, spawn a fresh one.select {case <-.donec():.dropIfStopped(, )continuedefault:}, := .AddConnection()// If the actor stopped before it could register the connection, drop it// and retry with a fresh actor.if ! {.dropIfStopped(, )continue}return ,}}// ResolveRemote asks the actor for addr.ID to resolve and register more// candidate paths, spawning the actor if needed. It returns the lookup error if// any. It races idle teardown the same way as [RemoteMap.AddConnection].func ( *RemoteMap) ( netaddr.EndpointAddr) error {for {.mu.Lock():= .actor(.ID).mu.Unlock()select {case <-.donec():.dropIfStopped(.ID, )continuedefault:}:= .ResolveRemote()// A canceled result with a stopped actor means we raced teardown; retry.if == context.Canceled {select {case <-.donec():.dropIfStopped(.ID, )continuedefault:}}return}}// dropIfStopped removes a from the map if it is the stopped actor still// registered under id, so the next reference spawns a fresh actor. The actor's// own onExit already does this; dropIfStopped covers the window before onExit// has run.func ( *RemoteMap) ( key.EndpointID, *RemoteStateActor) {.mu.Lock()defer .mu.Unlock()if .actors[] == {delete(.actors, )}}
![]() |
The pages are generated with Golds v0.8.4. (GOOS=linux GOARCH=amd64) Golds is a Go 101 project developed by Tapir Liu. PR and bug reports are welcome and can be submitted to the issue list. Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds. |