actor: add example files

In this commit, we add a series of examples that show how the package
can be used in the wild. They can be run as normal Example tests.
This commit is contained in:
Olaoluwa Osuntokun
2025-05-14 18:58:25 -07:00
parent 3bf47ed695
commit 6184786caa
4 changed files with 489 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
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
}
// ExampleBasicActor demonstrates creating a single actor, sending it a message
// directly using Ask, and then unregistering and stopping it.
func ExampleBasicActor() {
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 := greeterKey.Spawn(system, actorID, greeterBehavior)
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
}

View File

@@ -0,0 +1,112 @@
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,
})
},
)
routerGreeterKey.Spawn(system, actorID1, greeterBehavior1)
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,
})
},
)
routerGreeterKey.Spawn(system, actorID2, greeterBehavior2)
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
}

View File

@@ -0,0 +1,147 @@
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"))
}
// ExampleStructActor demonstrates creating an actor whose behavior is defined
// by a struct with methods, allowing it to maintain internal state.
func ExampleStructActor() {
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 := counterServiceKey.Spawn(system, actorID, counterLogic)
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)
}

View File

@@ -0,0 +1,133 @@
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 := loggerServiceKey.Spawn(system, actorID, loggerLogic)
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.
}