mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-13 12:32:48 +02:00
Add BackpressureMailbox, a Mailbox implementation backed by queue.BackpressureQueue that consults a queue.DropCheckFunc on every Send/TrySend to enable RED-style load shedding before the mailbox is full. Add MailboxFactory type and ActorOption functional options (WithMailboxFactory, WithMailboxSize) so callers can inject custom mailbox implementations when spawning actors via RegisterWithSystem or ServiceKey.Spawn.
294 lines
9.2 KiB
Go
294 lines
9.2 KiB
Go
package actor
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/lightningnetwork/lnd/fn/v2"
|
|
)
|
|
|
|
// MailboxFactory is a function type that creates a Mailbox implementation.
|
|
// It receives the actor's context and the desired capacity, allowing custom
|
|
// mailbox implementations (e.g., BackpressureMailbox) to be injected.
|
|
type MailboxFactory[M Message, R any] func(ctx context.Context,
|
|
capacity int) Mailbox[M, R]
|
|
|
|
// ActorConfig holds the configuration parameters for creating a new Actor.
|
|
// It is generic over M (Message type) and R (Response type) to accommodate
|
|
// the actor's specific behavior.
|
|
type ActorConfig[M Message, R any] struct {
|
|
// ID is the unique identifier for the actor.
|
|
ID string
|
|
|
|
// Behavior defines how the actor responds to messages.
|
|
Behavior ActorBehavior[M, R]
|
|
|
|
// DLO is a reference to the dead letter office for this actor system.
|
|
// If nil, undeliverable messages during shutdown or due to a full
|
|
// mailbox (if such logic were added) might be dropped.
|
|
DLO ActorRef[Message, any]
|
|
|
|
// MailboxSize defines the buffer capacity of the actor's mailbox.
|
|
MailboxSize int
|
|
|
|
// MailboxFactory is an optional factory for creating the actor's
|
|
// mailbox. If nil, a default ChannelMailbox will be used.
|
|
MailboxFactory MailboxFactory[M, R]
|
|
}
|
|
|
|
// envelope wraps a message with its associated promise. This allows the sender
|
|
// of an "ask" message to await a response. If the promise is nil, it
|
|
// signifies a "tell" operation (fire-and-forget).
|
|
type envelope[M Message, R any] struct {
|
|
message M
|
|
promise Promise[R]
|
|
}
|
|
|
|
// Actor represents a concrete actor implementation. It encapsulates a behavior,
|
|
// manages its internal state implicitly through that behavior, and processes
|
|
// messages from its mailbox sequentially in its own goroutine.
|
|
type Actor[M Message, R any] struct {
|
|
// id is the unique identifier for the actor.
|
|
id string
|
|
|
|
// behavior defines how the actor responds to messages.
|
|
behavior ActorBehavior[M, R]
|
|
|
|
// mailbox is the incoming message queue for the actor.
|
|
mailbox Mailbox[M, R]
|
|
|
|
// ctx is the context governing the actor's lifecycle.
|
|
ctx context.Context
|
|
|
|
// cancel is the function to cancel the actor's context.
|
|
cancel context.CancelFunc
|
|
|
|
// dlo is a reference to the dead letter office for this actor system.
|
|
dlo ActorRef[Message, any]
|
|
|
|
// startOnce ensures the actor's processing loop is started only once.
|
|
startOnce sync.Once
|
|
|
|
// stopOnce ensures the actor's processing loop is stopped only once.
|
|
stopOnce sync.Once
|
|
|
|
// ref is the cached ActorRef for this actor.
|
|
ref ActorRef[M, R]
|
|
}
|
|
|
|
// NewActor creates a new actor instance with the given ID and behavior.
|
|
// It initializes the actor's internal structures but does not start its
|
|
// message processing goroutine. The Start() method must be called to begin
|
|
// processing messages.
|
|
func NewActor[M Message, R any](cfg ActorConfig[M, R]) (*Actor[M, R],
|
|
error) {
|
|
|
|
if cfg.ID == "" {
|
|
return nil, ErrEmptyActorID
|
|
}
|
|
|
|
if cfg.Behavior == nil {
|
|
return nil, ErrNilBehavior
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// Ensure MailboxSize has a sane default if not specified or zero. A
|
|
// capacity of 0 would make the channel unbuffered, which is generally
|
|
// not desired for actor mailboxes.
|
|
mailboxCapacity := cfg.MailboxSize
|
|
if mailboxCapacity <= 0 {
|
|
// Default to a small capacity if an invalid one is given. This
|
|
// could also come from a global constant.
|
|
mailboxCapacity = 1
|
|
}
|
|
|
|
// Create the mailbox using the factory if provided, otherwise use
|
|
// the default ChannelMailbox.
|
|
var mailbox Mailbox[M, R]
|
|
if cfg.MailboxFactory != nil {
|
|
mailbox = cfg.MailboxFactory(ctx, mailboxCapacity)
|
|
} else {
|
|
mailbox = NewChannelMailbox[M, R](ctx, mailboxCapacity)
|
|
}
|
|
|
|
actor := &Actor[M, R]{
|
|
id: cfg.ID,
|
|
behavior: cfg.Behavior,
|
|
mailbox: mailbox,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
dlo: cfg.DLO,
|
|
}
|
|
|
|
// Create and cache the actor's own reference.
|
|
actor.ref = &actorRefImpl[M, R]{
|
|
actor: actor,
|
|
}
|
|
|
|
return actor, nil
|
|
}
|
|
|
|
// Start initiates the actor's message processing loop in a new goroutine. This
|
|
// method should be called once after the actor is created.
|
|
func (a *Actor[M, R]) Start() {
|
|
a.startOnce.Do(func() {
|
|
log.Infof("Actor %s: starting", a.id)
|
|
|
|
go a.process()
|
|
})
|
|
}
|
|
|
|
// process is the main event loop for the actor. It continuously monitors its
|
|
// mailbox for incoming messages and its context for cancellation signals.
|
|
func (a *Actor[M, R]) process() {
|
|
// Use the new iterator pattern for receiving messages.
|
|
for env := range a.mailbox.Receive(a.ctx) {
|
|
result := a.behavior.Receive(a.ctx, env.message)
|
|
|
|
// If a promise was provided (i.e., it was an "ask"
|
|
// operation), complete the promise with the result from
|
|
// the behavior.
|
|
if env.promise != nil {
|
|
env.promise.Complete(result)
|
|
}
|
|
}
|
|
|
|
// Context was cancelled or mailbox closed, drain remaining messages.
|
|
a.mailbox.Close()
|
|
|
|
for env := range a.mailbox.Drain() {
|
|
// If a DLO is configured, send the original message there
|
|
// for auditing or potential manual reprocessing.
|
|
if a.dlo != nil {
|
|
a.dlo.Tell(context.Background(), env.message)
|
|
}
|
|
|
|
// If it was an Ask, complete the promise with an error
|
|
// indicating the actor terminated.
|
|
if env.promise != nil {
|
|
env.promise.Complete(fn.Err[R](ErrActorTerminated))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop signals the actor to terminate its processing loop and shut down.
|
|
// This is achieved by cancelling the actor's internal context. The actor's
|
|
// goroutine will exit once it detects the context cancellation.
|
|
func (a *Actor[M, R]) Stop() {
|
|
a.stopOnce.Do(func() {
|
|
log.Infof("Actor %s: stopping", a.id)
|
|
|
|
a.cancel()
|
|
})
|
|
}
|
|
|
|
// actorRefImpl provides a concrete implementation of the ActorRef interface. It
|
|
// holds a reference to the target Actor instance, enabling message sending.
|
|
type actorRefImpl[M Message, R any] struct {
|
|
actor *Actor[M, R]
|
|
}
|
|
|
|
// Tell sends a message without waiting for a response. If the context is
|
|
// cancelled before the message can be sent to the actor's mailbox, the message
|
|
// may be dropped.
|
|
//
|
|
//nolint:ll
|
|
func (ref *actorRefImpl[M, R]) Tell(ctx context.Context, msg M) {
|
|
// If the actor's own context is already done, don't try to send.
|
|
// Route to DLO if available.
|
|
if ref.actor.ctx.Err() != nil {
|
|
ref.trySendToDLO(msg)
|
|
return
|
|
}
|
|
|
|
env := envelope[M, R]{message: msg, promise: nil}
|
|
|
|
// Use mailbox Send method which internally checks both contexts.
|
|
if !ref.actor.mailbox.Send(ctx, env) {
|
|
// Failed to send - check if actor terminated.
|
|
if ref.actor.ctx.Err() != nil {
|
|
ref.trySendToDLO(msg)
|
|
}
|
|
// Otherwise the message was either dropped by backpressure
|
|
// (load shedding) or the caller's context was cancelled.
|
|
// Both are intentionally silent — no DLO routing.
|
|
}
|
|
}
|
|
|
|
// Ask sends a message and returns a Future for the response. The Future will be
|
|
// completed with the actor's reply or an error if the operation fails (e.g.,
|
|
// context cancellation before send).
|
|
//
|
|
//nolint:ll
|
|
func (ref *actorRefImpl[M, R]) Ask(ctx context.Context, msg M) Future[R] {
|
|
// Create a new promise that will be fulfilled with the actor's response.
|
|
promise := NewPromise[R]()
|
|
|
|
// If the actor's own context is already done, complete the promise with
|
|
// ErrActorTerminated and return immediately. This is the primary guard
|
|
// against trying to send to a stopped actor.
|
|
if ref.actor.ctx.Err() != nil {
|
|
promise.Complete(fn.Err[R](ErrActorTerminated))
|
|
return promise.Future()
|
|
}
|
|
|
|
// Check if the context is already done before attempting to send. This
|
|
// ensures deterministic behavior and prevents a race where the message
|
|
// could be enqueued even though the context was already cancelled.
|
|
if ctx.Err() != nil {
|
|
promise.Complete(fn.Err[R](ctx.Err()))
|
|
return promise.Future()
|
|
}
|
|
|
|
env := envelope[M, R]{message: msg, promise: promise}
|
|
|
|
// Use mailbox Send method which internally checks both contexts.
|
|
if !ref.actor.mailbox.Send(ctx, env) {
|
|
// Determine the error based on what failed.
|
|
switch {
|
|
case ref.actor.ctx.Err() != nil:
|
|
promise.Complete(fn.Err[R](ErrActorTerminated))
|
|
case ctx.Err() != nil:
|
|
promise.Complete(fn.Err[R](ctx.Err()))
|
|
default:
|
|
// Neither context is done — the mailbox's
|
|
// backpressure mechanism dropped the message.
|
|
promise.Complete(fn.Err[R](ErrMessageDropped))
|
|
}
|
|
}
|
|
|
|
// Return the future associated with the promise, allowing the caller to
|
|
// await the response.
|
|
return promise.Future()
|
|
}
|
|
|
|
// trySendToDLO attempts to send the message to the actor's DLO if configured.
|
|
func (ref *actorRefImpl[M, R]) trySendToDLO(msg M) {
|
|
if ref.actor.dlo != nil {
|
|
// Use context.Background() for sending to DLO as the
|
|
// original context might be done or the operation
|
|
// should not be bound by it.
|
|
// This Tell to DLO is fire-and-forget.
|
|
ref.actor.dlo.Tell(context.Background(), msg)
|
|
}
|
|
}
|
|
|
|
// ID returns the unique identifier for this actor.
|
|
func (ref *actorRefImpl[M, R]) ID() string {
|
|
return ref.actor.id
|
|
}
|
|
|
|
// Ref returns an ActorRef for this actor. This allows clients to interact with
|
|
// the actor (send messages) without having direct access to the Actor struct
|
|
// itself, promoting encapsulation and location transparency.
|
|
func (a *Actor[M, R]) Ref() ActorRef[M, R] {
|
|
return a.ref
|
|
}
|
|
|
|
// TellRef returns a TellOnlyRef for this actor. This allows clients to send
|
|
// messages to the actor using only the "tell" pattern (fire-and-forget),
|
|
// without having access to "ask" capabilities.
|
|
func (a *Actor[M, R]) TellRef() TellOnlyRef[M] {
|
|
return a.ref
|
|
}
|