Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
BIN
Binary file not shown.
BIN
Binary file not shown.
+10
-1
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
+54
-15
@@ -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
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
+217
-72
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user