mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-10-06 21:52:48 +02:00
actor: add README.md
In this commit, we add a readme which serves as a general introduction to the pacakge, and also the motivation of the package. It serves as a manual for developers that may wish to interact with the package.
This commit is contained in:
475
actor/README.md
Normal file
475
actor/README.md
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
# Actor Package
|
||||||
|
|
||||||
|
## Introduction to Actors
|
||||||
|
|
||||||
|
The actor model is a conceptual model for concurrent computation that treats
|
||||||
|
"actors" as the universal primitives of concurrent computation. Originating from
|
||||||
|
Carl Hewitt's work in the 1970s and popularized by languages like Erlang and
|
||||||
|
frameworks like Akka, actors provide a high-level abstraction for building
|
||||||
|
robust, concurrent, and distributed systems.
|
||||||
|
|
||||||
|
At its core, an actor is an independent unit of computation that encapsulates:
|
||||||
|
- **State**: An actor can maintain private state that it alone can modify.
|
||||||
|
- **Behavior**: An actor defines how it reacts to messages it receives.
|
||||||
|
- **Mailbox**: Each actor has a mailbox to queue incoming messages.
|
||||||
|
|
||||||
|
Actors communicate exclusively through asynchronous message passing. When an
|
||||||
|
actor receives a message, it can:
|
||||||
|
1. Send a finite number of messages to other actors.
|
||||||
|
2. Create a finite number of new actors.
|
||||||
|
3. Designate the behavior to be used for the next message it receives (which
|
||||||
|
can be the same behavior).
|
||||||
|
|
||||||
|
|
||||||
|
Concurrency is managed by the actor system, allowing many actors to
|
||||||
|
This model inherently promotes loose coupling, as actors do not share state
|
||||||
|
execute concurrently without explicit lock management by the developer for actor
|
||||||
|
state.
|
||||||
|
|
||||||
|
## Motivation for this Package
|
||||||
|
|
||||||
|
In large, long-lived systems like `lnd`, managing complexity, concurrency, and
|
||||||
|
component lifecycles becomes increasingly challenging. This `actor` package is
|
||||||
|
introduced to address several key motivations:
|
||||||
|
|
||||||
|
### Structured Message Passing
|
||||||
|
|
||||||
|
To move away from direct, synchronous method calls between major components,
|
||||||
|
especially where concurrency or complex state interactions are involved. Message
|
||||||
|
passing encourages clearer, more auditable interactions and helps manage
|
||||||
|
concurrent access to component state.
|
||||||
|
|
||||||
|
### Eliminating "God Structs"
|
||||||
|
|
||||||
|
Over time, systems can develop large "god structs" that hold references to
|
||||||
|
numerous sub-systems. This leads to tight coupling, makes dependency management
|
||||||
|
difficult, and can obscure the flow of control and data. Actors, by
|
||||||
|
encapsulating state and behavior and interacting via messages, help break down
|
||||||
|
these monolithic structures into more manageable, independent units.
|
||||||
|
|
||||||
|
### Decoupled Lifecycles
|
||||||
|
|
||||||
|
Often, the lifecycle of a sub-system is unnecessarily tied to a parent system,
|
||||||
|
or access to a sub-system requires traversing through a central "manager"
|
||||||
|
object. Actors can have independent lifecycles managed by an actor system,
|
||||||
|
allowing for more granular control over starting, stopping, and restarting
|
||||||
|
components.
|
||||||
|
|
||||||
|
An example of such interaction is when an RPC call needs to go through several
|
||||||
|
other structs to obtain a reference to a given sub-system, in order to make a
|
||||||
|
direct method call on that sub-system.
|
||||||
|
|
||||||
|
With the model described in this document, the RPC server just needs to know
|
||||||
|
about what is effectively an _abstract address_ of that sub-system. It can then
|
||||||
|
use that to obtain something similar to a mailbox to do the method call.
|
||||||
|
|
||||||
|
This allows for a more decoupled architecture, as the RPC server doesn't need to
|
||||||
|
know the exact "shape" of the method to call, just which message to send.
|
||||||
|
Refactors of the sub-system won't break the RPC server, as long as the message
|
||||||
|
(which can be constructed via a dedicated constructor) is the same.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This package provides a foundational actor framework tailored for Go, enabling
|
||||||
|
developers to build components that are easier to reason about, test, and
|
||||||
|
maintain in a concurrent environment.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
Let's explore the fundamental building blocks provided by this package.
|
||||||
|
|
||||||
|
### Messages
|
||||||
|
|
||||||
|
Actors communicate by sending and receiving messages. Any type that an actor
|
||||||
|
needs to process must implement the `actor.Message` interface. A simple way to
|
||||||
|
do this is by embedding `actor.BaseMessage`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package mymodule
|
||||||
|
|
||||||
|
import "github.com/lightningnetwork/lnd/actor"
|
||||||
|
|
||||||
|
// MyRequest is a custom message type.
|
||||||
|
type MyRequest struct {
|
||||||
|
// Embed BaseMessage to satisfy the Message interface.
|
||||||
|
actor.BaseMessage
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageType returns a string identifier for this message type.
|
||||||
|
func (m *MyRequest) MessageType() string {
|
||||||
|
return "MyRequest"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyResponse might be a corresponding response type.
|
||||||
|
type MyResponse struct {
|
||||||
|
actor.BaseMessage
|
||||||
|
Reply string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MyResponse) MessageType() string {
|
||||||
|
return "MyResponse"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The `MessageType()` method provides a string representation of the message type,
|
||||||
|
which can be useful for debugging or routing.
|
||||||
|
|
||||||
|
|
||||||
|
### Actor Behavior
|
||||||
|
|
||||||
|
The logic of an actor (how it responds to messages) is defined by its
|
||||||
|
`ActorBehavior`. This is an interface that you implement:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package actor
|
||||||
|
|
||||||
|
// ActorBehavior defines the logic for how an actor processes incoming messages.
|
||||||
|
type ActorBehavior[M Message, R any] interface {
|
||||||
|
Receive(actorCtx context.Context, msg M) fn.Result[R]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The `Receive` method passes in a caller context (useful for shutdown detection)
|
||||||
|
and the incoming message. It returns an `fn.Result[R]`, which can encapsulate
|
||||||
|
either a successful response of type `R` or an error.
|
||||||
|
|
||||||
|
For simple cases, you can use `actor.FunctionBehavior` to adapt a Go function
|
||||||
|
into an `ActorBehavior`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/lightningnetwork/lnd/actor"
|
||||||
|
"github.com/lightningnetwork/lnd/fn/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// myActorLogic defines the processing for MyRequest messages.
|
||||||
|
func myActorLogic(ctx context.Context, msg *MyRequest) fn.Result[*MyResponse] {
|
||||||
|
// In a real actor, you might interact with state or other services.
|
||||||
|
// The actor's context (ctx) can be checked for shutdown signals.
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fn.Err[*MyResponse](errors.New("actor shutting down"))
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &MyResponse{Reply: fmt.Sprintf("Processed: %s", msg.Data)}
|
||||||
|
return fn.Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a behavior from the function.
|
||||||
|
behavior := actor.NewFunctionBehavior(myActorLogic)
|
||||||
|
```
|
||||||
|
|
||||||
|
For more complex cases, you can implement the `Receive` method on a new struct,
|
||||||
|
and pass that around directly.
|
||||||
|
|
||||||
|
### Service Keys and Actor References: The Interaction Layer
|
||||||
|
|
||||||
|
Direct interaction with an actor's internal state or its concrete struct is
|
||||||
|
discouraged. Instead, communication and discovery are managed through two key
|
||||||
|
abstractions: `ServiceKey` and `ActorRef`. These provide a layer of indirection,
|
||||||
|
promoting loose coupling and location transparency (though the current
|
||||||
|
implementation is in-process).
|
||||||
|
|
||||||
|
#### `ServiceKey[M Message, R any]`
|
||||||
|
|
||||||
|
A `ServiceKey` is a type-safe identifier used for registering actors that
|
||||||
|
provide a particular service and for discovering them later. The generic type
|
||||||
|
parameters `M` (the type of message the actor handles) and `R` (the type of
|
||||||
|
response the actor produces for `Ask` operations) ensure that you discover
|
||||||
|
actors compatible with the interactions you intend to perform.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Define a service key for actors that handle MyRequest and produce MyResponse.
|
||||||
|
myServiceKey := actor.NewServiceKey[*MyRequest, *MyResponse]("my-custom-service")
|
||||||
|
|
||||||
|
// Later, this key would be used with a Receptionist (part of an ActorSystem)
|
||||||
|
// to find ActorRefs for actors offering this service.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `ActorRef[M Message, R any]`
|
||||||
|
|
||||||
|
An `ActorRef` is a lightweight, shareable reference to an actor. It's the
|
||||||
|
primary means by which you send messages to an actor. It is also generic over
|
||||||
|
the message type `M` and response type `R` that the target actor handles.
|
||||||
|
|
||||||
|
You typically obtain an `ActorRef` by looking it up in a `Receptionist` using a
|
||||||
|
`ServiceKey` (covered later when discussing the `ActorSystem`), or directly from
|
||||||
|
an actor instance via its `.Ref()` method (e.g., `sampleActor.Ref()` if you have
|
||||||
|
the `Actor` instance).
|
||||||
|
|
||||||
|
There are two main ways to send messages using an `ActorRef`:
|
||||||
|
|
||||||
|
1. **Tell (Fire-and-Forget)**: Used for sending messages when you don't need a
|
||||||
|
direct reply. The call returns immediately after attempting to enqueue the
|
||||||
|
message.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Assuming 'actorRef' is an ActorRef[*MyRequest, *MyResponse] obtained for an actor.
|
||||||
|
requestMsg := &MyRequest{Data: "A fire-and-forget message"}
|
||||||
|
actorRef.Tell(context.Background(), requestMsg)
|
||||||
|
// The message is now in the actor's mailbox (or will be shortly).
|
||||||
|
```
|
||||||
|
The `context.Context` passed to `Tell` can be used to cancel the send
|
||||||
|
operation if, for example, the actor's mailbox is full and the send would
|
||||||
|
block for too long.
|
||||||
|
|
||||||
|
2. **Ask (Request-Response)**: Used when you need a response from the actor.
|
||||||
|
This returns a `Future[R]`, which represents the eventual reply.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Assuming 'actorRef' is an ActorRef[*MyRequest, *MyResponse].
|
||||||
|
askMsg := &MyRequest{Data: "A request needing a response"}
|
||||||
|
futureResponse := actorRef.Ask(context.Background(), askMsg)
|
||||||
|
```
|
||||||
|
A `Future[R]` represents a result that will be available at some point. You
|
||||||
|
can block until it's ready using `Await`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Await the result. It's good practice to use a context with a timeout.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := futureResponse.Await(ctx)
|
||||||
|
response, err := result.Unpack()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Ask failed: %v\n", err)
|
||||||
|
// return or handle error
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Received reply: %s\n", response.Reply)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The `Future` interface also offers non-blocking ways to handle results, like
|
||||||
|
`OnComplete` (for callbacks) and `ThenApply` (for chaining transformations).
|
||||||
|
A more restricted `TellOnlyRef[M]` is also available if only fire-and-forget
|
||||||
|
semantics are required (obtained via an actor's `TellRef()` method).
|
||||||
|
|
||||||
|
### Actors
|
||||||
|
|
||||||
|
An `Actor` is the concrete entity that runs a behavior, manages a mailbox, and
|
||||||
|
has a lifecycle. You create an actor using `actor.NewActor` with an
|
||||||
|
`ActorConfig`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg := actor.ActorConfig[*MyRequest, *MyResponse]{
|
||||||
|
ID: "my-sample-actor",
|
||||||
|
Behavior: behavior,
|
||||||
|
MailboxSize: 10,
|
||||||
|
// Dead Letter Office (covered later)
|
||||||
|
DLO: nil,
|
||||||
|
}
|
||||||
|
sampleActor := actor.NewActor(cfg)
|
||||||
|
```
|
||||||
|
|
||||||
|
An actor doesn't start processing messages until its `Start()` method is called.
|
||||||
|
This launches a dedicated goroutine for the actor.
|
||||||
|
|
||||||
|
```go
|
||||||
|
sampleActor.Start()
|
||||||
|
```
|
||||||
|
To stop an actor, you call its `Stop()` method. This cancels the actor's
|
||||||
|
internal context, causing its goroutine to clean up and exit.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Sometime later...
|
||||||
|
sampleActor.Stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Visualizing Actor Relationships
|
||||||
|
|
||||||
|
The following diagram illustrates the primary components of the actor package
|
||||||
|
and their relationships. It provides a high-level overview of how actors are
|
||||||
|
managed, discovered, and interacted with.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
direction TB
|
||||||
|
|
||||||
|
class ActorSystem {
|
||||||
|
+Receptionist
|
||||||
|
+DeadLetters
|
||||||
|
+Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Receptionist {
|
||||||
|
+Find(ServiceKey) ActorRef[]
|
||||||
|
+Register(ServiceKey, ActorRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeadLetterOffice {
|
||||||
|
+Receive(undeliverable Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServiceKey {
|
||||||
|
+Spawn(ActorSystem, Behavior) ActorRef
|
||||||
|
}
|
||||||
|
|
||||||
|
class Actor {
|
||||||
|
-mailbox
|
||||||
|
-behavior
|
||||||
|
+Ref() ActorRef
|
||||||
|
+Start()
|
||||||
|
+Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActorRef {
|
||||||
|
<<Interface>>
|
||||||
|
+Tell(Message)
|
||||||
|
+Ask(Message) Future
|
||||||
|
}
|
||||||
|
|
||||||
|
class Message {
|
||||||
|
<<Interface>>
|
||||||
|
}
|
||||||
|
|
||||||
|
class Future {
|
||||||
|
+Await() Result
|
||||||
|
}
|
||||||
|
|
||||||
|
class Router {
|
||||||
|
+Tell(Message)
|
||||||
|
+Ask(Message) Future
|
||||||
|
}
|
||||||
|
|
||||||
|
%% Core system relationships
|
||||||
|
ActorSystem *-- Receptionist : has
|
||||||
|
ActorSystem *-- DeadLetterOffice : provides
|
||||||
|
ActorSystem o-- "manages" Actor
|
||||||
|
|
||||||
|
%% Actor and communication
|
||||||
|
Actor --> ActorRef : provides
|
||||||
|
Actor ..> Message : processes
|
||||||
|
ActorRef ..> Message : sends
|
||||||
|
ActorRef ..> Future : returns for Ask
|
||||||
|
|
||||||
|
%% Service discovery and routing
|
||||||
|
Receptionist o-- ServiceKey : uses for lookup
|
||||||
|
ServiceKey ..> Actor : creates
|
||||||
|
Router --> ActorRef : routes to
|
||||||
|
Router --> Receptionist : discovers actors via
|
||||||
|
|
||||||
|
note for ActorSystem "Central manager for actor lifecycle and service discovery"
|
||||||
|
note for Actor "Independent unit with encapsulated state and behavior"
|
||||||
|
note for ActorRef "Location-transparent handle for sending messages"
|
||||||
|
note for Message "Data exchanged between actors"
|
||||||
|
note for ServiceKey "Type-safe identifier for actor registration and discovery"
|
||||||
|
note for Router "Distributes messages among multiple actors"
|
||||||
|
note for DeadLetterOffice "Handles messages that cannot be delivered"
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Actor System
|
||||||
|
|
||||||
|
While individual actors are useful, they often need to be managed and
|
||||||
|
coordinated. The `ActorSystem` serves this purpose.
|
||||||
|
|
||||||
|
```go
|
||||||
|
system := actor.NewActorSystem()
|
||||||
|
// Ensures all actors in the system are stopped.
|
||||||
|
defer system.Shutdown()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actor Lifecycle and Registration
|
||||||
|
|
||||||
|
The `ActorSystem` can manage the lifecycle of actors. You can register actors
|
||||||
|
with the system:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Using 'behavior' from earlier and 'myServiceKey' defined in the
|
||||||
|
// "Service Keys and Actor References" section.
|
||||||
|
|
||||||
|
// RegisterWithSystem creates, starts, and registers the actor.
|
||||||
|
actorRefFromSystem := actor.RegisterWithSystem(
|
||||||
|
system, "system-managed-actor", myServiceKey, behavior,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, a `ServiceKey` itself provides a `Spawn` method for convenience:
|
||||||
|
```go
|
||||||
|
actorRefSpawned := myServiceKey.Spawn(system, "spawned-actor", behavior)
|
||||||
|
```
|
||||||
|
|
||||||
|
Actors registered with the system are automatically stopped when
|
||||||
|
`system.Shutdown()` is called. You can also stop and remove individual actors
|
||||||
|
using `system.StopAndRemoveActor(actorID)`.
|
||||||
|
|
||||||
|
A `ServiceKey` is essentially the mailbox address of an actor.
|
||||||
|
|
||||||
|
### Receptionist: Service Discovery
|
||||||
|
|
||||||
|
Actors often need to find other actors to communicate with. The `Receptionist`
|
||||||
|
facilitates this. Actors are registered with the receptionist using a
|
||||||
|
`ServiceKey`, which is type-safe.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get the system's receptionist.
|
||||||
|
receptionist := system.Receptionist()
|
||||||
|
|
||||||
|
// Find actors registered for a specific service key.
|
||||||
|
foundRefs := actor.FindInReceptionist(receptionist, myServiceKey)
|
||||||
|
if len(foundRefs) > 0 {
|
||||||
|
targetActor := foundRefs[0]
|
||||||
|
targetActor.Tell(context.Background(), &MyRequest{Data: "Hello from a discoverer!"})
|
||||||
|
} else {
|
||||||
|
fmt.Println("No actors found for service key:", myServiceKey)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
When an actor is stopped (e.g., via `ServiceKey.Unregister` or system shutdown),
|
||||||
|
it should also be unregistered from the receptionist.
|
||||||
|
|
||||||
|
### Dead Letter Office (DLO)
|
||||||
|
|
||||||
|
What happens to messages that cannot be delivered? For example, if an actor is
|
||||||
|
stopped while messages are still in its mailbox, or if a message is sent to an
|
||||||
|
actor that doesn't exist (though the current `ActorRef` design makes the latter
|
||||||
|
less likely for direct sends).
|
||||||
|
|
||||||
|
The `ActorSystem` provides a default `DeadLetterActor`. When an actor is
|
||||||
|
configured (via `ActorConfig.DLO`), undeliverable messages (e.g., those drained
|
||||||
|
from its mailbox upon shutdown) can be routed to this DLO. This allows for
|
||||||
|
logging, auditing, or potential manual intervention for "lost" messages.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Actors created via RegisterWithSystem or ServiceKey.Spawn
|
||||||
|
// are automatically configured to use the system's DLO.
|
||||||
|
// system.DeadLetters() returns an ActorRef to the system's DLO.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routers: Distributing Work
|
||||||
|
|
||||||
|
Sometimes, you might have multiple actors performing the same kind of task, and
|
||||||
|
you want to distribute messages among them. A `Router` can do this. It's not an
|
||||||
|
actor itself but acts as a dispatcher.
|
||||||
|
|
||||||
|
A `Router` uses a `RoutingStrategy` to pick one actor from a group registered
|
||||||
|
under a `ServiceKey`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Assume 'system' and 'myServiceKey' are set up, and multiple actors
|
||||||
|
// are registered with 'myServiceKey'.
|
||||||
|
|
||||||
|
// Create a round-robin routing strategy.
|
||||||
|
roundRobinStrategy := actor.NewRoundRobinStrategy[*MyRequest, *MyResponse]()
|
||||||
|
|
||||||
|
// Create a router for 'myServiceKey' using this strategy.
|
||||||
|
// Messages sent to this router will be forwarded to one of the actors
|
||||||
|
// registered under 'myServiceKey'.
|
||||||
|
// The router also needs a DLO for messages it can't route (e.g., if no actors are available).
|
||||||
|
serviceRouter := actor.NewRouter(
|
||||||
|
system.Receptionist(),
|
||||||
|
myServiceKey,
|
||||||
|
roundRobinStrategy,
|
||||||
|
system.DeadLetters(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now, interact with the router as if it were an ActorRef:
|
||||||
|
serviceRouter.Tell(context.Background(), &MyRequest{Data: "Message via router"})
|
||||||
|
|
||||||
|
futureReplyFromRouter := serviceRouter.Ask(context.Background(), &MyRequest{Data: "Ask via router"})
|
||||||
|
// ... await futureReplyFromRouter ...
|
||||||
|
```
|
||||||
|
If the router cannot find any available actors for the `ServiceKey` (e.g., none
|
||||||
|
are registered or running), `Tell` operations will typically send the message to
|
||||||
|
the router's configured DLO, and `Ask` operations will return a `Future`
|
||||||
|
completed with `ErrNoActorsAvailable`.
|
Reference in New Issue
Block a user