What changed, and why it matters
This commit only adds four example test files for the new 'actor' package. They are documentation-style code examples showing how to create actors, send messages, use routers, and use fire-and-forget messaging. No production code was changed, and nothing in the diff introduces or fixes a security issue.
No security action needed. Treat as routine documentation/test addition.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit adds example Go test files under actor/: example_basic_actor_test.go, example_router_test.go, example_struct_actor_test.go, and example_tell_only_test.go. All are package actor_test examples using the existing actor package API. They demonstrate Spawn, Ask, Tell, Router, ServiceKey, and Unregister patterns. There are no changes to the actor package implementation, no new dependencies, no cryptographic operations, no network listeners, no file I/O beyond test output, and no privilege changes.
Changed components
actor/example_basic_actor_test.goactor/example_router_test.goactor/example_struct_actor_test.goactor/example_tell_only_test.goInspect captured patch +511 / −0
diff --git a/actor/example_basic_actor_test.go b/actor/example_basic_actor_test.go
new file mode 100644
index 0000000..960602a
--- /dev/null
+++ b/actor/example_basic_actor_test.go
@@ -0,0 +1,101 @@
+package actor_test
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/lightningnetwork/lnd/actor"
+ "github.com/lightningnetwork/lnd/fn/v2"
+)
+
+// BasicGreetingMsg is a simple message type for the basic actor example.
+type BasicGreetingMsg struct {
+ actor.BaseMessage
+ Name string
+}
+
+// MessageType implements actor.Message.
+func (m BasicGreetingMsg) MessageType() string { return "BasicGreetingMsg" }
+
+// BasicGreetingResponse is a simple response type.
+type BasicGreetingResponse struct {
+ Greeting string
+}
+
+// ExampleActor demonstrates creating a single actor, sending it a message
+// directly using Ask, and then unregistering and stopping it.
+func ExampleActor() {
+ system := actor.NewActorSystem()
+ defer system.Shutdown()
+
+ //nolint:ll
+ greeterKey := actor.NewServiceKey[BasicGreetingMsg, BasicGreetingResponse](
+ "basic-greeter",
+ )
+
+ actorID := "my-greeter"
+ greeterBehavior := actor.NewFunctionBehavior(
+ func(ctx context.Context,
+ msg BasicGreetingMsg) fn.Result[BasicGreetingResponse] {
+
+ return fn.Ok(BasicGreetingResponse{
+ Greeting: "Hello, " + msg.Name + " from " +
+ actorID,
+ })
+ },
+ )
+
+ // Spawn the actor. This registers it with the system and receptionist,
+ // and starts it. It returns an ActorRef.
+ greeterRef, err := greeterKey.Spawn(system, actorID, greeterBehavior)
+ if err != nil {
+ fmt.Printf("Failed to spawn actor: %v\n", err)
+ return
+ }
+ fmt.Printf("Actor %s spawned.\n", greeterRef.ID())
+
+ // Send a message directly to the actor's reference.
+ askCtx, askCancel := context.WithTimeout(
+ context.Background(), 1*time.Second,
+ )
+ defer askCancel()
+ futureResponse := greeterRef.Ask(
+ askCtx, BasicGreetingMsg{Name: "World"},
+ )
+
+ awaitCtx, awaitCancel := context.WithTimeout(
+ context.Background(), 1*time.Second,
+ )
+ defer awaitCancel()
+ result := futureResponse.Await(awaitCtx)
+
+ result.WhenErr(func(err error) {
+ fmt.Printf("Error awaiting response: %v\n", err)
+ })
+ result.WhenOk(func(response BasicGreetingResponse) {
+ fmt.Printf("Received: %s\n", response.Greeting)
+ })
+
+ // Unregister the actor. This also stops the actor.
+ unregistered := greeterKey.Unregister(system, greeterRef)
+ if unregistered {
+ fmt.Printf("Actor %s unregistered and stopped.\n",
+ greeterRef.ID())
+ } else {
+ fmt.Printf("Failed to unregister actor %s.\n", greeterRef.ID())
+ }
+
+ // Verify it's no longer in the receptionist.
+ refsAfterUnregister := actor.FindInReceptionist(
+ system.Receptionist(), greeterKey,
+ )
+ fmt.Printf("Actors for key '%s' after unregister: %d\n",
+ "basic-greeter", len(refsAfterUnregister))
+
+ // Output:
+ // Actor my-greeter spawned.
+ // Received: Hello, World from my-greeter
+ // Actor my-greeter unregistered and stopped.
+ // Actors for key 'basic-greeter' after unregister: 0
+}
diff --git a/actor/example_router_test.go b/actor/example_router_test.go
new file mode 100644
index 0000000..f68cff6
--- /dev/null
+++ b/actor/example_router_test.go
@@ -0,0 +1,120 @@
+package actor_test
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/lightningnetwork/lnd/actor"
+ "github.com/lightningnetwork/lnd/fn/v2"
+)
+
+// RouterGreetingMsg is a message type for the router example.
+type RouterGreetingMsg struct {
+ actor.BaseMessage
+ Name string
+}
+
+// MessageType implements actor.Message.
+func (m RouterGreetingMsg) MessageType() string { return "RouterGreetingMsg" }
+
+// RouterGreetingResponse is a response type for the router example.
+type RouterGreetingResponse struct {
+ Greeting string
+ HandlerID string
+}
+
+// ExampleRouter demonstrates creating multiple actors under the same service
+// key and using a router to dispatch messages to them.
+func ExampleRouter() {
+ system := actor.NewActorSystem()
+ defer system.Shutdown()
+
+ //nolint:ll
+ routerGreeterKey := actor.NewServiceKey[RouterGreetingMsg, RouterGreetingResponse](
+ "router-greeter-service",
+ )
+
+ // Behavior for the first greeter actor.
+ actorID1 := "router-greeter-1"
+ greeterBehavior1 := actor.NewFunctionBehavior(
+ func(ctx context.Context,
+ msg RouterGreetingMsg) fn.Result[RouterGreetingResponse] {
+
+ return fn.Ok(RouterGreetingResponse{
+ Greeting: "Greetings, " + msg.Name + "!",
+ HandlerID: actorID1,
+ })
+ },
+ )
+ _, err := routerGreeterKey.Spawn(system, actorID1, greeterBehavior1)
+ if err != nil {
+ fmt.Printf("Failed to spawn actor: %v\n", err)
+ return
+ }
+ fmt.Printf("Actor %s spawned.\n", actorID1)
+
+ // Behavior for the second greeter actor.
+ actorID2 := "router-greeter-2"
+ greeterBehavior2 := actor.NewFunctionBehavior(
+ func(ctx context.Context,
+ msg RouterGreetingMsg) fn.Result[RouterGreetingResponse] {
+
+ return fn.Ok(RouterGreetingResponse{
+ Greeting: "Salutations, " + msg.Name + "!",
+ HandlerID: actorID2,
+ })
+ },
+ )
+ _, err = routerGreeterKey.Spawn(system, actorID2, greeterBehavior2)
+ if err != nil {
+ fmt.Printf("Failed to spawn actor: %v\n", err)
+ return
+ }
+ fmt.Printf("Actor %s spawned.\n", actorID2)
+
+ // Create a router for the "router-greeter-service".
+ greeterRouter := actor.NewRouter(
+ system.Receptionist(), routerGreeterKey,
+ actor.NewRoundRobinStrategy[RouterGreetingMsg,
+ RouterGreetingResponse](),
+ system.DeadLetters(),
+ )
+ fmt.Printf("Router %s created for service key '%s'.\n",
+ greeterRouter.ID(), "router-greeter-service")
+
+ // Send messages through the router.
+ names := []string{"Alice", "Bob", "Charlie", "David"}
+ for _, name := range names {
+ askCtx, askCancel := context.WithTimeout(
+ context.Background(), 1*time.Second,
+ )
+ futureResponse := greeterRouter.Ask(
+ askCtx, RouterGreetingMsg{Name: name},
+ )
+
+ awaitCtx, awaitCancel := context.WithTimeout(
+ context.Background(), 1*time.Second,
+ )
+ result := futureResponse.Await(awaitCtx)
+
+ result.WhenErr(func(err error) {
+ fmt.Printf("For %s: Error - %v\n", name, err)
+ })
+ result.WhenOk(func(response RouterGreetingResponse) {
+ fmt.Printf("For %s: Received '%s' from %s\n",
+ name, response.Greeting, response.HandlerID)
+ })
+ awaitCancel()
+ askCancel()
+ }
+
+ // Output:
+ // Actor router-greeter-1 spawned.
+ // Actor router-greeter-2 spawned.
+ // Router router(router-greeter-service) created for service key 'router-greeter-service'.
+ // For Alice: Received 'Greetings, Alice!' from router-greeter-1
+ // For Bob: Received 'Salutations, Bob!' from router-greeter-2
+ // For Charlie: Received 'Greetings, Charlie!' from router-greeter-1
+ // For David: Received 'Salutations, David!' from router-greeter-2
+}
diff --git a/actor/example_struct_actor_test.go b/actor/example_struct_actor_test.go
new file mode 100644
index 0000000..e92e1e1
--- /dev/null
+++ b/actor/example_struct_actor_test.go
@@ -0,0 +1,153 @@
+package actor_test
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/lightningnetwork/lnd/actor"
+ "github.com/lightningnetwork/lnd/fn/v2"
+)
+
+// CounterMsg is a message type for the stateful counter actor.
+// It can be used to increment the counter or get its current value.
+type CounterMsg struct {
+ actor.BaseMessage
+ Increment int
+ GetValue bool
+ Who string
+}
+
+// MessageType implements actor.Message.
+func (m CounterMsg) MessageType() string { return "CounterMsg" }
+
+// CounterResponse is a response type for the counter actor.
+type CounterResponse struct {
+ Value int
+ Responder string
+}
+
+// StatefulCounterActor demonstrates an actor that maintains internal state (a
+// counter) and processes messages to modify or query that state.
+type StatefulCounterActor struct {
+ counter int
+ actorID string
+}
+
+// NewStatefulCounterActor creates a new counter actor.
+func NewStatefulCounterActor(id string) *StatefulCounterActor {
+ return &StatefulCounterActor{
+ actorID: id,
+ }
+}
+
+// Receive is the message handler for the StatefulCounterActor.
+// It implements the actor.ActorBehavior interface implicitly when wrapped.
+func (s *StatefulCounterActor) Receive(ctx context.Context,
+ msg CounterMsg) fn.Result[CounterResponse] {
+
+ if msg.Increment > 0 {
+ // For increment, we can just acknowledge or return the new
+ // value. Messages are sent serially, so we don't need to worry
+ // about a mutex here.
+ s.counter += msg.Increment
+
+ return fn.Ok(CounterResponse{
+ Value: s.counter,
+ Responder: s.actorID,
+ })
+ }
+
+ if msg.GetValue {
+ return fn.Ok(CounterResponse{
+ Value: s.counter,
+ Responder: s.actorID,
+ })
+ }
+
+ return fn.Err[CounterResponse](fmt.Errorf("invalid CounterMsg"))
+}
+
+// ExampleActor_stateful demonstrates creating an actor whose behavior is defined
+// by a struct with methods, allowing it to maintain internal state.
+func ExampleActor_stateful() {
+ system := actor.NewActorSystem()
+ defer system.Shutdown()
+
+ counterServiceKey := actor.NewServiceKey[CounterMsg, CounterResponse](
+ "struct-counter-service",
+ )
+
+ // Create an instance of our stateful actor logic.
+ actorID := "counter-actor-1"
+ counterLogic := NewStatefulCounterActor(actorID)
+
+ // Spawn the actor.
+ // The counterLogic instance itself satisfies the ActorBehavior
+ // interface because its Receive method matches the required signature.
+ counterRef, err := counterServiceKey.Spawn(
+ system, actorID, counterLogic,
+ )
+ if err != nil {
+ fmt.Printf("Failed to spawn actor: %v\n", err)
+ return
+ }
+ fmt.Printf("Actor %s spawned.\n", counterRef.ID())
+
+ // Send messages to increment the counter.
+ for i := 1; i <= 3; i++ {
+ askCtx, askCancel := context.WithTimeout(
+ context.Background(), 1*time.Second,
+ )
+ futureResp := counterRef.Ask(askCtx,
+ CounterMsg{
+ Increment: i,
+ Who: fmt.Sprintf("Incrementer-%d", i),
+ },
+ )
+ awaitCtx, awaitCancel := context.WithTimeout(
+ context.Background(), 1*time.Second,
+ )
+ resp := futureResp.Await(awaitCtx)
+
+ resp.WhenOk(func(r CounterResponse) {
+ fmt.Printf("Incremented by %d, new value: %d "+
+ "(from %s)\n", i, r.Value, r.Responder)
+ })
+ resp.WhenErr(func(e error) {
+ fmt.Printf("Error incrementing: %v\n", e)
+ })
+ awaitCancel()
+ askCancel()
+ }
+
+ // Send a message to get the current value.
+ askCtx, askCancel := context.WithTimeout(
+ context.Background(), 1*time.Second,
+ )
+ futureResp := counterRef.Ask(
+ askCtx, CounterMsg{GetValue: true, Who: "Getter"},
+ )
+
+ awaitCtx, awaitCancel := context.WithTimeout(
+ context.Background(), 1*time.Second,
+ )
+
+ finalValueResp := futureResp.Await(awaitCtx)
+ finalValueResp.WhenOk(func(r CounterResponse) {
+ fmt.Printf("Final counter value: %d (from %s)\n",
+ r.Value, r.Responder)
+ })
+ finalValueResp.WhenErr(func(e error) {
+ fmt.Printf("Error getting value: %v\n", e)
+ })
+ awaitCancel()
+ askCancel()
+
+ // Output:
+ // Actor counter-actor-1 spawned.
+ // Incremented by 1, new value: 1 (from counter-actor-1)
+ // Incremented by 2, new value: 3 (from counter-actor-1)
+ // Incremented by 3, new value: 6 (from counter-actor-1)
+ // Final counter value: 6 (from counter-actor-1)
+}
diff --git a/actor/example_tell_only_test.go b/actor/example_tell_only_test.go
new file mode 100644
index 0000000..a637131
--- /dev/null
+++ b/actor/example_tell_only_test.go
@@ -0,0 +1,137 @@
+package actor_test
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/lightningnetwork/lnd/actor"
+ "github.com/lightningnetwork/lnd/fn/v2"
+)
+
+// LogMsg is a message type for the TellOnly example.
+type LogMsg struct {
+ actor.BaseMessage
+ Text string
+}
+
+// MessageType implements actor.Message.
+func (m LogMsg) MessageType() string { return "LogMsg" }
+
+// LoggerActorBehavior is a simple actor behavior that logs messages. It doesn't
+// produce a meaningful response for Ask, so it's a good candidate for TellOnly
+// interactions.
+type LoggerActorBehavior struct {
+ mu sync.Mutex
+ logs []string
+ actorID string
+}
+
+func NewLoggerActorBehavior(id string) *LoggerActorBehavior {
+ return &LoggerActorBehavior{actorID: id}
+}
+
+// Receive processes LogMsg messages by appending them to an internal log. The
+// response type is 'any' as it's not typically used with Ask.
+func (l *LoggerActorBehavior) Receive(ctx context.Context,
+ msg actor.Message) fn.Result[any] {
+
+ logMessage, ok := msg.(LogMsg)
+ if !ok {
+ return fn.Err[any](fmt.Errorf("unexpected message "+
+ "type: %s", msg.MessageType()))
+ }
+
+ l.mu.Lock()
+ defer l.mu.Unlock()
+
+ entry := fmt.Sprintf("[%s from %s]: %s", time.Now().Format("15:04:05"),
+ l.actorID, logMessage.Text)
+ l.logs = append(l.logs, entry)
+
+ // For Tell, the result is often ignored, but we must return something.
+ return fn.Ok[any](nil)
+}
+
+func (l *LoggerActorBehavior) GetLogs() []string {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+
+ copiedLogs := make([]string, len(l.logs))
+ copy(copiedLogs, l.logs)
+
+ return copiedLogs
+}
+
+// ExampleTellOnlyRef demonstrates using a TellOnlyRef for fire-and-forget
+// messaging with an actor.
+func ExampleTellOnlyRef() {
+ system := actor.NewActorSystem()
+ defer system.Shutdown()
+
+ // The logger actor doesn't really have a response type for Ask, so we
+ // use 'any'.
+ loggerServiceKey := actor.NewServiceKey[actor.Message, any](
+ "tell-only-logger-service",
+ )
+
+ actorID := "my-logger"
+ loggerLogic := NewLoggerActorBehavior(actorID)
+
+ // Spawn the actor.
+ fullRef, err := loggerServiceKey.Spawn(system, actorID, loggerLogic)
+ if err != nil {
+ fmt.Printf("Failed to spawn actor: %v\n", err)
+ return
+ }
+ fmt.Printf("Actor %s spawned.\n", fullRef.ID())
+
+ // Get a TellOnlyRef for the actor. We can get this from the Actor
+ // instance itself if we had it, or by type assertion if we know the
+ // underlying ref supports it. Since fullRef is ActorRef[actor.Message,
+ // any], it already satisfies TellOnlyRef[actor.Message].
+ //
+ // Or, if we had the *Actor instance: tellOnlyLogger =
+ // actorInstance.TellRef()
+ var tellOnlyLogger actor.TellOnlyRef[actor.Message] = fullRef
+
+ fmt.Printf("Obtained TellOnlyRef for %s.\n", tellOnlyLogger.ID())
+
+ // Send messages using Tell.
+ tellOnlyLogger.Tell(
+ context.Background(), LogMsg{Text: "First log entry."},
+ )
+ tellOnlyLogger.Tell(
+ context.Background(), LogMsg{Text: "Second log entry."},
+ )
+
+ // Allow some time for messages to be processed.
+ time.Sleep(10 * time.Millisecond)
+
+ // Retrieve logs directly from the behavior for verification in this
+ // example. In a real scenario, this might not be possible or desired.
+ logs := loggerLogic.GetLogs()
+ fmt.Println("Logged entries:")
+ for _, entry := range logs {
+ // Strip the timestamp and actor ID for consistent example
+ // output. Example entry: "[15:04:05 from my-logger]: Actual log
+ // text"
+ parts := strings.SplitN(entry, "]: ", 2)
+ if len(parts) == 2 {
+ fmt.Println(parts[1])
+ }
+ }
+
+ // Attempting to Ask using tellOnlyLogger would be a compile-time error:
+ // tellOnlyLogger.Ask(context.Background(), LogMsg{Text: "This would
+ // fail"})
+
+ // Output:
+ // Actor my-logger spawned.
+ // Obtained TellOnlyRef for my-logger.
+ // Logged entries:
+ // First log entry.
+ // Second log entry.
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.