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.
447 lines
14 KiB
Go
447 lines
14 KiB
Go
package actor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/lightningnetwork/lnd/fn/v2"
|
|
)
|
|
|
|
// stoppable defines an interface for components that can be stopped.
|
|
// This is unexported as it's an internal detail of ActorSystem for managing
|
|
// actors that need to be shut down.
|
|
type stoppable interface {
|
|
Stop()
|
|
}
|
|
|
|
// SystemConfig holds configuration parameters for the ActorSystem.
|
|
type SystemConfig struct {
|
|
// MailboxCapacity is the default capacity for actor mailboxes.
|
|
MailboxCapacity int
|
|
}
|
|
|
|
// DefaultConfig returns a default configuration for the ActorSystem.
|
|
// The default mailbox capacity of 100 means each actor can buffer up to 100
|
|
// pending messages (envelopes). Each envelope holds a message and an optional
|
|
// promise pointer, so the memory overhead per actor is roughly proportional to
|
|
// the size of the messages being sent multiplied by this capacity.
|
|
func DefaultConfig() SystemConfig {
|
|
return SystemConfig{
|
|
MailboxCapacity: 100,
|
|
}
|
|
}
|
|
|
|
// ActorSystem manages the lifecycle of actors and provides coordination
|
|
// services such as a receptionist for actor discovery and a dead letter office
|
|
// for undeliverable messages. It also handles the graceful shutdown of all
|
|
// managed actors.
|
|
type ActorSystem struct {
|
|
// receptionist is used for actor discovery.
|
|
receptionist *Receptionist
|
|
|
|
// actors stores all actors managed by the system, keyed by their ID.
|
|
// This includes the deadLetterActor.
|
|
actors map[string]stoppable
|
|
|
|
// deadLetterActor handles undeliverable messages.
|
|
deadLetterActor ActorRef[Message, any]
|
|
|
|
// config holds the system-wide configuration.
|
|
config SystemConfig
|
|
|
|
// mu protects the 'actors' map.
|
|
mu sync.RWMutex
|
|
|
|
// ctx is the main context for the actor system.
|
|
ctx context.Context
|
|
|
|
// cancel cancels the main system context.
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewActorSystem creates a new actor system using the default configuration.
|
|
func NewActorSystem() *ActorSystem {
|
|
return NewActorSystemWithConfig(DefaultConfig())
|
|
}
|
|
|
|
// NewActorSystemWithConfig creates a new actor system with custom configuration
|
|
func NewActorSystemWithConfig(config SystemConfig) *ActorSystem {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// Initialize the core ActorSystem components.
|
|
system := &ActorSystem{
|
|
receptionist: newReceptionist(),
|
|
config: config,
|
|
actors: make(map[string]stoppable),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
|
|
// Define the behavior for the dead letter actor. It logs undeliverable
|
|
// messages and returns an error.
|
|
deadLetterBehavior := NewFunctionBehavior(
|
|
func(ctx context.Context, msg Message) fn.Result[any] {
|
|
log.Warnf("Dead letter received: message type=%s",
|
|
msg.MessageType())
|
|
|
|
return fn.Err[any](errors.New(
|
|
"message undeliverable: " + msg.MessageType(),
|
|
))
|
|
},
|
|
)
|
|
|
|
// Create the raw dead letter actor (*Actor instance). The DLO's own DLO
|
|
// reference is nil to prevent loops if messages to the DLO itself fail.
|
|
deadLetterActorCfg := ActorConfig[Message, any]{
|
|
ID: "dead-letters",
|
|
Behavior: deadLetterBehavior,
|
|
DLO: nil,
|
|
MailboxSize: config.MailboxCapacity,
|
|
}
|
|
deadLetterRawActor, err := NewActor[Message, any](deadLetterActorCfg)
|
|
if err != nil {
|
|
// This should never happen since we control the DLO config.
|
|
panic("failed to create dead letter actor: " + err.Error())
|
|
}
|
|
deadLetterRawActor.Start()
|
|
system.deadLetterActor = deadLetterRawActor.Ref()
|
|
|
|
// Add the raw actor to the map of stoppable actors. No lock needed here
|
|
// as 'system' is not yet accessible concurrently.
|
|
system.actors[deadLetterRawActor.id] = deadLetterRawActor
|
|
|
|
// The system is now fully initialized and ready.
|
|
return system
|
|
}
|
|
|
|
// ActorOption is a functional option for customizing actor creation.
|
|
type ActorOption[M Message, R any] func(*ActorConfig[M, R])
|
|
|
|
// WithMailboxFactory returns an ActorOption that sets a custom mailbox factory.
|
|
func WithMailboxFactory[M Message, R any](
|
|
f MailboxFactory[M, R]) ActorOption[M, R] {
|
|
|
|
return func(cfg *ActorConfig[M, R]) {
|
|
cfg.MailboxFactory = f
|
|
}
|
|
}
|
|
|
|
// WithMailboxSize returns an ActorOption that overrides the default mailbox
|
|
// size.
|
|
func WithMailboxSize[M Message, R any](size int) ActorOption[M, R] {
|
|
return func(cfg *ActorConfig[M, R]) {
|
|
cfg.MailboxSize = size
|
|
}
|
|
}
|
|
|
|
// RegisterWithSystem creates an actor with the given ID, service key, and
|
|
// behavior within the specified ActorSystem. It starts the actor, adds it to
|
|
// the system's management, registers it with the receptionist using the
|
|
// provided key, and returns its ActorRef.
|
|
func RegisterWithSystem[M Message, R any](as *ActorSystem, id string,
|
|
key ServiceKey[M, R],
|
|
behavior ActorBehavior[M, R],
|
|
opts ...ActorOption[M, R]) (ActorRef[M, R], error) {
|
|
|
|
actorCfg := ActorConfig[M, R]{
|
|
ID: id,
|
|
Behavior: behavior,
|
|
DLO: as.deadLetterActor,
|
|
MailboxSize: as.config.MailboxCapacity,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(&actorCfg)
|
|
}
|
|
// Check for duplicate actor ID before creating the actor.
|
|
as.mu.Lock()
|
|
if _, exists := as.actors[id]; exists {
|
|
as.mu.Unlock()
|
|
|
|
return nil, fmt.Errorf("%w: %s", ErrDuplicateActorID, id)
|
|
}
|
|
|
|
actorInstance, err := NewActor(actorCfg)
|
|
if err != nil {
|
|
as.mu.Unlock()
|
|
|
|
return nil, err
|
|
}
|
|
actorInstance.Start()
|
|
|
|
// Add the actor instance to the system's list of stoppable actors.
|
|
as.actors[actorInstance.id] = actorInstance
|
|
as.mu.Unlock()
|
|
|
|
log.Infof("ActorSystem: registered actor %s with service key %s",
|
|
id, key.name)
|
|
|
|
// Register the actor's reference with the receptionist under the given
|
|
// service key, making it discoverable by other parts of the system.
|
|
RegisterWithReceptionist(as.receptionist, key, actorInstance.Ref())
|
|
|
|
return actorInstance.Ref(), nil
|
|
}
|
|
|
|
// Receptionist returns the system's receptionist, which can be used for
|
|
// actor service discovery (finding actors by ServiceKey).
|
|
func (as *ActorSystem) Receptionist() *Receptionist {
|
|
return as.receptionist
|
|
}
|
|
|
|
// DeadLetters returns a reference to the system's dead letter actor. Messages
|
|
// that cannot be delivered to their intended recipient (e.g., if an Ask
|
|
// context is cancelled before enqueuing) may be routed here if not otherwise
|
|
// handled.
|
|
func (as *ActorSystem) DeadLetters() ActorRef[Message, any] {
|
|
return as.deadLetterActor
|
|
}
|
|
|
|
// Shutdown gracefully stops the actor system. It iterates through all managed
|
|
// actors, including the dead letter actor, and calls their Stop method.
|
|
// After initiating the stop for all actors, it cancels the main system context.
|
|
// This method is safe for concurrent use.
|
|
func (as *ActorSystem) Shutdown() error {
|
|
log.Infof("ActorSystem: initiating shutdown")
|
|
|
|
// Create a slice of actors to stop. This avoids holding the lock while
|
|
// calling Stop() on each actor, and includes the dead letter actor.
|
|
var actorsToStop []stoppable
|
|
as.mu.RLock()
|
|
for _, actor := range as.actors {
|
|
actorsToStop = append(actorsToStop, actor)
|
|
}
|
|
as.mu.RUnlock()
|
|
|
|
// Notify all managed actors to stop. Actor.Stop() is non-blocking.
|
|
// Each actor's Stop method will cancel its internal context, leading
|
|
// to the termination of its processing goroutine.
|
|
for _, actor := range actorsToStop {
|
|
actor.Stop()
|
|
}
|
|
|
|
// Clear the actors map after initiating their shutdown.
|
|
as.mu.Lock()
|
|
as.actors = nil
|
|
as.mu.Unlock()
|
|
|
|
// Finally cancel the main context
|
|
// This signals to any other components observing the system's context
|
|
// that shutdown has been initiated.
|
|
as.cancel()
|
|
|
|
return nil
|
|
}
|
|
|
|
// StopAndRemoveActor stops a specific actor by its ID and removes it from the
|
|
// ActorSystem's management. It returns true if the actor was found and stopped,
|
|
// false otherwise.
|
|
func (as *ActorSystem) StopAndRemoveActor(id string) bool {
|
|
as.mu.Lock()
|
|
defer as.mu.Unlock()
|
|
|
|
actorToStop, exists := as.actors[id]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
// Stop the actor. This is non-blocking.
|
|
actorToStop.Stop()
|
|
|
|
// Remove from the system's management.
|
|
delete(as.actors, id)
|
|
|
|
return true
|
|
}
|
|
|
|
// UnregisterFromReceptionist removes an actor reference from a service key in
|
|
// the given receptionist. It returns true if the reference was found and
|
|
// removed, and false otherwise. This is a package-level generic function
|
|
// because methods cannot have their own type parameters in Go.
|
|
func UnregisterFromReceptionist[M Message, R any](r *Receptionist,
|
|
key ServiceKey[M, R], refToRemove ActorRef[M, R]) bool {
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
refs, exists := r.registrations[key.name]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
found := false
|
|
|
|
// Build a new slice containing only the references that are not the one
|
|
// to be removed.
|
|
newRefs := make([]any, 0, max(0, len(refs)-1))
|
|
for _, itemInSlice := range refs {
|
|
// Try to assert the item from the slice to the specific
|
|
// ActorRef[M,R] type we are trying to remove.
|
|
if specificActorRef, ok := itemInSlice.(ActorRef[M, R]); ok {
|
|
// If the type assertion is successful and it's the one
|
|
// we want to remove, mark as found and skip adding it
|
|
// to newRefs.
|
|
if specificActorRef == refToRemove {
|
|
found = true
|
|
continue
|
|
}
|
|
}
|
|
newRefs = append(newRefs, itemInSlice)
|
|
}
|
|
|
|
if !found {
|
|
return false
|
|
}
|
|
|
|
// If the new list of references is empty, remove the key from the map.
|
|
// Otherwise, update the map with the new slice.
|
|
if len(newRefs) == 0 {
|
|
delete(r.registrations, key.name)
|
|
} else {
|
|
r.registrations[key.name] = newRefs
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ServiceKey is a type-safe identifier used for registering and discovering
|
|
// actors via the Receptionist. The generic type parameters M (Message) and R
|
|
// (Response) ensure that only actors handling compatible message/response types
|
|
// are associated with and retrieved for this key.
|
|
type ServiceKey[M Message, R any] struct {
|
|
name string
|
|
}
|
|
|
|
// NewServiceKey creates a new service key with the given name. The name is used
|
|
// as the lookup key within the Receptionist.
|
|
func NewServiceKey[M Message, R any](name string) ServiceKey[M, R] {
|
|
return ServiceKey[M, R]{name: name}
|
|
}
|
|
|
|
// Spawn registers an actor for this service key within the given ActorSystem.
|
|
// It's a convenience method that calls RegisterWithSystem, starting the actor
|
|
// and registering it with the receptionist.
|
|
func (sk ServiceKey[M, R]) Spawn(as *ActorSystem, id string,
|
|
behavior ActorBehavior[M, R],
|
|
opts ...ActorOption[M, R]) (ActorRef[M, R], error) {
|
|
|
|
return RegisterWithSystem(as, id, sk, behavior, opts...)
|
|
}
|
|
|
|
// Unregister removes an actor reference associated with this service key from
|
|
// the ActorSystem's receptionist and also stops the actor.
|
|
// It returns true if the actor was successfully unregistered from the
|
|
// receptionist AND successfully stopped and removed from the system's
|
|
// management. Otherwise, it returns false.
|
|
func (sk ServiceKey[M, R]) Unregister(as *ActorSystem,
|
|
refToRemove ActorRef[M, R]) bool {
|
|
|
|
unregisteredFromReceptionist := UnregisterFromReceptionist(
|
|
as.Receptionist(), sk, refToRemove,
|
|
)
|
|
|
|
// If not found in receptionist, no need to try stopping.
|
|
if !unregisteredFromReceptionist {
|
|
return false
|
|
}
|
|
|
|
// Attempt to stop and remove the actor from the system.
|
|
stoppedAndRemoved := as.StopAndRemoveActor(refToRemove.ID())
|
|
|
|
return unregisteredFromReceptionist && stoppedAndRemoved
|
|
}
|
|
|
|
// UnregisterAll finds all actor references associated with this service key in
|
|
// the ActorSystem's receptionist. For each found actor, it attempts to stop it
|
|
// and remove it from system management, and also unregisters it from the
|
|
// receptionist.
|
|
func (sk ServiceKey[M, R]) UnregisterAll(as *ActorSystem) int {
|
|
// First find all the refs that match this service key.
|
|
refsFound := FindInReceptionist(as.Receptionist(), sk)
|
|
|
|
actorsStoppedCount := 0
|
|
for _, ref := range refsFound {
|
|
// Attempt to stop and remove the actor from the system's active
|
|
// management. This is the primary action to deactivate the
|
|
// actor. If StopAndRemoveActor returns true, it means an active
|
|
// actor was found in the system's `actors` map and was stopped.
|
|
if as.StopAndRemoveActor(ref.ID()) {
|
|
actorsStoppedCount++
|
|
}
|
|
|
|
// Regardless of whether the actor was actively managed by the
|
|
// system (i.e., found in as.actors), attempt to unregister its
|
|
// reference from the receptionist. This helps clean up any
|
|
// potentially stale entries in the receptionist if an actor was
|
|
// removed from the system's management without also being
|
|
// unregistered from the receptionist.
|
|
UnregisterFromReceptionist(as.Receptionist(), sk, ref)
|
|
}
|
|
|
|
return actorsStoppedCount
|
|
}
|
|
|
|
// Receptionist provides service discovery for actors. Actors can be registered
|
|
// under a ServiceKey and later discovered by other actors or system components.
|
|
type Receptionist struct {
|
|
// registrations stores ActorRef instances, keyed by ServiceKey.name.
|
|
registrations map[string][]any
|
|
|
|
// mu protects access to registrations.
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// newReceptionist creates a new Receptionist instance.
|
|
func newReceptionist() *Receptionist {
|
|
return &Receptionist{
|
|
registrations: make(map[string][]any),
|
|
}
|
|
}
|
|
|
|
// RegisterWithReceptionist registers an actor with a service key in the given
|
|
// receptionist. This is a package-level generic function because methods
|
|
// cannot have their own type parameters in Go (as of the current version).
|
|
// It appends the actor reference to the list associated with the key's name.
|
|
func RegisterWithReceptionist[M Message, R any](r *Receptionist,
|
|
key ServiceKey[M, R], ref ActorRef[M, R]) {
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
// Initialize the slice for this key if it's the first registration.
|
|
if _, exists := r.registrations[key.name]; !exists {
|
|
r.registrations[key.name] = make([]any, 0)
|
|
}
|
|
|
|
r.registrations[key.name] = append(r.registrations[key.name], ref)
|
|
}
|
|
|
|
// FindInReceptionist returns all actors registered with a service key in the
|
|
// given receptionist. This is a package-level generic function because methods
|
|
// cannot have their own type parameters. It performs a type assertion to ensure
|
|
// that only ActorRefs matching the ServiceKey's generic types (M, R) are
|
|
// returned, providing type safety.
|
|
func FindInReceptionist[M Message, R any](r *Receptionist,
|
|
key ServiceKey[M, R]) []ActorRef[M, R] {
|
|
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
if refs, exists := r.registrations[key.name]; exists {
|
|
typedRefs := make([]ActorRef[M, R], 0, len(refs))
|
|
for _, ref := range refs {
|
|
// Make sure that the reference is of the correct type.
|
|
// This type assertion is crucial for type safety, ensuring
|
|
// that the returned ActorRefs match the expected M and R.
|
|
if typedRef, ok := ref.(ActorRef[M, R]); ok {
|
|
typedRefs = append(typedRefs, typedRef)
|
|
}
|
|
}
|
|
|
|
return typedRefs
|
|
}
|
|
|
|
return nil
|
|
}
|