feat(bus): 进程内事件总线(通配订阅/非阻塞扇出/Close 幂等)
This commit is contained in:
1 parent
c1fb20bd6b
commit
dd16176bb7
4 files changed
+833
No files matched your search
@@ -0,0 +1,124 @@
|
||||
// Package bus provides an in-process publish/subscribe event bus. It is the
|
||||
// asynchronous communication channel mandated by the Cyrene architecture:
|
||||
// synchronous calls go through interfaces and dependency injection, while
|
||||
// asynchronous events and agent-to-agent messages flow through this bus. The
|
||||
// API is deliberately NATS-like so the bus can later be swapped for an
|
||||
// out-of-process NATS deployment (or another transport) without changing call
|
||||
// sites; see DevDocs/00-overview.md and DevDocs/02-development-constraints.md.
|
||||
//
|
||||
// # Matching
|
||||
//
|
||||
// Both subjects and subscription patterns are dot-separated token strings.
|
||||
// Two wildcards are supported:
|
||||
//
|
||||
// - "*" matches exactly one non-empty token. For example "runtime.*.start"
|
||||
// matches "runtime.loop.start" but not "runtime.loop".
|
||||
// - ">" matches one or more trailing tokens and may only appear as the last
|
||||
// token (NATS semantics). For example "runtime.>" matches
|
||||
// "runtime.loop.start" but not "runtime".
|
||||
//
|
||||
// A pattern with no wildcard matches only an identical subject.
|
||||
//
|
||||
// # Delivery semantics
|
||||
//
|
||||
// Publish fans out asynchronously and never waits for a handler to finish.
|
||||
// Each subscription is served by a single worker goroutine reading from a
|
||||
// bounded per-subscription channel, which serializes handler execution for
|
||||
// that subscription and bounds its memory. When the buffer is full the event
|
||||
// is dropped and counted (see busImpl.Dropped) instead of blocking the
|
||||
// publisher; that trade-off is what keeps Publish non-blocking and is the
|
||||
// documented cost of an unbounded producer.
|
||||
//
|
||||
// Handler errors are ignored by this M0 core; consumers that need them should
|
||||
// capture and log them at the call site. A panic raised by a handler is
|
||||
// recovered so a misbehaving subscriber cannot take down the bus or starve
|
||||
// other subscribers.
|
||||
package bus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Event is a single published message.
|
||||
type Event struct {
|
||||
// Subject is the token string the message was published to.
|
||||
Subject string
|
||||
// Payload is the arbitrary value associated with the event.
|
||||
Payload any
|
||||
}
|
||||
|
||||
// Handler processes a delivered Event. It runs on the subscription's own
|
||||
// worker goroutine, so invocations for the same subscription are serialized.
|
||||
// A non-nil error is ignored by the M0 core (see the package comment).
|
||||
type Handler func(ctx context.Context, e Event) error
|
||||
|
||||
// Bus is the in-process publish/subscribe contract. Keep this interface
|
||||
// narrow so an alternate transport (for example NATS) can satisfy it without
|
||||
// changing consumers, and so it is trivial to mock in tests.
|
||||
type Bus interface {
|
||||
// Publish asynchronously fans an event out to every subscription whose
|
||||
// pattern matches subject. It never blocks waiting for a handler to run;
|
||||
// when a subscription's buffer is full the event is dropped for that
|
||||
// subscription and counted. It returns ErrClosed if the bus is closed and
|
||||
// a wrapped error if subject is invalid or ctx is canceled.
|
||||
Publish(ctx context.Context, subject string, payload any) error
|
||||
// Subscribe registers h for events whose subject matches pattern and
|
||||
// returns an unsubscribe function that removes the registration. When ctx
|
||||
// is canceled the registration is removed automatically. It returns
|
||||
// ErrClosed if the bus is already closed.
|
||||
Subscribe(ctx context.Context, pattern string, h Handler) (unsubscribe func(), err error)
|
||||
// Close is idempotent and safe to call concurrently. It prevents further
|
||||
// Publish/Subscribe calls, unsubscribes every registration, cancels the
|
||||
// handlers' contexts, and stops every worker. It returns nil.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Sentinel errors returned by the bus. Use errors.Is to test for them.
|
||||
var (
|
||||
// ErrClosed is returned when publishing to or subscribing on a closed bus.
|
||||
ErrClosed = errors.New("bus: closed")
|
||||
// ErrInvalidArgument is returned when a subject is empty, a pattern is
|
||||
// malformed, or a handler is nil.
|
||||
ErrInvalidArgument = errors.New("bus: invalid argument")
|
||||
)
|
||||
|
||||
// defaultQueueSize is the per-subscription event buffer used when no option
|
||||
// overrides it.
|
||||
const defaultQueueSize = 64
|
||||
|
||||
// config carries the resolved construction options.
|
||||
type config struct {
|
||||
queueSize int
|
||||
}
|
||||
|
||||
// Option customizes a bus built by New. Options are applied in order before
|
||||
// the bus starts accepting traffic.
|
||||
type Option func(*config)
|
||||
|
||||
// WithQueueSize sets the per-subscription event buffer size. Buffers bound how
|
||||
// many undelivered events a subscription may hold before events begin to be
|
||||
// dropped. Non-positive values are ignored and the default is kept.
|
||||
func WithQueueSize(n int) Option {
|
||||
return func(c *config) {
|
||||
if n > 0 {
|
||||
c.queueSize = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New builds a bus with the supplied options. The returned *busImpl
|
||||
// implements Bus and also exposes busImpl.Dropped for observability.
|
||||
func New(opts ...Option) *busImpl {
|
||||
cfg := config{queueSize: defaultQueueSize}
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &busImpl{
|
||||
queueSize: cfg.queueSize,
|
||||
subs: make(map[uint64]*subscription),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user