Improved perfs
This commit is contained in:
+69
-11
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/uopi/uopi/internal/datasource"
|
||||
)
|
||||
@@ -41,18 +42,38 @@ type Broker struct {
|
||||
sources map[string]datasource.DataSource
|
||||
subs map[SignalRef]*signalSub
|
||||
|
||||
log *slog.Logger
|
||||
maxInterval time.Duration // 0 = unlimited
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// Option is a functional option for Broker configuration.
|
||||
type Option func(*Broker)
|
||||
|
||||
// WithMaxUpdateRate limits fan-out to at most hz updates per second per signal.
|
||||
// When upstream delivers faster, intermediate values are coalesced: the most
|
||||
// recent value in each interval is forwarded once the interval elapses.
|
||||
// A value ≤ 0 disables rate limiting (the default).
|
||||
func WithMaxUpdateRate(hz float64) Option {
|
||||
return func(b *Broker) {
|
||||
if hz > 0 {
|
||||
b.maxInterval = time.Duration(float64(time.Second) / hz)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a Broker whose upstream subscriptions are bound to ctx.
|
||||
// Cancel ctx (or the parent context passed to main) to shut everything down.
|
||||
func New(ctx context.Context, log *slog.Logger) *Broker {
|
||||
return &Broker{
|
||||
func New(ctx context.Context, log *slog.Logger, opts ...Option) *Broker {
|
||||
b := &Broker{
|
||||
ctx: ctx,
|
||||
sources: make(map[string]datasource.DataSource),
|
||||
subs: make(map[SignalRef]*signalSub),
|
||||
log: log,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(b)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Register adds a DataSource to the broker. Must be called before Subscribe.
|
||||
@@ -169,7 +190,36 @@ func (b *Broker) unsubscribe(ref SignalRef, ch chan<- Update) {
|
||||
|
||||
// fanOut reads values from rawCh and dispatches them to all registered clients.
|
||||
// It exits when sub.done is closed or rawCh is closed.
|
||||
//
|
||||
// When b.maxInterval > 0, updates that arrive faster than the interval are
|
||||
// coalesced: only the most recent value in each interval window is forwarded,
|
||||
// delivered at the end of the interval by a flush ticker. This ensures the
|
||||
// latest value is always eventually seen even when the source fires rapidly.
|
||||
func (b *Broker) fanOut(ref SignalRef, sub *signalSub, rawCh <-chan datasource.Value) {
|
||||
maxInterval := b.maxInterval
|
||||
|
||||
var lastSent time.Time
|
||||
var pending *Update
|
||||
|
||||
// Only allocate a ticker when rate-limiting is configured.
|
||||
var flushC <-chan time.Time
|
||||
if maxInterval > 0 {
|
||||
t := time.NewTicker(maxInterval)
|
||||
defer t.Stop()
|
||||
flushC = t.C
|
||||
}
|
||||
|
||||
dispatch := func(u Update) {
|
||||
sub.mu.RLock()
|
||||
for ch := range sub.clients {
|
||||
select {
|
||||
case ch <- u:
|
||||
default: // slow consumer: drop rather than block
|
||||
}
|
||||
}
|
||||
sub.mu.RUnlock()
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case v, ok := <-rawCh:
|
||||
@@ -177,15 +227,23 @@ func (b *Broker) fanOut(ref SignalRef, sub *signalSub, rawCh <-chan datasource.V
|
||||
return
|
||||
}
|
||||
update := Update{Ref: ref, Value: v}
|
||||
sub.mu.RLock()
|
||||
for ch := range sub.clients {
|
||||
select {
|
||||
case ch <- update:
|
||||
default:
|
||||
// slow consumer: drop rather than block
|
||||
}
|
||||
// Rate-limit: if an interval is configured and the last delivery
|
||||
// was recent, hold this update as pending (coalesce).
|
||||
if maxInterval > 0 && !lastSent.IsZero() && time.Since(lastSent) < maxInterval {
|
||||
pending = &update
|
||||
continue
|
||||
}
|
||||
lastSent = time.Now()
|
||||
pending = nil
|
||||
dispatch(update)
|
||||
|
||||
case <-flushC:
|
||||
// Deliver the most recent coalesced value, if any.
|
||||
if pending != nil {
|
||||
lastSent = time.Now()
|
||||
dispatch(*pending)
|
||||
pending = nil
|
||||
}
|
||||
sub.mu.RUnlock()
|
||||
|
||||
case <-sub.done:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user