This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

Binary file not shown.
Binary file not shown.
+10 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
@@ -103,6 +103,15 @@ func (b *Bus) Debugger() *Debugger {
return &Debugger{b}
}
// ProbeLocks acquires and releases the bus's internal mutexes.
func (b *Bus) ProbeLocks() {
b.topicsMu.Lock()
b.topicsMu.Unlock()
b.clientsMu.Lock()
b.clientsMu.Unlock()
}
// Close closes the bus. It implicitly closes all clients, publishers and
// subscribers attached to the bus.
//
+16 -4
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
@@ -146,7 +146,10 @@ func Subscribe[T any](c *Client) *Subscriber[T] {
r := c.subscribeStateLocked()
s := newSubscriber[T](r, logfForCaller(c.logger()))
r.addSubscriber(s)
// Register the non-generic core with the bus rather than the typed facade,
// mirroring SubscribeFunc and Publish: this keeps the bus's outputs map
// and subscriber-interface itab out of per-T cost.
r.addSubscriber(s.core)
return s
}
@@ -169,7 +172,11 @@ func SubscribeFunc[T any](c *Client, f func(T)) *SubscriberFunc[T] {
r := c.subscribeStateLocked()
s := newSubscriberFunc[T](r, f, logfForCaller(c.logger()))
r.addSubscriber(s)
// Register the non-generic core, not the typed facade. Doing
// so means the bus's outputs map and the subscriber interface
// itab are not parameterized by T, eliminating per-T itab and
// dictionary cost.
r.addSubscriber(s.core)
return s
}
@@ -177,6 +184,11 @@ func SubscribeFunc[T any](c *Client, f func(T)) *SubscriberFunc[T] {
// It panics if c is closed.
func Publish[T any](c *Client) *Publisher[T] {
p := newPublisher[T](c)
c.addPublisher(p)
// Register the non-generic core with the client so the
// per-Client publisher set, the publisher interface itab, and
// the publisher equality function are not parameterized by T.
// This eliminates per-T itab/dictionary/eq cost for every new
// event type passed through Publish[T].
c.addPublisher(p.core)
return p
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !android && !ts_omit_debugeventbus
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios || android || ts_omit_debugeventbus
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package eventbus provides an in-process event bus.
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
+54 -15
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
@@ -7,8 +7,13 @@ import (
"reflect"
)
// publisher is a uniformly typed wrapper around Publisher[T], so that
// debugging facilities can look at active publishers.
// publisher is a uniformly typed wrapper around publisherCore so that
// debugging facilities can enumerate active publishers on a [Client]
// and report the types each one publishes. The interface is
// implemented by the non-generic *publisherCore (not by the typed
// user-facing *Publisher[T]); this keeps the bus's per-Client
// publisher set, and the publisher itab/dictionary, free of
// per-T duplication.
type publisher interface {
publishType() reflect.Type
Close()
@@ -16,12 +21,35 @@ type publisher interface {
// A Publisher publishes typed events on a bus.
type Publisher[T any] struct {
// Implementation note: Publisher[T] is a thin user-facing facade over a
// non-generic *publisherCore. Carrying T on the public type preserves the
// typed API of Publish(v T), but all of the actual state (the *Client
// back-pointer, the stop flag, and the cached reflect.Type used by
// diagnostic introspection) lives on the core and is not duplicated per T.
//
// The diagnostic surface that motivates the publisher interface
// (Debugger.PublishTypes) is served by *publisherCore directly, so adding
// new typed publishers does not pay an itab+dictionary cost just to satisfy
// diagnostic enumeration.
core *publisherCore
}
// publisherCore is the non-generic implementation of a Publisher.
// It implements the package-private publisher interface; the bus's
// outputs map and itab key on this single type, not on Publisher[T].
type publisherCore struct {
client *Client
stop stopFlag
typ reflect.Type // cached reflect.TypeFor[T]()
}
func newPublisher[T any](c *Client) *Publisher[T] {
return &Publisher[T]{client: c}
return &Publisher[T]{
core: &publisherCore{
client: c,
typ: reflect.TypeFor[T](),
},
}
}
// Close closes the publisher.
@@ -31,35 +59,46 @@ func newPublisher[T any](c *Client) *Publisher[T] {
// If the Bus or Client from which the Publisher was created is closed,
// the Publisher is implicitly closed and does not need to be closed
// separately.
func (p *Publisher[T]) Close() {
func (p *Publisher[T]) Close() { p.core.Close() }
// Close implements the publisher interface and the user-facing
// (*Publisher[T]).Close.
func (c *publisherCore) Close() {
// Just unblocks any active calls to Publish, no other
// synchronization needed.
p.stop.Stop()
p.client.deletePublisher(p)
c.stop.Stop()
c.client.deletePublisher(c)
}
func (p *Publisher[T]) publishType() reflect.Type {
return reflect.TypeFor[T]()
}
// publishType implements the publisher interface.
func (c *publisherCore) publishType() reflect.Type { return c.typ }
// Publish publishes event v on the bus.
func (p *Publisher[T]) Publish(v T) {
publish(p.core, v)
}
// publish is the non-generic body of Publisher[T].Publish. The only
// per-T work is the boxing of v into evt.Event (an `any` field) and
// the construction of the PublishedEvent struct itself; all of the
// channel/select dance is shared across every T.
func publish(c *publisherCore, v any) {
// Check for just a stopped publisher or bus before trying to
// write, so that once closed Publish consistently does nothing.
select {
case <-p.stop.Done():
case <-c.stop.Done():
return
default:
}
evt := PublishedEvent{
Event: v,
From: p.client,
From: c.client,
}
select {
case p.client.publish() <- evt:
case <-p.stop.Done():
case c.client.publish() <- evt:
case <-c.stop.Done():
}
}
@@ -70,5 +109,5 @@ func (p *Publisher[T]) Publish(v T) {
// nobody seems to care. Publishers must not assume that someone will
// definitely receive an event if ShouldPublish returns true.
func (p *Publisher[T]) ShouldPublish() bool {
return p.client.shouldPublish(reflect.TypeFor[T]())
return p.core.client.shouldPublish(p.core.typ)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
+217 -72
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
@@ -184,49 +184,62 @@ func (s *subscribeState) closed() <-chan struct{} {
// A Subscriber delivers one type of event from a [Client].
// Events are sent to the [Subscriber.Events] channel.
type Subscriber[T any] struct {
stop stopFlag
read chan T
unregister func()
logf logger.Logf
slow *time.Timer // used to detect slow subscriber service
// core holds the non-generic subscriber-interface implementation
// (Close, subscribeType, dispatch, slow timer, unregister) shared
// with [SubscriberFunc] via [subscriberCore]. The only per-T state
// owned by the facade itself is the typed delivery channel; the
// dispatch loop, unlike SubscriberFunc, must remain per-T — see
// [Subscriber.dispatchTyped].
core *subscriberCore
read chan T
}
func newSubscriber[T any](r *subscribeState, logf logger.Logf) *Subscriber[T] {
slow := time.NewTimer(0)
slow.Stop() // reset in dispatch
return &Subscriber[T]{
read: make(chan T),
unregister: func() { r.deleteSubscriber(reflect.TypeFor[T]()) },
logf: logf,
slow: slow,
core := newSubscriberCore(r, logf, reflect.TypeFor[T]())
s := &Subscriber[T]{
core: core,
read: make(chan T),
}
}
func newMonitor[T any](attach func(fn func(T)) (cancel func())) *Subscriber[T] {
ret := &Subscriber[T]{
read: make(chan T, 100), // arbitrary, large
// Subscriber[T] keeps a per-T dispatch loop; see [Subscriber.dispatchTyped]
// for why we don't share the non-generic dispatchFunc that SubscriberFunc
// uses.
core.dispatchFn = func(
ctx context.Context,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
) bool {
return s.dispatchTyped(ctx, vals, acceptCh, snapshot)
}
ret.unregister = attach(ret.monitor)
return ret
return s
}
func (s *Subscriber[T]) subscribeType() reflect.Type {
return reflect.TypeFor[T]()
}
func (s *Subscriber[T]) monitor(debugEvent T) {
select {
case s.read <- debugEvent:
case <-s.stop.Done():
}
}
func (s *Subscriber[T]) dispatch(ctx context.Context, vals *queue[DeliveredEvent], acceptCh func() chan DeliveredEvent, snapshot chan chan []DeliveredEvent) bool {
// dispatchTyped is the per-T dispatch loop for Subscriber[T]. It has to remain
// generic because the typed channel send `case s.read <- t:` must appear
// lexically inside the select. The rest of the cases match the non-generic
// dispatchFunc body to keep behavior aligned between Subscriber and
// SubscriberFunc.
//
// We don't share dispatchFunc (the way SubscriberFunc does) because bridging
// the typed channel send and the non-generic select would require running the
// send on its own goroutine on every event delivery. That bridge was measured
// at ~2.7x throughput regression on BenchmarkBasicThroughput, so we keep
// dispatchTyped generic and pay the per-shape stencil cost instead (measured
// at ~1,600 B body + ~1,100 B pclntab per shape on linux/amd64 tailscaled).
// Only the typed select lives in the per-shape stencil; the surrounding state
// (slow timer, log function, type name) is reached through the non-generic
// core.
func (s *Subscriber[T]) dispatchTyped(
ctx context.Context,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
) bool {
t := vals.Peek().Event.(T)
start := time.Now()
s.slow.Reset(slowSubscriberTimeout)
defer s.slow.Stop()
s.core.slow.Reset(slowSubscriberTimeout)
defer s.core.slow.Stop()
for {
// Keep the cases in this select in sync with subscribeState.pump
@@ -242,13 +255,35 @@ func (s *Subscriber[T]) dispatch(ctx context.Context, vals *queue[DeliveredEvent
return false
case ch := <-snapshot:
ch <- vals.Snapshot()
case <-s.slow.C:
s.logf("subscriber for %T is slow (%v elapsed)", t, time.Since(start))
s.slow.Reset(slowSubscriberTimeout)
case <-s.core.slow.C:
s.core.logf("subscriber for %s is slow (%v elapsed)", s.core.typeName, time.Since(start))
s.core.slow.Reset(slowSubscriberTimeout)
}
}
}
func newMonitor[T any](attach func(fn func(T)) (cancel func())) *Subscriber[T] {
s := &Subscriber[T]{
// Monitors don't go through the bus's dispatch path (they
// are attached directly to the debug hook), so they don't
// need a fully-initialized subscriberCore — only the typed
// delivery channel and an unregister callback. We give them
// a placeholder core so Close() and Done() work uniformly.
core: &subscriberCore{},
read: make(chan T, 100), // arbitrary, large
}
cancel := attach(s.monitor)
s.core.unregister = func(reflect.Type) { cancel() }
return s
}
func (s *Subscriber[T]) monitor(debugEvent T) {
select {
case s.read <- debugEvent:
case <-s.core.stop.Done():
}
}
// Events returns a channel on which the subscriber's events are
// delivered.
func (s *Subscriber[T]) Events() <-chan T {
@@ -258,7 +293,7 @@ func (s *Subscriber[T]) Events() <-chan T {
// Done returns a channel that is closed when the subscriber is
// closed.
func (s *Subscriber[T]) Done() <-chan struct{} {
return s.stop.Done()
return s.core.stop.Done()
}
// Close closes the Subscriber, indicating the caller no longer wishes
@@ -268,30 +303,105 @@ func (s *Subscriber[T]) Done() <-chan struct{} {
// If the Bus from which the Subscriber was created is closed,
// the Subscriber is implicitly closed and does not need to be closed
// separately.
func (s *Subscriber[T]) Close() {
s.stop.Stop() // unblock receivers
s.unregister()
}
func (s *Subscriber[T]) Close() { s.core.Close() }
// A SubscriberFunc delivers one type of event from a [Client].
// Events are forwarded synchronously to a function provided at construction.
type SubscriberFunc[T any] struct {
// core holds the non-generic subscriber-interface implementation shared
// with [Subscriber] via [subscriberCore]. The user callback is captured
// in the dispatchFn closure on the core, so SubscriberFunc[T] itself
// carries no per-T state beyond the core pointer; per-T cost is limited
// to the small forwarding Close method below.
core *subscriberCore
}
// subscriberCore is the non-generic backing for both Subscriber[T] and
// SubscriberFunc[T]. It implements the package-private subscriber interface
// so that the bus (and the subscribeState map) can store it without per-T
// itabs or dictionaries. The per-T behavior (type assertion plus either typed
// channel send or user callback invocation) is encapsulated in the dispatchFn
// closure set up by the constructor of the typed facade.
type subscriberCore struct {
stop stopFlag
read func(T)
unregister func()
unregister func(reflect.Type)
logf logger.Logf
slow *time.Timer // used to detect slow subscriber service
// typ is the cached reflect.Type of T. Returned by
// subscribeType() and used by the dispatch closure to format
// slow-subscriber log messages.
typ reflect.Type
// typeName is the cached reflect.TypeFor[T]().String() result.
// Computed once at construction time so the dispatch closure
// (which runs once per delivered event) doesn't allocate a
// fresh string on every call. The string is also independent
// of T, so it doesn't contribute to per-T stencil cost.
typeName string
// dispatchFn is the per-T dispatch closure. It performs the type
// assertion vals.Peek().Event.(T) and runs the typed delivery (either a
// user-callback invocation for SubscriberFunc[T] or a typed channel send
// for Subscriber[T]). The closure body is non-generic apart from those
// two T-bound operations; the bulk of the dispatch work happens in the
// non-generic dispatchFunc helper (used by SubscriberFunc) or in the
// Subscriber[T].dispatchTyped per-shape stencil.
dispatchFn func(
ctx context.Context,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
) bool
}
func newSubscriberFunc[T any](r *subscribeState, f func(T), logf logger.Logf) *SubscriberFunc[T] {
core := newSubscriberCore(r, logf, reflect.TypeFor[T]())
// The dispatch closure is the only piece that intrinsically
// needs T: it performs the type assertion on the head queue
// value and forwards the unboxed value to the user callback.
// All non-generic setup (timer, core allocation, unregister
// closure) lives in newSubscriberCore so it isn't
// duplicated per T.
core.dispatchFn = func(
ctx context.Context,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
) bool {
t := vals.Peek().Event.(T)
callDone := make(chan struct{})
// `go runFuncCallback(f, t, callDone)` binds its arguments
// directly to the new goroutine's frame; using a closure
// (`go func() { f(t) }()`) would allocate a closure on the
// heap on every dispatched event.
go runFuncCallback(f, t, callDone)
return dispatchFunc(ctx, core, vals, acceptCh, snapshot, callDone)
}
return &SubscriberFunc[T]{core: core}
}
// newSubscriberCore performs the non-generic portion of subscriber
// construction: timer setup, core struct allocation, and assignment of the
// unregister method-value. The caller fills in the per-T dispatchFn
// afterward.
//
// Hoisting this out of the typed constructors (newSubscriber[T] and
// newSubscriberFunc[T]) eliminates the bulk of their per-T stencil cost; the
// only T-typed instructions left in each generic constructor are the
// reflect.TypeFor[T]() call (whose body is shared via the
// internal/abi.TypeFor[T] dictionary) and the construction of the dispatch
// closure itself.
func newSubscriberCore(r *subscribeState, logf logger.Logf, typ reflect.Type) *subscriberCore {
slow := time.NewTimer(0)
slow.Stop() // reset in dispatch
return &SubscriberFunc[T]{
read: f,
unregister: func() { r.deleteSubscriber(reflect.TypeFor[T]()) },
logf: logf,
slow: slow,
core := &subscriberCore{
logf: logf,
slow: slow,
typ: typ,
typeName: typ.String(),
}
core.unregister = r.deleteSubscriber
return core
}
// Close closes the SubscriberFunc, indicating the caller no longer wishes to
@@ -300,24 +410,54 @@ func newSubscriberFunc[T any](r *subscribeState, f func(T), logf logger.Logf) *S
//
// If the [Bus] from which s was created is closed, s is implicitly closed and
// does not need to be closed separately.
func (s *SubscriberFunc[T]) Close() { s.stop.Stop(); s.unregister() }
func (s *SubscriberFunc[T]) Close() { s.core.Close() }
// subscribeType implements part of the subscriber interface.
func (s *SubscriberFunc[T]) subscribeType() reflect.Type { return reflect.TypeFor[T]() }
// Close implements the subscriber interface and the user-facing Close on
// both Subscriber[T] and SubscriberFunc[T].
func (c *subscriberCore) Close() {
c.stop.Stop()
c.unregister(c.typ)
}
// dispatch implements part of the subscriber interface.
func (s *SubscriberFunc[T]) dispatch(ctx context.Context, vals *queue[DeliveredEvent], acceptCh func() chan DeliveredEvent, snapshot chan chan []DeliveredEvent) bool {
t := vals.Peek().Event.(T)
callDone := make(chan struct{})
go s.runCallback(t, callDone)
// subscribeType implements the subscriber interface.
func (c *subscriberCore) subscribeType() reflect.Type { return c.typ }
// dispatch implements the subscriber interface by invoking the
// per-T dispatch closure that was captured at construction time.
func (c *subscriberCore) dispatch(
ctx context.Context,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
) bool {
return c.dispatchFn(ctx, vals, acceptCh, snapshot)
}
// dispatchFunc is the non-generic body of SubscriberFunc[T].dispatch.
// It is identical in observable behavior to the original loop; the
// only differences are that the dispatched value has already been
// unboxed by the caller (and the user callback is already running
// on its own goroutine, signaling completion via callDone) and the
// slow-subscriber timer / cached type name come from the
// non-generic core, not from a per-T struct.
//
// callDone is closed by runFuncCallback when the user callback returns.
func dispatchFunc(
ctx context.Context,
core *subscriberCore,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
callDone chan struct{},
) bool {
start := time.Now()
s.slow.Reset(slowSubscriberTimeout)
defer s.slow.Stop()
core.slow.Reset(slowSubscriberTimeout)
defer core.slow.Stop()
// Keep the cases in this select in sync with subscribeState.pump
// above. The only difference should be that this select
// delivers a value by calling s.read.
// delivers a value by calling the user callback (via the
// goroutine spawned by the typed wrapper).
for {
select {
case <-callDone:
@@ -327,30 +467,35 @@ func (s *SubscriberFunc[T]) dispatch(ctx context.Context, vals *queue[DeliveredE
vals.Add(val)
case <-ctx.Done():
// Wait for the callback to be complete, but not forever.
s.slow.Reset(5 * slowSubscriberTimeout)
core.slow.Reset(5 * slowSubscriberTimeout)
select {
case <-s.slow.C:
s.logf("giving up on subscriber for %T after %v at close", t, time.Since(start))
case <-core.slow.C:
core.logf("giving up on subscriber for %s after %v at close", core.typeName, time.Since(start))
if cibuild.On() {
all := make([]byte, 2<<20)
n := runtime.Stack(all, true)
s.logf("goroutine stacks:\n%s", all[:n])
core.logf("goroutine stacks:\n%s", all[:n])
}
case <-callDone:
}
return false
case ch := <-snapshot:
ch <- vals.Snapshot()
case <-s.slow.C:
s.logf("subscriber for %T is slow (%v elapsed)", t, time.Since(start))
s.slow.Reset(slowSubscriberTimeout)
case <-core.slow.C:
core.logf("subscriber for %s is slow (%v elapsed)", core.typeName, time.Since(start))
core.slow.Reset(slowSubscriberTimeout)
}
}
}
// runCallback invokes the callback on v and closes ch when it returns.
// This should be run in a goroutine.
func (s *SubscriberFunc[T]) runCallback(v T, ch chan struct{}) {
defer close(ch)
s.read(v)
// runFuncCallback runs f(t) and closes done when it returns. It is
// the per-T worker spawned as a goroutine for each dispatched
// event. Keeping it as a regular generic function (rather than a
// closure) means `go runFuncCallback(f, t, done)` binds its
// arguments to the goroutine's frame directly, with no per-event
// closure allocation. The body is small (defer + one indirect
// call), so the per-shape stencil cost is minimal.
func runFuncCallback[T any](f func(T), t T, done chan struct{}) {
defer close(done)
f(t)
}