mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-09-27 19:46:22 +02:00
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:
97
actor/example_basic_actor_test.go
Normal file
97
actor/example_basic_actor_test.go
Normal 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
|
||||
}
|
112
actor/example_router_test.go
Normal file
112
actor/example_router_test.go
Normal 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
|
||||
}
|
147
actor/example_struct_actor_test.go
Normal file
147
actor/example_struct_actor_test.go
Normal 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)
|
||||
}
|
133
actor/example_tell_only_test.go
Normal file
133
actor/example_tell_only_test.go
Normal 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.
|
||||
}
|
Reference in New Issue
Block a user