Source File
path_watcher.go
Belonging Package
github.com/tmc/go-iroh/internal/socket
package socketimport ()// PathBroadcastCapacity is the per-subscriber buffer capacity for path events. A// subscriber that falls more than this many events behind is told how many it// missed via a [PathEventLagged] event rather than silently dropping. It matches// the Rust BROADCAST_CAPACITY// (iroh/src/socket/remote_map/remote_state/path_watcher.rs:50).const PathBroadcastCapacity = 8// PathEventKind tags the variant of a [PathEvent].type PathEventKind intconst (// PathEventOpened reports a newly-opened network path.PathEventOpened PathEventKind = iota// PathEventClosed reports a closed network path.PathEventClosed// PathEventSelected reports that a path was selected for transmission.PathEventSelected// PathEventLagged reports that events were dropped before a subscriber read// them; Missed carries the count.PathEventLagged)func ( PathEventKind) () string {switch {case PathEventOpened:return "opened"case PathEventClosed:return "closed"case PathEventSelected:return "selected"case PathEventLagged:return "lagged"default:return "invalid"}}// PathEvent is a lifecycle notification for a network path of a connection. It// is the Go analog of the Rust PathEvent enum (path_watcher.rs:55).//// For Opened, Closed, and Selected, Addr identifies the path. For Lagged, Missed// is the number of events the subscriber missed and Addr is the zero value.type PathEvent struct {// Kind is which kind of event this is.Kind PathEventKind// Addr is the path's transport address (zero for Lagged).Addr Addr// Missed is the number of dropped events (only for Lagged).Missed uint64}// PathWatcher is a drop-oldest broadcast of [PathEvent]s to any number of// subscribers. Each subscriber has its own ring buffer of [PathBroadcastCapacity]// events; when a subscriber falls behind, the oldest buffered event is dropped// and the next event the subscriber receives is a [PathEventLagged] with the// running missed count, mirroring tokio::broadcast's lagged-receiver behavior// (path_watcher.rs).//// PathWatcher is safe for concurrent use. The writer calls [PathWatcher.Send];// readers call [PathWatcher.Subscribe] and consume the returned channel. Each// subscriber is served by a dedicated delivery goroutine that stops when the// subscriber is cancelled or the watcher is closed.type PathWatcher struct {mu sync.Mutexsubs map[*pathSub]struct{}closed bool}// pathSub is a single subscriber. The ring buffer and lag bookkeeping are// guarded by mu; cond wakes the delivery goroutine when an event is enqueued or// the subscriber is closed.type pathSub struct {mu sync.Mutexcond *sync.Condring []PathEvent // pending events, oldest first; len <= PathBroadcastCapacitymissed uint64 // events dropped since the last delivered Laggedlagged bool // a Lagged event is pending deliveryclosed boolch chan PathEventdone chan struct{}once sync.Once}// NewPathWatcher returns an empty broadcast with no subscribers.func () *PathWatcher {return &PathWatcher{subs: make(map[*pathSub]struct{})}}// Subscribe registers a new subscriber and returns the channel its events are// delivered on plus a function to unsubscribe and stop delivery. Events sent// before Subscribe are not replayed. The channel is closed when the subscriber// is cancelled or the watcher is closed.//// The cancel function should be called when the subscriber is done, like// [time.Ticker.Stop]: each subscription runs a delivery goroutine, which// cancel stops without waiting for the subscriber to drain. [PathWatcher.Close]// stops the goroutine of every remaining subscriber, so an abandoned// subscription outlives at most the watcher itself. Events already buffered on// the channel stay readable after it is closed; events still pending delivery// when a subscriber stops reading are dropped.func ( *PathWatcher) () (<-chan PathEvent, func()) {.mu.Lock()defer .mu.Unlock():= &pathSub{ch: make(chan PathEvent, PathBroadcastCapacity+1),done: make(chan struct{}),}.cond = sync.NewCond(&.mu)if .closed {close(.ch).closed = truereturn .ch, func() {}}.subs[] = struct{}{}go .deliver()return .ch, func() { .unsubscribe() }}func ( *PathWatcher) ( *pathSub) {.mu.Lock()if , := .subs[]; ! {.mu.Unlock()return}delete(.subs, ).mu.Unlock().close()}// Send broadcasts ev to every subscriber. A subscriber whose ring buffer is full// has its oldest pending event dropped and its missed counter incremented; the// next event that subscriber receives is a [PathEventLagged] carrying the missed// count. Send never blocks.func ( *PathWatcher) ( PathEvent) {.mu.Lock()if .closed {.mu.Unlock()return}:= make([]*pathSub, 0, len(.subs))for := range .subs {= append(, )}.mu.Unlock()for , := range {.enqueue()}}// Close stops every subscriber's delivery goroutine, closing its channel, and// rejects future sends and subscriptions. It is idempotent. It is the analog of// dropping the Rust broadcast sender, which ends every outstanding receiver.func ( *PathWatcher) () {.mu.Lock()if .closed {.mu.Unlock()return}.closed = true:= make([]*pathSub, 0, len(.subs))for := range .subs {= append(, )}.subs = make(map[*pathSub]struct{}).mu.Unlock()for , := range {.close()}}// enqueue appends ev to the subscriber's ring buffer, dropping the oldest// pending event and accruing a missed count if the buffer is full, then wakes// the delivery goroutine. Never blocks.func ( *pathSub) ( PathEvent) {.mu.Lock()if .closed {.mu.Unlock()return}if len(.ring) >= PathBroadcastCapacity {.ring = .ring[1:] // drop oldest.missed++.lagged = true}.ring = append(.ring, ).cond.Signal().mu.Unlock()}// close marks the subscriber closed, wakes its delivery goroutine, and// interrupts an in-flight send so the goroutine cannot outlive the// subscription. Idempotent.func ( *pathSub) () {.mu.Lock()if .closed {.mu.Unlock()return}.closed = true.cond.Signal().mu.Unlock().once.Do(func() { close(.done) })}// deliver is the per-subscriber delivery goroutine. It pops events from the ring// buffer and sends them on the channel, prepending a single Lagged event// whenever one is pending. It exits and closes the channel when the subscriber// is closed and its ring is drained. If the subscriber stops reading, close// interrupts an in-flight send instead of waiting forever for the drain.func ( *pathSub) () {for {.mu.Lock()for !.closed && !.lagged && len(.ring) == 0 {.cond.Wait()}if .closed && !.lagged && len(.ring) == 0 {.mu.Unlock()close(.ch)return}var PathEventif .lagged {= PathEvent{Kind: PathEventLagged, Missed: .missed}.lagged = false.missed = 0} else {= .ring[0].ring = .ring[1:]}.mu.Unlock()select {case .ch <- :continuedefault:}select {case .ch <- :case <-.done:close(.ch)return}}}
![]() |
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. |