diff --git a/baselib/PROTOFSM_ACTOR_GUIDE.md b/baselib/PROTOFSM_ACTOR_GUIDE.md new file mode 100644 index 000000000..90181b734 --- /dev/null +++ b/baselib/PROTOFSM_ACTOR_GUIDE.md @@ -0,0 +1,870 @@ +# Actor-Based State Machine (protofsm + actor) Usage Guide + +This guide explains how to build restart-safe, event-driven workflows using the `protofsm` and `actor` packages together. These patterns were refined through refactoring three production swap state machines. + +## Table of Contents + +1. [Core Concepts](#core-concepts) +2. [Quick Start](#quick-start) +3. [FSM Design Patterns](#fsm-design-patterns) +4. [Actor Integration Patterns](#actor-integration-patterns) +5. [Restart Safety](#restart-safety) +6. [Common Patterns](#common-patterns) +7. [Testing](#testing) +8. [Troubleshooting](#troubleshooting) + +--- + +## Core Concepts + +### The protofsm Package + +**protofsm** provides a type-safe, event-driven finite state machine: + +- **State**: Processes events and returns state transitions +- **Event**: Triggers state transitions (internal to FSM) +- **OutboxEvent**: Emitted to external actors for side effects +- **Environment**: Provides dependencies to state processors +- **StateMachine**: Event queue processor and orchestrator + +### The actor Package + +**actor** provides message-passing concurrency: + +- **Actor**: Goroutine with mailbox, processes messages sequentially +- **ActorRef**: Reference for sending messages (Tell/Ask) +- **ActorSystem**: Manages actor lifecycle and service registry +- **ServiceKey**: Typed key for actor lookup by service name + +### Integration: ActorStateMachine + +The `protofsm.ActorStateMachine` wraps a `StateMachine` as an `ActorBehavior`, enabling: +- FSM runs inside an actor (one FSM per actor) +- Outbox events automatically dispatch to other actors via service keys +- Multiple FSM actors can coexist in the same ActorSystem + +--- + +## Quick Start + +### Step 1: Define Your Events + +Use sealed interfaces to define all possible events: + +```go +// Event is the sealed interface for all FSM events. +type Event interface { + isEventSealed() +} + +// EventStart begins the workflow. +type EventStart struct { + ID string +} + +func (EventStart) isEventSealed() {} + +// EventComplete finishes the workflow. +type EventComplete struct { + Result string +} + +func (EventComplete) isEventSealed() {} + +// EventResume is sent when resuming from storage. +type EventResume struct{} + +func (EventResume) isEventSealed() {} +``` + +**Why sealed?** Type safety - only your package can define events. + +### Step 2: Define Your Outbox Events + +Outbox events are routed to actors for side effects: + +```go +// OutboxEvent is the sealed interface for outbox events. +type OutboxEvent interface { + protofsm.ActorOutboxEvent + isOutboxEventSealed() +} + +// OutboxPersist requests persistence. +type OutboxPersist struct { + protofsm.RoutedOutboxEvent[StorePersistMsg, StorePersistResp] +} + +func NewOutboxPersist(id string, data interface{}) OutboxPersist { + return OutboxPersist{ + RoutedOutboxEvent: protofsm.NewAskOutboxEvent( + StoreServiceKey, + StorePersistMsg{ID: id, Data: data}, + ), + } +} + +func (OutboxPersist) isOutboxEventSealed() {} +``` + +**Key point:** Use `RoutedOutboxEvent` to dispatch to actors via service keys. + +### Step 3: Define Your States + +Each state implements the `State` interface: + +```go +// State is the sealed interface for all FSM states. +type State interface { + protofsm.State[Event, OutboxEvent, *Environment] + isStateSealed() +} + +// StateInit is the initial state. +type StateInit struct{} + +func (StateInit) isStateSealed() {} +func (StateInit) IsTerminal() bool { return false } +func (StateInit) String() string { return "Init" } + +func (s *StateInit) ProcessEvent(ctx context.Context, event Event, + env *Environment) (*protofsm.StateTransition[Event, OutboxEvent, *Environment], error) { + + switch e := event.(type) { + case EventStart: + nextState := &StateProcessing{id: e.ID} + + return &protofsm.StateTransition[Event, OutboxEvent, *Environment]{ + NextState: nextState, + NewEvents: fn.Some(protofsm.EmittedEvent[Event, OutboxEvent]{ + Outbox: []OutboxEvent{ + NewOutboxPersist(e.ID, nextState), + }, + }), + }, nil + + default: + return nil, fmt.Errorf("unexpected event %T in StateInit", e) + } +} +``` + +**Pattern:** Pattern-match on event type, create next state, emit outbox events. + +### Step 4: Define Your Environment + +Environment provides dependencies and receives ActorRef injection: + +```go +// Environment holds FSM execution context. +type Environment struct { + store Store + actorRef actor.TellOnlyRef[protofsm.ActorMessage[Event]] +} + +func NewEnvironment(store Store) *Environment { + return &Environment{store: store} +} + +// SetTellOnlyRef is called by protofsm to inject the FSM's actor reference. +func (e *Environment) SetTellOnlyRef(ref actor.TellOnlyRef[protofsm.ActorMessage[Event]]) { + e.actorRef = ref +} + +func (e *Environment) GetTellOnlyRef() actor.TellOnlyRef[protofsm.ActorMessage[Event]] { + return e.actorRef +} + +// Compile-time check. +var _ protofsm.TellRefEnv[Event] = (*Environment)(nil) +``` + +**Why?** Actors need to send events back to the FSM (e.g., "operation completed"). + +### Step 5: Create Actors for Side Effects + +Actors handle external operations (storage, API calls, monitoring): + +```go +const storeActorName = "store" + +var StoreServiceKey = actor.NewServiceKey[StorePersistMsg, StorePersistResp](storeActorName) + +type StorePersistMsg struct { + actor.BaseMessage + ID string + Data interface{} +} + +func (m StorePersistMsg) MessageType() string { return "StorePersist" } + +type StorePersistResp struct { + Success bool +} + +type StoreActorBehavior struct { + store Store +} + +func (s *StoreActorBehavior) Receive(ctx context.Context, + msg StorePersistMsg) fn.Result[StorePersistResp] { + + // Perform persistence. + if err := s.store.Save(msg.ID, msg.Data); err != nil { + return fn.Err[StorePersistResp](err) + } + + return fn.Ok(StorePersistResp{Success: true}) +} +``` + +**Pattern:** Actor receives message, performs side effect, returns result. + +### Step 6: Wire It All Together + +Create ActorSystem, register actors, spawn FSM actors: + +```go +func main() { + ctx := context.Background() + + // Create actor system. + system := actor.NewActorSystemWithConfig(actor.SystemConfig{ + MailboxCapacity: 100, + }) + defer system.Shutdown() + + // Register shared actors. + storeActor := &StoreActorBehavior{store: NewStore()} + actor.RegisterWithSystem(system, storeActorName, StoreServiceKey, storeActor) + + // Create FSM configuration. + env := NewEnvironment(store) + cfg := protofsm.StateMachineCfg[Event, OutboxEvent, *Environment]{ + Logger: logger, + InitialState: &StateInit{}, + Env: env, + } + + // Spawn FSM as actor. + fsmRef := protofsm.NewSystemsActorStateMachine( + ctx, cfg, system, "workflow-123", + ) + + // Send initial event. + fsmRef.Tell(ctx, protofsm.ActorMessage[Event]{ + Event: EventStart{ID: "workflow-123"}, + }) +} +``` + +--- + +## FSM Design Patterns + +### Pattern 1: Sealed Interfaces + +Always use sealed interfaces for Events, States, and OutboxEvents: + +```go +// Event is sealed via unexported method. +type Event interface { + isEventSealed() +} + +// Only types in this package can implement isEventSealed(). +``` + +**Why?** Type safety - compiler catches invalid event types. + +### Pattern 2: State Holds Data, Not Behavior + +States are data containers with transition logic: + +```go +// GOOD: State holds necessary data for transitions. +type StateProcessing struct { + id string + startTime time.Time + retries int +} + +// AVOID: State with channels, goroutines, or mutable shared state. +type StateBad struct { + id string + resultCh chan Result // ❌ Don't do this + mu sync.Mutex // ❌ States are immutable +} +``` + +**Why?** States must be serializable for restart safety. + +### Pattern 3: Emit Outbox Events for Side Effects + +Never perform side effects directly in ProcessEvent: + +```go +// GOOD: Emit outbox event for persistence. +func (s *StateInit) ProcessEvent(ctx context.Context, event Event, + env *Environment) (*protofsm.StateTransition[Event, OutboxEvent, *Environment], error) { + + switch e := event.(type) { + case EventStart: + nextState := &StateProcessing{id: e.ID} + + return &protofsm.StateTransition[Event, OutboxEvent, *Environment]{ + NextState: nextState, + NewEvents: fn.Some(protofsm.EmittedEvent[Event, OutboxEvent]{ + Outbox: []OutboxEvent{ + NewOutboxPersist(e.ID, nextState), // ✅ Emit event + }, + }), + }, nil + } +} + +// AVOID: Direct side effects. +func (s *StateBad) ProcessEvent(ctx context.Context, event Event, + env *Environment) (*protofsm.StateTransition[Event, OutboxEvent, *Environment], error) { + + env.store.Save("key", "value") // ❌ Don't call external systems directly + + return &protofsm.StateTransition[...]{...}, nil +} +``` + +**Why?** Keeps FSM pure, testable, and allows outbox events to be dispatched asynchronously. + +### Pattern 4: EventResume for Restart Safety + +Every non-terminal state should handle EventResume: + +```go +func (s *StateProcessing) ProcessEvent(ctx context.Context, event Event, + env *Environment) (*protofsm.StateTransition[Event, OutboxEvent, *Environment], error) { + + switch e := event.(type) { + case EventResume: + // Re-emit pending outbox events to re-establish monitoring. + return &protofsm.StateTransition[Event, OutboxEvent, *Environment]{ + NextState: s, // Stay in same state + NewEvents: fn.Some(protofsm.EmittedEvent[Event, OutboxEvent]{ + Outbox: []OutboxEvent{ + NewOutboxMonitor(s.id, env.actorRef), + }, + }), + }, nil + + // ... other events + } +} +``` + +**Why?** When resuming from storage, pending operations must be re-established. + +--- + +## Actor Integration Patterns + +### Pattern 1: Actor Sends Events Back to FSM + +Actors receive a `TellOnlyRef` to send result events back: + +```go +type MonitorMsg struct { + actor.BaseMessage + ID string + ActorTellRef actor.TellOnlyRef[protofsm.ActorMessage[Event]] +} + +func (b *MonitorBehavior) Receive(ctx context.Context, + msg MonitorMsg) fn.Result[MonitorResp] { + + go func() { + // Perform background monitoring. + result := b.pollUntilComplete(ctx, msg.ID) + + // Send result event back to FSM. + msg.ActorTellRef.Tell(ctx, protofsm.ActorMessage[Event]{ + Event: EventComplete{Result: result}, + }) + }() + + return fn.Ok(MonitorResp{Success: true}) +} +``` + +**Pattern:** Actor starts background work, sends event when done. + +### Pattern 2: Tell vs Ask for Outbox Events + +Use `Tell` for fire-and-forget, `Ask` for request-response: + +```go +// Tell: Fire-and-forget (monitoring, notifications). +func NewOutboxMonitor(id string) OutboxMonitor { + return OutboxMonitor{ + RoutedOutboxEvent: protofsm.NewTellOutboxEvent( + MonitorServiceKey, + MonitorMsg{ID: id}, + ), + } +} + +// Ask: Wait for response (persistence, validation). +func NewOutboxPersist(id string) OutboxPersist { + return OutboxPersist{ + RoutedOutboxEvent: protofsm.NewAskOutboxEvent( + StoreServiceKey, + StorePersistMsg{ID: id}, + ), + } +} +``` + +**Guideline:** Use Ask for critical operations (persistence), Tell for non-blocking operations (monitoring). + +### Pattern 3: Service Keys for Routing + +Service keys enable location transparency: + +```go +// Define service key (global/package-level). +var StoreServiceKey = actor.NewServiceKey[StorePersistMsg, StorePersistResp]("store") + +// Register actor. +actor.RegisterWithSystem(system, "store", StoreServiceKey, storeBehavior) + +// Emit outbox event (routed automatically via service key). +NewOutboxPersist(id, data) // Uses StoreServiceKey internally +``` + +**Why?** FSM doesn't need ActorRef - service key handles routing. + +--- + +## Restart Safety + +### Key Principle: Idempotent Resumption + +When a process crashes and restarts: +1. Load persisted state from storage +2. Spawn FSM actor with loaded state as `InitialState` +3. Send `EventResume` to re-establish pending operations + +### Pattern: Dual Persistence + +Persist both state name AND state data: + +```go +type StoredWorkflow struct { + ID string + State State // The actual state object + Data interface{} // State-specific data + CreatedAt time.Time + UpdatedAt time.Time +} + +// When loading from DB, reconstruct state from name + data. +func ReconstructStateFromName(stateName string, stored *StoredWorkflow) (State, error) { + switch stateName { + case "Processing": + return &StateProcessing{ + id: stored.ID, + startTime: stored.Data.(time.Time), + }, nil + + // ... other states + } +} +``` + +### Pattern: EventResume Re-emits Outbox Events + +```go +case EventResume: + // Re-emit pending operations. + return &protofsm.StateTransition[Event, OutboxEvent, *Environment]{ + NextState: s, // Stay in same state + NewEvents: fn.Some(protofsm.EmittedEvent[Event, OutboxEvent]{ + Outbox: []OutboxEvent{ + // Re-emit monitoring (idempotent). + NewOutboxMonitor(s.id, env.actorRef), + }, + }), + }, nil +``` + +**Critical:** Actors must be idempotent - receiving the same message twice is safe. + +### Pattern: Delta Updates for Persistence + +Only persist changed fields: + +```go +type WorkflowUpdates struct { + StartTime *time.Time // Only set if changed + Retries *int // Only set if changed + State State // Always set to reflect current state +} + +// StoreActor applies deltas. +func (s *StoreActor) Receive(ctx context.Context, msg StorePersistMsg) fn.Result[StorePersistResp] { + existing, _ := s.store.Get(msg.ID) + + // Apply only non-nil fields. + if msg.Updates.StartTime != nil { + existing.StartTime = *msg.Updates.StartTime + } + if msg.Updates.Retries != nil { + existing.Retries = *msg.Updates.Retries + } + if msg.Updates.State != nil { + existing.State = msg.Updates.State + } + + s.store.Save(existing) + return fn.Ok(StorePersistResp{Success: true}) +} +``` + +--- + +## Common Patterns + +### Pattern: Background Monitoring Actor + +Actors can spawn goroutines for long-running operations: + +```go +func (b *MonitorBehavior) Receive(ctx context.Context, + msg MonitorMsg) fn.Result[MonitorResp] { + + // Return immediately (Tell pattern). + go b.monitorInBackground(ctx, msg) + return fn.Ok(MonitorResp{Success: true}) +} + +func (b *MonitorBehavior) monitorInBackground(ctx context.Context, msg MonitorMsg) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if b.checkComplete(msg.ID) { + // Send completion event to FSM. + msg.ActorTellRef.Tell(ctx, protofsm.ActorMessage[Event]{ + Event: EventComplete{}, + }) + return + } + } + } +} +``` + +**Use case:** Polling external systems, waiting for blockchain events, timeouts. + +### Pattern: Manager Spawns FSM Actors + +One manager owns the ActorSystem and spawns FSM actors: + +```go +type Manager struct { + system *actor.ActorSystem + fsmActors map[string]actor.ActorRef[protofsm.ActorMessage[Event], ...] + mu sync.RWMutex +} + +func (m *Manager) StartWorkflow(ctx context.Context, id string) error { + // Spawn FSM actor. + env := NewEnvironment(m.store) + cfg := protofsm.StateMachineCfg[Event, OutboxEvent, *Environment]{ + Logger: m.logger.WithPrefix(id), + InitialState: &StateInit{}, + Env: env, + } + + fsmRef := protofsm.NewSystemsActorStateMachine(ctx, cfg, m.system, id) + + m.mu.Lock() + m.fsmActors[id] = fsmRef + m.mu.Unlock() + + // Send initial event. + fsmRef.Tell(ctx, protofsm.ActorMessage[Event]{ + Event: EventStart{ID: id}, + }) + + return nil +} + +func (m *Manager) ResumeWorkflows(ctx context.Context) error { + workflows, _ := m.store.ListPending() + + for _, wf := range workflows { + // Spawn FSM actor with persisted state. + cfg := protofsm.StateMachineCfg[Event, OutboxEvent, *Environment]{ + Logger: m.logger.WithPrefix(wf.ID), + InitialState: wf.State, // Reconstructed from storage + Env: NewEnvironment(m.store), + } + + fsmRef := protofsm.NewSystemsActorStateMachine(ctx, cfg, m.system, wf.ID) + + m.mu.Lock() + m.fsmActors[wf.ID] = fsmRef + m.mu.Unlock() + + // Re-establish pending operations. + fsmRef.Tell(ctx, protofsm.ActorMessage[Event]{ + Event: EventResume{}, + }) + } + + return nil +} +``` + +**Pattern:** Manager = one ActorSystem, multiple FSM actors (one per workflow instance). + +### Pattern: Cleanup on Terminal States + +Remove FSM actors when workflow completes: + +```go +type Environment struct { + store Store + actorRef actor.TellOnlyRef[protofsm.ActorMessage[Event]] + cleanupFunc func(ctx context.Context, id string) +} + +// In terminal states, call cleanup. +func (s *StateCompleted) ProcessEvent(ctx context.Context, event Event, + env *Environment) (*protofsm.StateTransition[Event, OutboxEvent, *Environment], error) { + + // Cleanup FSM actor from manager registry. + if env.cleanupFunc != nil { + env.cleanupFunc(ctx, s.id) + } + + return nil, fmt.Errorf("no events expected in terminal state") +} +``` + +### Pattern: Dual Dispatch for Blocking Operations + +When you have both async actors and blocking operations (e.g., HTLC interception): + +```go +// Actor sends to BOTH: +// 1. Blocking channel (for waiting goroutine) +// 2. FSM event (for state machine completion) + +func (h *HTLCInterceptor) Receive(ctx context.Context, + msg SettleMsg) fn.Result[SettleResp] { + + // Send preimage to blocking channel. + h.claimChans[msg.PaymentHash] <- msg.Preimage + + // Send event to FSM. + msg.ActorTellRef.Tell(ctx, protofsm.ActorMessage[Event]{ + Event: EventHTLCSettled{}, + }) + + return fn.Ok(SettleResp{Success: true}) +} +``` + +--- + +## Testing + +### Unit Testing States + +Test state transitions in isolation: + +```go +func TestStateInit_EventStart(t *testing.T) { + state := &StateInit{} + env := &Environment{} + + transition, err := state.ProcessEvent( + context.Background(), + EventStart{ID: "test-123"}, + env, + ) + + require.NoError(t, err) + require.Equal(t, "Processing", transition.NextState.String()) + require.Len(t, transition.NewEvents.UnwrapOr(...).Outbox, 1) +} +``` + +### Integration Testing with Actors + +Test the full actor + FSM integration: + +```go +func TestWorkflowIntegration(t *testing.T) { + ctx := context.Background() + system := actor.NewActorSystemWithConfig(actor.SystemConfig{}) + defer system.Shutdown() + + // Register actors. + actor.RegisterWithSystem(system, "store", StoreServiceKey, &StoreActorBehavior{}) + + // Spawn FSM. + cfg := protofsm.StateMachineCfg[Event, OutboxEvent, *Environment]{ + Logger: btclog.Disabled, + InitialState: &StateInit{}, + Env: NewEnvironment(store), + } + + fsmRef := protofsm.NewSystemsActorStateMachine(ctx, cfg, system, "test-workflow") + + // Send event and verify state transition. + resp := fsmRef.Ask(ctx, protofsm.ActorMessage[Event]{ + Event: EventStart{ID: "test-123"}, + }).Await(ctx) + + require.NoError(t, resp.Err()) +} +``` + +### Testing Restart Safety + +Test that workflows resume correctly: + +```go +func TestRestart(t *testing.T) { + // 1. Start workflow, persist state. + manager1 := NewManager(store) + manager1.StartWorkflow(ctx, "wf-1") + // ... wait for state = Processing + + // 2. Stop manager (simulate crash). + manager1.Stop() + + // 3. Restart with same store. + manager2 := NewManager(store) + manager2.ResumeWorkflows(ctx) + + // 4. Verify workflow continues from persisted state. + // ... assert workflow completes successfully +} +``` + +--- + +## Troubleshooting + +### Issue: "swap not found" when persisting + +**Cause:** StoreActor calls UpdateSwap before swap exists in store. + +**Solution:** Check if swap exists, use CreateSwap for new, UpdateSwap for existing: + +```go +existing, err := s.store.Get(msg.ID) +isNew := err != nil + +if isNew { + s.store.Create(newSwap) +} else { + s.store.Update(existing) +} +``` + +### Issue: FSM stuck in non-terminal state + +**Cause:** Actor performed operation but didn't send result event to FSM. + +**Solution:** Always send result event via `ActorTellRef`: + +```go +// After completing operation: +msg.ActorTellRef.Tell(ctx, protofsm.ActorMessage[Event]{ + Event: EventOperationComplete{}, +}) +``` + +### Issue: EventResume doesn't re-establish monitoring + +**Cause:** State doesn't re-emit outbox events on EventResume. + +**Solution:** Handle EventResume in every non-terminal state: + +```go +case EventResume: + return &protofsm.StateTransition[...]{ + NextState: s, + NewEvents: fn.Some(protofsm.EmittedEvent[...]{ + Outbox: []OutboxEvent{ + NewOutboxMonitor(s.id, env.actorRef), + }, + }), + }, nil +``` + +### Issue: Method name conflicts in composed interfaces + +**Cause:** Multiple interfaces with same method name but different return types. + +**Solution:** Use package-specific method names: + +```go +// Instead of: +type InStore interface { + UpsertSwap(ctx context.Context, swap *StoredSwap) error +} + +type OutStore interface { + UpsertSwap(ctx context.Context, swap *StoredSwap) error // Conflict! +} + +// Do: +type InStore interface { + UpsertInSwap(ctx context.Context, swap *StoredSwap) error +} + +type OutStore interface { + UpsertOutSwap(ctx context.Context, swap *StoredSwap) error +} +``` + +--- + +## Best Practices Summary + +1. ✅ **Use sealed interfaces** for Events, States, OutboxEvents +2. ✅ **States are immutable data** - no channels, mutexes, or goroutines +3. ✅ **Emit outbox events for side effects** - never call external systems from ProcessEvent +4. ✅ **Always handle EventResume** in non-terminal states +5. ✅ **Actors send result events** back to FSM via ActorTellRef +6. ✅ **Use Tell for async, Ask for sync** outbox events +7. ✅ **One ActorSystem per manager**, multiple FSM actors per workflow instance +8. ✅ **Persist state after every transition** via OutboxPersist +9. ✅ **Make actors idempotent** - safe to receive same message twice +10. ✅ **Test restart scenarios** - load from storage, send EventResume, verify completion + +--- + +## Example: See It In Action + +Check `baselib/example/` for a complete document approval workflow: +- `example_protofsm.go` - FSM states and events +- `example_actors.go` - ReviewService and NotificationService actors +- `example_test.go` - Integration test with actor system + +Or study the production implementations: +- `sdk/swaps/in/` - Client in-swap (simple: fund → monitor → complete/refund) +- `swapserver/out/` - Server out-swap (complex: intercept → fund → monitor → settle HTLC) +- `swapserver/in/` - Server in-swap (multi-actor: monitor → pay invoice → claim) + +--- + +**Summary:** The actor + protofsm pattern provides restart-safe, event-driven workflows with clean separation between FSM logic (pure state transitions) and side effects (actors). Key insight: FSMs emit outbox events which are automatically routed to actors, actors send result events back to FSMs. diff --git a/baselib/actor/README.md b/baselib/actor/README.md new file mode 100644 index 000000000..5d968ebe6 --- /dev/null +++ b/baselib/actor/README.md @@ -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/lightninglabs/darepo-client/baselib/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/lightninglabs/darepo-client/baselib/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 { + <> + +Tell(Message) + +Ask(Message) Future + } + + class Message { + <> + } + + 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`. diff --git a/baselib/actor/actor.go b/baselib/actor/actor.go new file mode 100644 index 000000000..5776eaf76 --- /dev/null +++ b/baselib/actor/actor.go @@ -0,0 +1,280 @@ +package actor + +import ( + "context" + "sync" + + "github.com/lightningnetwork/lnd/fn/v2" +) + +// ActorConfig holds the configuration parameters for creating a new Actor. +// It is generic over M (Message type) and R (Response type) to accommodate +// the actor's specific behavior. +type ActorConfig[M Message, R any] struct { + // ID is the unique identifier for the actor. + ID string + + // Behavior defines how the actor responds to messages. + Behavior ActorBehavior[M, R] + + // DLO is a reference to the dead letter office for this actor system. + // If nil, undeliverable messages during shutdown or due to a full + // mailbox (if such logic were added) might be dropped. + DLO ActorRef[Message, any] + + // MailboxSize defines the buffer capacity of the actor's mailbox. + MailboxSize int +} + +// envelope wraps a message with its associated promise. This allows the sender +// of an "ask" message to await a response. If the promise is nil, it +// signifies a "tell" operation (fire-and-forget). +type envelope[M Message, R any] struct { + message M + promise Promise[R] +} + +// Actor represents a concrete actor implementation. It encapsulates a behavior, +// manages its internal state implicitly through that behavior, and processes +// messages from its mailbox sequentially in its own goroutine. +type Actor[M Message, R any] struct { + // id is the unique identifier for the actor. + id string + + // behavior defines how the actor responds to messages. + behavior ActorBehavior[M, R] + + // mailbox is the incoming message queue for the actor. + mailbox chan envelope[M, R] + + // ctx is the context governing the actor's lifecycle. + ctx context.Context + + // cancel is the function to cancel the actor's context. + cancel context.CancelFunc + + // dlo is a reference to the dead letter office for this actor system. + dlo ActorRef[Message, any] + + // startOnce ensures the actor's processing loop is started only once. + startOnce sync.Once + + // stopOnce ensures the actor's processing loop is stopped only once. + stopOnce sync.Once + + // ref is the cached ActorRef for this actor. + ref ActorRef[M, R] +} + +// NewActor creates a new actor instance with the given ID and behavior. +// It initializes the actor's internal structures but does not start its +// message processing goroutine. The Start() method must be called to begin +// processing messages. +func NewActor[M Message, R any](cfg ActorConfig[M, R]) *Actor[M, R] { + ctx, cancel := context.WithCancel(context.Background()) + + // Ensure MailboxSize has a sane default if not specified or zero. A + // capacity of 0 would make the channel unbuffered, which is generally + // not desired for actor mailboxes. + mailboxCapacity := cfg.MailboxSize + if mailboxCapacity <= 0 { + // Default to a small capacity if an invalid one is given. This + // could also come from a global constant. + mailboxCapacity = 1 + } + + actor := &Actor[M, R]{ + id: cfg.ID, + behavior: cfg.Behavior, + mailbox: make(chan envelope[M, R], mailboxCapacity), + ctx: ctx, + cancel: cancel, + dlo: cfg.DLO, + } + + // Create and cache the actor's own reference. + actor.ref = &actorRefImpl[M, R]{ + actor: actor, + } + + return actor +} + +// Start initiates the actor's message processing loop in a new goroutine. This +// method should be called once after the actor is created. +func (a *Actor[M, R]) Start() { + a.startOnce.Do(func() { + go a.process() + }) +} + +// process is the main event loop for the actor. It continuously monitors its +// mailbox for incoming messages and its context for cancellation signals. +func (a *Actor[M, R]) process() { + for { + select { + case env := <-a.mailbox: + result := a.behavior.Receive(a.ctx, env.message) + + // If a promise was provided (i.e., it was an "ask" + // operation), complete the promise with the result from + // the behavior. + if env.promise != nil { + env.promise.Complete(result) + } + + // The actor's context has been cancelled, signaling a stop + // request. Exit the processing loop to terminate the actor's + // goroutine. Before exiting, drain any remaining messages from + // the mailbox. + case <-a.ctx.Done(): + // Close the mailbox to prevent new incoming messages + // and to allow the range operator below to terminate. + close(a.mailbox) + + // Drain any remaining messages. + for env := range a.mailbox { + // If a DLO is configured, send the original + // message there for auditing or potential + // manual reprocessing. + if a.dlo != nil { + a.dlo.Tell( + context.Background(), + env.message, + ) + } + + // If it was an Ask, complete the promise with + // an error indicating the actor terminated. + if env.promise != nil { + env.promise.Complete(fn.Err[R]( + ErrActorTerminated), + ) + } + } + + return + } + } +} + +// Stop signals the actor to terminate its processing loop and shut down. +// This is achieved by cancelling the actor's internal context. The actor's +// goroutine will exit once it detects the context cancellation. +func (a *Actor[M, R]) Stop() { + a.stopOnce.Do(func() { + a.cancel() + }) +} + +// actorRefImpl provides a concrete implementation of the ActorRef interface. It +// holds a reference to the target Actor instance, enabling message sending. +type actorRefImpl[M Message, R any] struct { + actor *Actor[M, R] +} + +// Tell sends a message without waiting for a response. If the context is +// cancelled before the message can be sent to the actor's mailbox, the message +// may be dropped. +// +//nolint:lll +func (ref *actorRefImpl[M, R]) Tell(ctx context.Context, msg M) { + // If the actor's own context is already done, don't try to send. + // Route to DLO if available. + if ref.actor.ctx.Err() != nil { + ref.trySendToDLO(msg) + return + } + + select { + // Message successfully enqueued in the actor's mailbox. + case ref.actor.mailbox <- envelope[M, R]{message: msg, promise: nil}: + + // The context for the Tell operation was cancelled before the message + // could be enqueued. The message is dropped. + case <-ctx.Done(): + + // The actor itself has been stopped/terminated. + case <-ref.actor.ctx.Done(): + // If the actor is terminated and has a DLO, send the message + // there. Otherwise, it's dropped. + ref.trySendToDLO(msg) + } +} + +// Ask sends a message and returns a Future for the response. The Future will be +// completed with the actor's reply or an error if the operation fails (e.g., +// context cancellation before send). +// +//nolint:lll +func (ref *actorRefImpl[M, R]) Ask(ctx context.Context, msg M) Future[R] { + // Create a new promise that will be fulfilled with the actor's response. + promise := NewPromise[R]() + + // If the actor's own context is already done, complete the promise with + // ErrActorTerminated and return immediately. This is the primary guard + // against trying to send to a stopped actor. + if ref.actor.ctx.Err() != nil { + promise.Complete(fn.Err[R](ErrActorTerminated)) + return promise.Future() + } + + // Check if the context is already done before attempting to send. This + // ensures deterministic behavior and prevents a race where the message + // could be enqueued even though the context was already cancelled. + if ctx.Err() != nil { + promise.Complete(fn.Err[R](ctx.Err())) + return promise.Future() + } + + select { + // Attempt to send the message along with its promise to the actor's + // mailbox. + case ref.actor.mailbox <- envelope[M, R]{message: msg, promise: promise}: + + // The context for the Ask operation was cancelled before the message + // could be enqueued. Complete the promise with the context's error to + // unblock the caller. + case <-ctx.Done(): + promise.Complete(fn.Err[R](ctx.Err())) + + // The actor's context was cancelled (e.g., actor stopped) while this + // Ask operation was attempting to send (e.g., mailbox was full). + case <-ref.actor.ctx.Done(): + promise.Complete(fn.Err[R](ErrActorTerminated)) + } + + // Return the future associated with the promise, allowing the caller to + // await the response. + return promise.Future() +} + +// trySendToDLO attempts to send the message to the actor's DLO if configured. +func (ref *actorRefImpl[M, R]) trySendToDLO(msg M) { + if ref.actor.dlo != nil { + // Use context.Background() for sending to DLO as the + // original context might be done or the operation + // should not be bound by it. + // This Tell to DLO is fire-and-forget. + ref.actor.dlo.Tell(context.Background(), msg) + } +} + +// ID returns the unique identifier for this actor. +func (ref *actorRefImpl[M, R]) ID() string { + return ref.actor.id +} + +// Ref returns an ActorRef for this actor. This allows clients to interact with +// the actor (send messages) without having direct access to the Actor struct +// itself, promoting encapsulation and location transparency. +func (a *Actor[M, R]) Ref() ActorRef[M, R] { + return a.ref +} + +// TellRef returns a TellOnlyRef for this actor. This allows clients to send +// messages to the actor using only the "tell" pattern (fire-and-forget), +// without having access to "ask" capabilities. +func (a *Actor[M, R]) TellRef() TellOnlyRef[M] { + return a.ref +} diff --git a/baselib/actor/actor_test.go b/baselib/actor/actor_test.go new file mode 100644 index 000000000..d39fb028c --- /dev/null +++ b/baselib/actor/actor_test.go @@ -0,0 +1,395 @@ +package actor + +import ( + "context" + "errors" + "fmt" + "reflect" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/stretchr/testify/require" +) + +// testMsg is a simple message type for testing. It embeds BaseMessage to +// satisfy the actor.Message interface. +type testMsg struct { + BaseMessage + data string + + replyChan chan string +} + +// MessageType returns the type name of the message. +func (m *testMsg) MessageType() string { + return "testMsg" +} + +// newTestMsg creates a new test message. +func newTestMsg(data string) *testMsg { + return &testMsg{data: data} +} + +// newTestMsgWithReply creates a new test message that includes a reply channel. +// This can be used by test behaviors to send data back to the test +// synchronously, especially for Tell operations. +func newTestMsgWithReply(data string, replyChan chan string) *testMsg { + return &testMsg{data: data, replyChan: replyChan} +} + +// echoBehavior is a simple actor behavior that processes *testMsg messages. It +// stores the last message's data and, for Ask, echoes it back. For Tell, if +// replyChan is set in testMsg, it sends data back on it. +type echoBehavior struct { + lastMsgData atomic.Value + processingDelay time.Duration + t *testing.T +} + +// newEchoBehavior creates a new echoBehavior. +func newEchoBehavior(t *testing.T, delay time.Duration) *echoBehavior { + return &echoBehavior{t: t, processingDelay: delay} +} + +// Receive handles incoming messages. It simulates work if processingDelay is +// set, stores the message data, and responds for Ask operations or via +// replyChan for Tell. +func (b *echoBehavior) Receive(_ context.Context, + msg *testMsg) fn.Result[string] { + + if b.processingDelay > 0 { + time.Sleep(b.processingDelay) + } + + b.lastMsgData.Store(msg.data) + + if msg.replyChan != nil { + // Attempt to send the data on the reply channel, but quit if + // it takes longer than 1 second (e.g., channel unbuffered + // and no receiver). + select { + case msg.replyChan <- msg.data: + case <-time.After(time.Second): + b.t.Logf("warning: replyChan send timed out") + } + } + + return fn.Ok(fmt.Sprintf("echo: %s", msg.data)) +} + +// GetLastMsgData retrieves the data from the last message processed. +func (b *echoBehavior) GetLastMsgData() (string, bool) { + val := b.lastMsgData.Load() + if val == nil { + return "", false + } + data, ok := val.(string) + return data, ok +} + +// errorBehavior is an actor behavior that always returns a predefined error +// upon receiving a message. +type errorBehavior struct { + err error +} + +// newErrorBehavior creates a new errorBehavior. +func newErrorBehavior(err error) *errorBehavior { + return &errorBehavior{err: err} +} + +// Receive always returns the configured error. +func (b *errorBehavior) Receive(_ context.Context, + _ *testMsg) fn.Result[string] { + + return fn.Err[string](b.err) +} + +// blockingBehavior is an actor behavior that blocks until its actorCtx is done. +type blockingBehavior struct{} + +// Receive blocks until the actor's context is cancelled, then returns the +// context's error. +func (b *blockingBehavior) Receive(actorCtx context.Context, + _ *testMsg) fn.Result[string] { + + <-actorCtx.Done() + return fn.Err[string](actorCtx.Err()) +} + +// deadLetterTestMsg is a distinct message type used for testing DLO +// interactions. +type deadLetterTestMsg struct { + BaseMessage + id string +} + +// MessageType returns the type name of the message. +func (m *deadLetterTestMsg) MessageType() string { + return "deadLetterTestMsg" +} + +// deadLetterObserverBehavior is a behavior for a test Dead Letter Office actor. +// It records all messages sent to it, allowing tests to verify DLO +// interactions. +type deadLetterObserverBehavior struct { + mu sync.Mutex + receivedMsgs []Message +} + +// newDeadLetterObserverBehavior creates a new deadLetterObserverBehavior. +func newDeadLetterObserverBehavior() *deadLetterObserverBehavior { + return &deadLetterObserverBehavior{ + receivedMsgs: make([]Message, 0), + } +} + +// Receive records the incoming message and returns a successful result. +func (b *deadLetterObserverBehavior) Receive(_ context.Context, + msg Message) fn.Result[any] { + + b.mu.Lock() + b.receivedMsgs = append(b.receivedMsgs, msg) + b.mu.Unlock() + + return fn.Ok[any](nil) +} + +// GetReceivedMsgs returns a copy of all messages received by this DLO. +func (b *deadLetterObserverBehavior) GetReceivedMsgs() []Message { + b.mu.Lock() + defer b.mu.Unlock() + + msgs := make([]Message, len(b.receivedMsgs)) + copy(msgs, b.receivedMsgs) + + return msgs +} + +// actorTestHarness provides helper methods for setting up actors in tests. It +// manages a dedicated DLO for actors created through it. +type actorTestHarness struct { + t *testing.T + dlo *Actor[Message, any] + dloBeh *deadLetterObserverBehavior +} + +// newActorTestHarness sets up a test harness with a dedicated DLO. The DLO is +// automatically stopped when the test cleans up. +func newActorTestHarness(t *testing.T) *actorTestHarness { + t.Helper() + + dloBeh := newDeadLetterObserverBehavior() + dloCfg := ActorConfig[Message, any]{ + ID: "test-dlo-" + t.Name(), + Behavior: dloBeh, + DLO: nil, + MailboxSize: 10, + } + dloActor := NewActor[Message, any](dloCfg) + dloActor.Start() + + t.Cleanup(dloActor.Stop) + + return &actorTestHarness{ + t: t, + dlo: dloActor, + dloBeh: dloBeh, + } +} + +// newActor creates, starts, and registers a new actor for cleanup. The actor +// will use the harness's DLO. +func (h *actorTestHarness) newActor(id string, + beh ActorBehavior[*testMsg, string], + mailboxSize int) *Actor[*testMsg, string] { + + h.t.Helper() + + cfg := ActorConfig[*testMsg, string]{ + ID: id, + Behavior: beh, + DLO: h.dlo.Ref(), + MailboxSize: mailboxSize, + } + actor := NewActor(cfg) + actor.Start() + + h.t.Cleanup(actor.Stop) + + return actor +} + +// assertDLOMessage checks that the DLO eventually receives a specific message. +func (h *actorTestHarness) assertDLOMessage(expectedMsg Message) { + h.t.Helper() + require.Eventually(h.t, func() bool { + msgs := h.dloBeh.GetReceivedMsgs() + for _, m := range msgs { + if reflect.DeepEqual(m, expectedMsg) { + return true + } + } + return false + }, time.Second, 10*time.Millisecond, + "dLO did not receive expected message: %v", expectedMsg, + ) +} + +// assertNoDLOMessages checks that the DLO has not received any messages. +func (h *actorTestHarness) assertNoDLOMessages() { + h.t.Helper() + + // Allow a very brief moment for any async DLO sends to occur. + time.Sleep(20 * time.Millisecond) + + msgs := h.dloBeh.GetReceivedMsgs() + + require.Empty(h.t, msgs, "dLO received unexpected messages") +} + +// TestActorNewActorIDAndRefs verifies that NewActor correctly initializes an +// actor's ID and provides functional ActorRef and TellOnlyRef instances. +func TestActorNewActorIDAndRefs(t *testing.T) { + t.Parallel() + + h := newActorTestHarness(t) + actorID := "test-actor-1" + beh := newEchoBehavior(t, 0) + actor := h.newActor(actorID, beh, 1) + + require.Equal(t, actorID, actor.Ref().ID(), "actorRef ID mismatch") + require.Equal( + t, actorID, actor.TellRef().ID(), "tellOnlyRef ID mismatch", + ) + require.NotNil(t, actor.Ref(), "actorRef should not be nil") + require.NotNil(t, actor.TellRef(), "tellOnlyRef should not be nil") +} + +// TestActorStartStop verifies the basic lifecycle of an actor: starting, +// processing messages, and stopping. +func TestActorStartStop(t *testing.T) { + t.Parallel() + + h := newActorTestHarness(t) + beh := newEchoBehavior(t, 0) + actor := h.newActor("test-actor-lifecycle", beh, 1) + + // Actor should be running and process a message. + msgData := "hello" + replyChan := make(chan string, 1) + actor.Ref().Tell( + context.Background(), newTestMsgWithReply(msgData, replyChan), + ) + + received, err := fn.RecvOrTimeout(replyChan, 100*time.Millisecond) + if err != nil { + t.Fatal("timed out waiting for actor to process message") + } + require.Equal( + t, msgData, received, "actor did not process message before stop", + ) + + actor.Stop() + time.Sleep(50 * time.Millisecond) + + // Try sending another message; it should ideally not be processed or go + // to DLO. + msgDataAfterStop := "message-after-stop" + replyChanAfterStop := make(chan string, 1) + actor.Ref().Tell( + context.Background(), + newTestMsgWithReply(msgDataAfterStop, replyChanAfterStop), + ) + + // We expect a timeout here, meaning the message was not processed by + // the echoBehavior's replyChan. + _, err = fn.RecvOrTimeout(replyChanAfterStop, 100*time.Millisecond) + if err == nil { // err == nil means a message was received + t.Fatal("actor processed message after Stop()") + } + require.ErrorContains(t, err, "timeout hit") + + h.assertDLOMessage( + &testMsg{data: msgDataAfterStop, replyChan: replyChanAfterStop}, + ) +} + +// TestActorTellBasic verifies that a message sent via Tell is processed by the +// actor's behavior. +func TestActorTellBasic(t *testing.T) { + t.Parallel() + + h := newActorTestHarness(t) + beh := newEchoBehavior(t, 0) + actor := h.newActor("test-actor-tell", beh, 1) + + msgData := "tell-message" + replyChan := make(chan string, 1) + actor.Ref().Tell( + context.Background(), newTestMsgWithReply(msgData, replyChan), + ) + + receivedTell, errTell := fn.RecvOrTimeout(replyChan, 100*time.Millisecond) + if errTell != nil { + t.Fatal("timed out waiting for Tell message processing") + } + require.Equal( + t, msgData, receivedTell, "behavior did not receive Tell message data", + ) + + lastData, ok := beh.GetLastMsgData() + require.True(t, ok, "last message data not set in behavior") + require.Equal(t, msgData, lastData, "last message data mismatch") + h.assertNoDLOMessages() +} + +// TestActorAskSuccess verifies that a message sent via Ask is processed, and +// the returned Future is completed with the behavior's successful result. +func TestActorAskSuccess(t *testing.T) { + t.Parallel() + + h := newActorTestHarness(t) + beh := newEchoBehavior(t, 0) + actor := h.newActor("test-actor-ask-success", beh, 1) + + msgData := "ask-message" + future := actor.Ref().Ask(context.Background(), newTestMsg(msgData)) + + result := future.Await(context.Background()) + require.False(t, result.IsErr(), "ask returned an error: %v", result.Err()) + + result.WhenOk(func(val string) { + expectedReply := fmt.Sprintf("echo: %s", msgData) + require.Equal(t, expectedReply, val, "ask response mismatch") + }) + + lastData, ok := beh.GetLastMsgData() + require.True(t, ok, "last message data not set in behavior") + require.Equal(t, msgData, lastData, "last message data mismatch") + h.assertNoDLOMessages() +} + +// TestActorAskErrorBehavior verifies that if an actor's behavior returns an +// error, the Future from an Ask call is completed with that error. +func TestActorAskErrorBehavior(t *testing.T) { + t.Parallel() + + h := newActorTestHarness(t) + expectedErr := errors.New("behavior error") + beh := newErrorBehavior(expectedErr) + actor := h.newActor("test-actor-ask-error", beh, 1) + + future := actor.Ref().Ask( + context.Background(), newTestMsg("ask-error-test"), + ) + + result := future.Await(context.Background()) + require.True(t, result.IsErr(), "ask should have returned an error") + require.ErrorIs(t, result.Err(), expectedErr, "ask error mismatch") + + h.assertNoDLOMessages() +} diff --git a/baselib/actor/example_basic_actor_test.go b/baselib/actor/example_basic_actor_test.go new file mode 100644 index 000000000..ef79a666c --- /dev/null +++ b/baselib/actor/example_basic_actor_test.go @@ -0,0 +1,97 @@ +package actor_test + +import ( + "context" + "fmt" + "time" + + "github.com/lightninglabs/darepo-client/baselib/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 := 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 +} diff --git a/baselib/actor/example_router_test.go b/baselib/actor/example_router_test.go new file mode 100644 index 000000000..65b91cc44 --- /dev/null +++ b/baselib/actor/example_router_test.go @@ -0,0 +1,112 @@ +package actor_test + +import ( + "context" + "fmt" + "time" + + "github.com/lightninglabs/darepo-client/baselib/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 +} diff --git a/baselib/actor/example_struct_actor_test.go b/baselib/actor/example_struct_actor_test.go new file mode 100644 index 000000000..3beab9c89 --- /dev/null +++ b/baselib/actor/example_struct_actor_test.go @@ -0,0 +1,147 @@ +package actor_test + +import ( + "context" + "fmt" + "time" + + "github.com/lightninglabs/darepo-client/baselib/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 := 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) +} diff --git a/baselib/actor/example_tell_only_test.go b/baselib/actor/example_tell_only_test.go new file mode 100644 index 000000000..6650af29f --- /dev/null +++ b/baselib/actor/example_tell_only_test.go @@ -0,0 +1,133 @@ +package actor_test + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/lightninglabs/darepo-client/baselib/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. +} diff --git a/baselib/actor/func_actor.go b/baselib/actor/func_actor.go new file mode 100644 index 000000000..f1580f03f --- /dev/null +++ b/baselib/actor/func_actor.go @@ -0,0 +1,45 @@ +package actor + +import ( + "context" + + "github.com/lightningnetwork/lnd/fn/v2" +) + +// ActorFunc is a function type that represents an actor which functions purely +// based on a simple function processor. +type ActorFunc[M Message, R any] func(context.Context, M) fn.Result[R] + +// FunctionBehavior adapts a function to the ActorBehavior interface. +type FunctionBehavior[M Message, R any] struct { + fn ActorFunc[M, R] +} + +// NewFunctionBehavior creates a behavior from a function. +func NewFunctionBehavior[M Message, R any]( + fn ActorFunc[M, R]) *FunctionBehavior[M, R] { + + return &FunctionBehavior[M, R]{fn: fn} +} + +// Receive implements ActorBehavior interface for the function. +// +// TODO(roasbeef): just base it off the function direct instead? +func (b *FunctionBehavior[M, R]) Receive(ctx context.Context, + msg M) fn.Result[R] { + + return b.fn(ctx, msg) +} + +// FunctionBehaviorFromSimple adapts a simpler function to the ActorBehavior +// interface. +func FunctionBehaviorFromSimple[M Message, R any]( + sFunc func(M) (R, error)) *FunctionBehavior[M, R] { + + return NewFunctionBehavior( + func(ctx context.Context, msg M) fn.Result[R] { + val, err := sFunc(msg) + return fn.NewResult(val, err) + }, + ) +} diff --git a/baselib/actor/future.go b/baselib/actor/future.go new file mode 100644 index 000000000..a4db1c37d --- /dev/null +++ b/baselib/actor/future.go @@ -0,0 +1,152 @@ +package actor + +import ( + "context" + "sync" + "sync/atomic" + + "github.com/lightningnetwork/lnd/fn/v2" +) + +// promiseImpl is a structure that can be used to complete a Future. It provides +// methods to set the result of an asynchronous operation and to obtain the +// Future interface for consumers. +// The promiseImpl itself is not typically exposed directly to consumers of the +// future's result; they interact with the Future interface. +type promiseImpl[T any] struct { + fut *futureImpl[T] +} + +// NewPromise creates a new Promise. The associated Future, which consumers can +// use to await the result, can be obtained via the Future() method. The Future +// is completed by calling the Complete() method on this Promise. +func NewPromise[T any]() Promise[T] { + return &promiseImpl[T]{ + fut: &futureImpl[T]{ + // done is a channel that will be closed when the future + // is completed. + done: make(chan struct{}), + }, + } +} + +// Future returns the Future interface associated with this Promise. Consumers +// can use this to Await the result or register callbacks. +func (p *promiseImpl[T]) Future() Future[T] { + return p.fut +} + +// Complete attempts to set the result of the future. It returns true if this +// call successfully set the result (i.e., it was the first to complete it), +// and false if the future had already been completed. This ensures that a +// future can only be completed once. The completion involves storing the result +// and signaling any goroutines waiting on the future's done channel. +func (p *promiseImpl[T]) Complete(result fn.Result[T]) bool { + var success bool + p.fut.completeOnce.Do(func() { + p.fut.resultCache.Store(&result) + close(p.fut.done) + success = true + }) + return success +} + +// futureImpl is the concrete implementation of the Future interface. It manages +// the state of an asynchronous computation's result. +type futureImpl[T any] struct { + // resultCache stores the fn.Result[T] after the future is completed. + // It's of type atomic.Pointer to allow lock-free reads after completion + // with improved type safety over atomic.Value. + resultCache atomic.Pointer[fn.Result[T]] + + // done is closed once the future is completed, signaling any waiting + // Await calls. + done chan struct{} + + // completeOnce ensures that the logic to set the result and close the + // done channel is executed only once. + completeOnce sync.Once +} + +// Await blocks until the result is available or the passed context is +// cancelled. If the future is already completed, it returns the result +// immediately. Otherwise, it waits for either the future's completion or the +// context's cancellation. +func (f *futureImpl[T]) Await(ctx context.Context) fn.Result[T] { + // First, try a non-blocking load from the cache. If the future is + // already completed, this will return the result directly. + if resPtr := f.resultCache.Load(); resPtr != nil { + return *resPtr + } + + // Wait for either the future to be done or the context to be cancelled. + select { + case <-f.done: + // The future has been completed. Load the result from the + // cache. It must be present now. Load and dereference. + // This load is safe because the 'done' channel is closed only + // after the resultCache is written (ensured by completeOnce). + resPtr := f.resultCache.Load() + + // resPtr should not be nil here as <-f.done was signaled. + return *resPtr + + case <-ctx.Done(): + // The waiting context was cancelled before the future completed. + return fn.Err[T](ctx.Err()) + } +} + +// ThenApply registers a function to transform the result of a future. The +// original future is not modified; a new Future instance representing the +// transformed result is returned. Once the original future completes +// successfully, the provided transformation function (fApply) is called with +// the result. The transformation is applied asynchronously in a new goroutine. +// If the passed context is cancelled while waiting for the +// original future to complete, the returned future will yield the context's +// error. +func (f *futureImpl[T]) ThenApply(ctx context.Context, fApply func(T) T) Future[T] { + // Create a new promise for the transformed result. + transformedPromise := NewPromise[T]() + + go func() { + // Await the original future's result, respecting the passed + // context for cancellation. + originalResult := f.Await(ctx) + + // If the original future completed with an error (or Await was + // cancelled by its context), complete the transformed future + // with the same error. + // This also handles the case where originalResult.Await(ctx) + // itself returned ctx.Err(). + if originalResult.IsErr() { + transformedPromise.Complete(originalResult) + return + } + + // Otherwise, the original future completed successfully. Apply the + // transformation function to its result. + originalResult.WhenOk(func(res T) { + newValue := fApply(res) + transformedPromise.Complete(fn.Ok(newValue)) + }) + }() + + return transformedPromise.Future() +} + +// OnComplete registers a function to be called when the result is ready. If the +// passed context is cancelled before the future completes, the callback +// function (cFunc) will be invoked with the context's error. The callback is +// executed in a new goroutine, so it does not block the completion path of the +// original future. +func (f *futureImpl[T]) OnComplete(ctx context.Context, cFunc func(fn.Result[T])) { + go func() { + // Await the original future's result, respecting the passed + // context for cancellation. + result := f.Await(ctx) + + // Call the callback function with the result. + cFunc(result) + }() +} diff --git a/baselib/actor/future_test.go b/baselib/actor/future_test.go new file mode 100644 index 000000000..07bd81fbc --- /dev/null +++ b/baselib/actor/future_test.go @@ -0,0 +1,449 @@ +package actor + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +// TestFutureAwaitContextCancellation tests that Await respects context +// cancellation if the context is cancelled before the future resolves. +func TestFutureAwaitContextCancellation(t *testing.T) { + t.Parallel() + + rapid.Check(t, func(t *rapid.T) { + // Test cancellation when the Await context is cancelled via + // context.Cancel. The underlying future will not be completed, allowing + // us to test the cancellation path of Await. + prom1 := NewPromise[int]() + fut1 := prom1.Future() + ctx1, cancel1 := context.WithCancel(context.Background()) + + // We'll cancel the future immediately after creating it. + cancel1() + + result1 := fut1.Await(ctx1) + if !result1.IsErr() || + !errors.Is(result1.Err(), context.Canceled) { + t.Fatalf("await with immediate cancel: expected "+ + "context.Canceled, got %v", result1.Err()) + } + + // Test cancellation when the Await context times out. The + // underlying future will also not be completed. + prom2 := NewPromise[int]() + fut2 := prom2.Future() + + // Use a very short timeout that will trigger. + ctx2, cancel2 := context.WithTimeout( + context.Background(), 1*time.Nanosecond, + ) + defer cancel2() + + // Await the future; it should fall through to the timeout + // because the future itself is not completed. + result2 := fut2.Await(ctx2) + if !result2.IsErr() || + !errors.Is(result2.Err(), context.DeadlineExceeded) { + + t.Fatalf("await with timeout: expected "+ + "context.DeadlineExceeded, got %v", + result2.Err()) + } + }) +} + +// TestFutureAwaitFutureCompletes tests that Await returns the future's +// result if the context is not cancelled before the future resolves. +func TestFutureAwaitFutureCompletes(t *testing.T) { + t.Parallel() + + rapid.Check(t, func(t *rapid.T) { + valToSet := rapid.Int().Draw(t, "valToSet") + + // With a 50% chance, configure the test to complete the future + // with an error instead of a successful value. + var errToSet error + if rapid.Bool().Draw(t, "have_error") { + errToSet = fmt.Errorf("err") + } + + promise := NewPromise[int]() + fut := promise.Future() + + // Use a background context for Await, as we expect the future + // to complete normally. + ctx := context.Background() + + // Complete the future in a separate goroutine to simulate an + // asynchronous operation. + go func() { + if errToSet != nil { + promise.Complete(fn.Err[int](errToSet)) + } else { + promise.Complete(fn.Ok(valToSet)) + } + }() + + // Now we'll wait for the future to complete, then verify below + // that the result (value or error) is as expected. + result := fut.Await(ctx) + + if errToSet != nil { + // If an error was set, verify that Await returns that + // specific error. + if !result.IsErr() || + !errors.Is(result.Err(), errToSet) { + + t.Fatalf("await with error: expected "+ + "error %v, got %v", errToSet, + result.Err()) + } + } else { + // If no error was set, verify that Await returns the + // correct value. + if result.IsErr() { + t.Fatalf("await with value: expected success, "+ + "got error %v", result.Err()) + } + result.WhenOk(func(val int) { + if val != valToSet { + t.Fatalf("await with value: "+ + "expected %v, got %v", + valToSet, val) + } + }) + } + }) +} + +// TestFutureThenApplyContextCancellation tests that ThenApply respects its +// context, yielding a context error if cancelled before the original future +// completes. +func TestFutureThenApplyContextCancellation(t *testing.T) { + t.Parallel() + + rapid.Check(t, func(t *rapid.T) { + // The original future will not be completed in this test case, + // allowing us to specifically test the cancellation behavior of + // the context passed to ThenApply. + originalPromise := NewPromise[int]() + originalFut := originalPromise.Future() + + // Create a context for ThenApply and cancel it immediately. + ctxApply, cancelApply := context.WithCancel( + context.Background(), + ) + cancelApply() + + var transformCalled atomic.Bool + transform := func(i int) int { + transformCalled.Store(true) + return i * 2 + } + + // Register the transformation. The ThenApply operation itself + // will start a goroutine to await the originalFut. + newFut := originalFut.ThenApply(ctxApply, transform) + + // Await the new (transformed) future. Use a background context + // for this Await to isolate the test to the cancellation of + // ctxApply. + result := newFut.Await(context.Background()) + + if !result.IsErr() || + !errors.Is(result.Err(), context.Canceled) { + + t.Fatalf("ThenApply with cancelled context: expected "+ + "context.Canceled, got %v", result.Err()) + } + if transformCalled.Load() { + t.Fatal("ThenApply transform function called " + + "despite context cancellation") + } + }) +} + +// TestFutureThenApplyOriginalFutureCompletes tests ThenApply's behavior when +// the original future completes (with a value or error) before ThenApply's +// context is cancelled. +func TestFutureThenApplyOriginalFutureCompletes(t *testing.T) { + t.Parallel() + + rapid.Check(t, func(t *rapid.T) { + initialVal := rapid.Int().Draw(t, "initialVal") + + // Configure whether the original future completes with an error + // or a successful value. + var originalErr error + if rapid.Bool().Draw(t, "have_error") { + originalErr = fmt.Errorf("original error") + } + + originalPromise := NewPromise[int]() + originalFut := originalPromise.Future() + + // Create a context for ThenApply that should not cancel before + // the original future completes. + ctxApply, cancelApply := context.WithTimeout( + context.Background(), 50*time.Millisecond, + ) + defer cancelApply() + + var transformCalled atomic.Bool + transform := func(i int) int { + transformCalled.Store(true) + return i * 2 + } + + newFut := originalFut.ThenApply(ctxApply, transform) + + // Complete the original future in a separate goroutine to + // simulate asynchrony. + go func() { + if originalErr != nil { + originalPromise.Complete( + fn.Err[int](originalErr), + ) + } else { + originalPromise.Complete(fn.Ok(initialVal)) + } + }() + + // Await our new future which transforms the original future's + // result. Use a background context for this Await. + result := newFut.Await(context.Background()) + + if originalErr != nil { + // If the original future had an error, the transformed + // future should also yield that same error. + if !result.IsErr() || + !errors.Is(result.Err(), originalErr) { + + t.Fatalf("ThenApply with original error: "+ + "expected error %v, got %v", + originalErr, result.Err()) + } + if transformCalled.Load() { + t.Fatal("ThenApply transform function called " + + "despite original future having " + + "an error") + } + } else { + // If the original future completed successfully, the + // transformed future should contain the transformed value. + if result.IsErr() { + t.Fatalf("ThenApply with original value: "+ + "expected success, got "+ + "error %v", result.Err()) + } + + if !transformCalled.Load() { + t.Fatal("ThenApply transform function not " + + "called for successful original future") + } + + result.WhenOk(func(val int) { + expectedTransformedVal := initialVal * 2 + if val != expectedTransformedVal { + t.Fatalf("ThenApply with original "+ + "value: expected "+ + "transformed %v, got %v", + expectedTransformedVal, val) + } + }) + } + }) +} + +// TestFutureOnCompleteContextCancellation tests that OnComplete's callback +// receives a context error if its context is cancelled before the future +// completes. +func TestFutureOnCompleteContextCancellation(t *testing.T) { + t.Parallel() + + rapid.Check(t, func(t *rapid.T) { + // The original future will not complete in this test, allowing + // us to focus on the cancellation of OnComplete's context. + originalPromise := NewPromise[int]() + originalFut := originalPromise.Future() + + // Create a context for OnComplete and cancel it immediately to + // simulate a premature cancellation. + ctxComplete, cancelComplete := context.WithCancel( + context.Background(), + ) + cancelComplete() + + var wg sync.WaitGroup + wg.Add(1) + var ( + callbackInvoked atomic.Bool + callbackResultValue fn.Result[int] + + // mu is a mutex to protect callbackResultValue as it's + // written by the callback goroutine and read by the + // test goroutine. + mu sync.Mutex + ) + + // Register an OnComplete callback. The callback itself runs in + // a new goroutine started by OnComplete. + originalFut.OnComplete(ctxComplete, func(res fn.Result[int]) { + mu.Lock() + callbackResultValue = res + mu.Unlock() + + callbackInvoked.Store(true) + wg.Done() + }) + + // Use a wait group and a channel to wait for the callback to + // be invoked. + waitChan := make(chan struct{}) + go func() { + wg.Wait() + close(waitChan) + }() + + select { + // The callback should be invoked, even if with a context error. + case <-waitChan: + case <-time.After(50 * time.Millisecond): + t.Fatal("OnComplete callback timed out " + + "waiting for execution after context cancel") + } + + require.True( + t, callbackInvoked.Load(), + "OnComplete callback not invoked", + ) + + mu.Lock() + defer mu.Unlock() + + // Verify that the callback received a context.Canceled error + // because its context (ctxComplete) was cancelled. + if !callbackResultValue.IsErr() || + !errors.Is(callbackResultValue.Err(), + context.Canceled) { + + t.Fatalf("OnComplete with cancelled context: callback "+ + "expected context.Canceled, got %v", + callbackResultValue.Err()) + } + }) +} + +// TestFutureOnCompleteFutureCompletes tests OnComplete's behavior when the +// future completes (with value or error) before its context is cancelled. +func TestFutureOnCompleteFutureCompletes(t *testing.T) { + t.Parallel() + + rapid.Check(t, func(t *rapid.T) { + valToSet := rapid.Int().Draw(t, "valToSet") + + // Configure whether the original future completes with an error + // or a successful value. + var originalErr error + if rapid.Bool().Draw(t, "have_error") { + originalErr = fmt.Errorf("original error") + } + + originalPromise := NewPromise[int]() + originalFut := originalPromise.Future() + + // Use a background context for OnComplete, as we expect the + // future to complete normally. + ctxComplete := context.Background() + + var wg sync.WaitGroup + wg.Add(1) + + var ( + callbackInvoked atomic.Bool + callbackResultValue fn.Result[int] + mu sync.Mutex + ) + + // Register an OnComplete callback. This callback will execute + // once the originalFut completes. + originalFut.OnComplete(ctxComplete, func(res fn.Result[int]) { + mu.Lock() + callbackResultValue = res + mu.Unlock() + + callbackInvoked.Store(true) + + wg.Done() + }) + + // Complete the original future in a separate goroutine to + // simulate an asynchronous operation. + go func() { + if originalErr != nil { + originalPromise.Complete( + fn.Err[int](originalErr), + ) + } else { + originalPromise.Complete(fn.Ok(valToSet)) + } + }() + + // Use a wait group and a channel to wait for the callback's + // execution. + waitChan := make(chan struct{}) + go func() { + wg.Wait() + close(waitChan) + }() + + select { + // The callback should be invoked as the future completes. + case <-waitChan: + case <-time.After(50 * time.Millisecond): + t.Fatal("OnComplete callback timed out " + + "waiting for execution") + } + + require.True(t, callbackInvoked.Load()) + + mu.Lock() + defer mu.Unlock() + + // Verify that the callback received the correct result (either + // the error or the value from the completed future). + if originalErr != nil { + if !callbackResultValue.IsErr() || + !errors.Is(callbackResultValue.Err(), + originalErr) { + + t.Fatalf("OnComplete with error: callback "+ + "expected error %v, got %v", + originalErr, + callbackResultValue.Err()) + } + } else { + if callbackResultValue.IsErr() { + t.Fatalf("OnComplete with value: callback "+ + "expected success, got error %v", + callbackResultValue.Err()) + } + callbackResultValue.WhenOk(func(val int) { + if val != valToSet { + t.Fatalf("OnComplete with value: "+ + "callback expected %v, got %v", + valToSet, val) + } + }) + } + }) +} diff --git a/baselib/actor/interface.go b/baselib/actor/interface.go new file mode 100644 index 000000000..da555b4da --- /dev/null +++ b/baselib/actor/interface.go @@ -0,0 +1,118 @@ +package actor + +import ( + "context" + "fmt" + + "github.com/lightningnetwork/lnd/fn/v2" +) + +// ErrActorTerminated indicates that an operation failed because the target +// actor was terminated or in the process of shutting down. +var ErrActorTerminated = fmt.Errorf("actor terminated") + +// BaseMessage is a helper struct that can be embedded in message types defined +// outside the actor package to satisfy the Message interface's unexported +// messageMarker method. +type BaseMessage struct{} + +// messageMarker implements the unexported method for the Message interface, +// allowing types that embed BaseMessage to satisfy the Message interface. +func (BaseMessage) messageMarker() {} + +// Message is a sealed interface for actor messages. Actors will receive +// messages conforming to this interface. The interface is "sealed" by the +// unexported messageMarker method, meaning only types that can satisfy it +// (e.g., by embedding BaseMessage or being in the same package) can be Messages. +type Message interface { + // messageMarker is a private method that makes this a sealed interface + // (see BaseMessage for embedding). + messageMarker() + + // MessageType returns the type name of the message for + // routing/filtering. + MessageType() string +} + +// PriorityMessage is an extension of the Message interface for messages that +// carry a priority level. This can be used by actor mailboxes or schedulers +// to prioritize message processing. +type PriorityMessage interface { + Message + + // Priority returns the processing priority of this message (higher = + // more important). + Priority() int +} + +// Future represents the result of an asynchronous computation. It allows +// consumers to wait for the result (Await), apply transformations upon +// completion (ThenApply), or register a callback to be executed when the +// result is available (OnComplete). +type Future[T any] interface { + // Await blocks until the result is available or the context is + // cancelled, then returns it. + Await(ctx context.Context) fn.Result[T] + + // ThenApply registers a function to transform the result of a future. + // The original future is not modified, a new instance of the future is + // returned. If the passed context is cancelled while waiting for the + // original future to complete, the new future will complete with the + // context's error. + ThenApply(ctx context.Context, fn func(T) T) Future[T] + + // OnComplete registers a function to be called when the result of the + // future is ready. If the passed context is cancelled before the future + // completes, the callback function will be invoked with the context's + // error. + OnComplete(ctx context.Context, fn func(fn.Result[T])) +} + +// Promise is an interface that allows for the completion of an associated +// Future. It provides a way to set the result of an asynchronous operation. +// The producer of an asynchronous result uses a Promise to set the outcome, +// while consumers use the associated Future to retrieve it. +type Promise[T any] interface { + // Future returns the Future interface associated with this Promise. + // Consumers can use this to Await the result or register callbacks. + Future() Future[T] + + // Complete attempts to set the result of the future. It returns true if + // this call successfully set the result (i.e., it was the first to + // complete it), and false if the future had already been completed. + Complete(result fn.Result[T]) bool +} + +// TellOnlyRef is a reference to an actor that only supports "tell" operations. +// This is useful for scenarios where only fire-and-forget message passing is +// needed, or to restrict capabilities. +type TellOnlyRef[M Message] interface { + // Tell sends a message without waiting for a response. If the + // context is cancelled before the message can be sent to the actor's + // mailbox, the message may be dropped. + Tell(ctx context.Context, msg M) + + // ID returns the unique identifier for this actor. + ID() string +} + +// ActorRef is a reference to an actor that supports both "tell" and "ask" +// operations. It embeds TellOnlyRef and adds the Ask method for +// request-response interactions. +type ActorRef[M Message, R any] interface { + TellOnlyRef[M] + + // Ask sends a message and returns a Future for the response. + // The Future will be completed with the actor's reply or an error + // if the operation fails (e.g., context cancellation before send). + Ask(ctx context.Context, msg M) Future[R] +} + +// ActorBehavior defines the logic for how an actor processes incoming messages. +// It is a strategy interface that encapsulates the actor's reaction to messages. +type ActorBehavior[M Message, R any] interface { + // Receive processes a message and returns a Result. The provided + // context is the actor's internal context, which can be used to + // detect actor shutdown requests. + Receive(actorCtx context.Context, msg M) fn.Result[R] +} diff --git a/baselib/actor/router.go b/baselib/actor/router.go new file mode 100644 index 000000000..29ceed4b1 --- /dev/null +++ b/baselib/actor/router.go @@ -0,0 +1,143 @@ +package actor + +import ( + "context" + "errors" + "sync/atomic" + + "github.com/lightningnetwork/lnd/fn/v2" +) + +// ErrNoActorsAvailable is returned when a router cannot find any actors +// registered for its service key to forward a message to. +var ErrNoActorsAvailable = errors.New("no actors available for service key") + +// RoutingStrategy defines the interface for selecting an actor from a list of +// available actors. +// The M (Message) and R (Response) type parameters ensure that the strategy +// is compatible with the types of actors it will be selecting. +type RoutingStrategy[M Message, R any] interface { + // Select chooses an ActorRef from the provided slice. It returns the + // selected actor or an error if no actor can be selected (e.g., if the + // list is empty or another strategy-specific issue occurs). + Select(refs []ActorRef[M, R]) (ActorRef[M, R], error) +} + +// RoundRobinStrategy implements a round-robin selection strategy. It is generic +// over M and R to match the RoutingStrategy interface, though its logic doesn't +// depend on these types directly for the selection mechanism itself. +type RoundRobinStrategy[M Message, R any] struct { + // index is used to pick the next actor in a round-robin fashion. It + // must be accessed atomically to ensure thread-safety if multiple + // goroutines use the same strategy instance (which they will via the + // router). + index uint64 +} + +// NewRoundRobinStrategy creates a new RoundRobinStrategy, initialized for +// round-robin selection. +func NewRoundRobinStrategy[M Message, R any]() *RoundRobinStrategy[M, R] { + return &RoundRobinStrategy[M, R]{} +} + +// Select picks an actor from the list using a round-robin algorithm. +func (s *RoundRobinStrategy[M, R]) Select(refs []ActorRef[M, R]) (ActorRef[M, R], error) { + if len(refs) == 0 { + return nil, ErrNoActorsAvailable + } + + // Atomically increment and get the current index for selection. + // We subtract 1 because AddUint64 returns the new value (which is + // 1-based for the first call after initialization to 0), and slice + // indexing is 0-based. + idx := atomic.AddUint64(&s.index, 1) - 1 + selectedRef := refs[idx%uint64(len(refs))] + + return selectedRef, nil +} + +// Router is a message-dispatching component that fronts multiple actors +// registered under a specific ServiceKey. It uses a RoutingStrategy to +// distribute messages to one of the available actors. It is generic over M +// (Message type) and R (Response type) to match the actors it routes to. +type Router[M Message, R any] struct { + receptionist *Receptionist + serviceKey ServiceKey[M, R] + strategy RoutingStrategy[M, R] + dlo ActorRef[Message, any] // Dead Letter Office reference. +} + +// NewRouter creates a new Router for a given service key and strategy. The +// receptionist is used to discover actors registered with the service key. +// The router itself is not an actor but a message dispatcher that behaves like +// an ActorRef from the sender's perspective. +func NewRouter[M Message, R any](receptionist *Receptionist, + key ServiceKey[M, R], strategy RoutingStrategy[M, R], + dlo ActorRef[Message, any]) *Router[M, R] { + + return &Router[M, R]{ + receptionist: receptionist, + serviceKey: key, + strategy: strategy, + dlo: dlo, + } +} + +// getActor dynamically finds available actors for the service key and selects +// one using the configured strategy. This method is called internally by Tell +// and Ask on each invocation to ensure up-to-date actor discovery. +func (r *Router[M, R]) getActor() (ActorRef[M, R], error) { + // Discover available actors from the receptionist. + availableActors := FindInReceptionist(r.receptionist, r.serviceKey) + if len(availableActors) == 0 { + return nil, ErrNoActorsAvailable + } + + // Select one actor using the strategy. + return r.strategy.Select(availableActors) +} + +// Tell sends a message to one of the actors managed by the router, selected by +// the routing strategy. If no actors are available or the send context is +// cancelled before the message can be enqueued in the target actor's mailbox, +// the message may be dropped. Errors during actor selection (e.g., +// ErrNoActorsAvailable) are currently not propagated from Tell, aligning with +// its fire-and-forget nature. Such errors could be logged internally if needed. +func (r *Router[M, R]) Tell(ctx context.Context, msg M) { + selectedActor, err := r.getActor() + if err != nil { + // If no actors are available for the service, and a DLO is + // configured, forward the message there. + if errors.Is(err, ErrNoActorsAvailable) && r.dlo != nil { + r.dlo.Tell(context.Background(), msg) + } + return + } + + selectedActor.Tell(ctx, msg) +} + +// Ask sends a message to one of the actors managed by the router, selected by +// the routing strategy, and returns a Future for the response. If no actors are +// available (ErrNoActorsAvailable), the Future will be completed with this +// error. If the send context is cancelled before the message can be enqueued in +// the chosen actor's mailbox, the Future will be completed with the context's error. +func (r *Router[M, R]) Ask(ctx context.Context, msg M) Future[R] { + selectedActor, err := r.getActor() + if err != nil { + // If no actor could be selected (e.g., none available), + // complete the promise immediately with the selection error. + promise := NewPromise[R]() + promise.Complete(fn.Err[R](err)) + return promise.Future() + } + + return selectedActor.Ask(ctx, msg) +} + +// ID provides an identifier for the router. Since a router isn't an actor +// itself but a dispatcher for a service, its ID can be based on the service +// key. +func (r *Router[M, R]) ID() string { + return "router(" + r.serviceKey.name + ")" +} diff --git a/baselib/actor/system.go b/baselib/actor/system.go new file mode 100644 index 000000000..7b142b56a --- /dev/null +++ b/baselib/actor/system.go @@ -0,0 +1,401 @@ +package actor + +import ( + "context" + "errors" + "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. +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 simply returns an + // error indicating the message was undeliverable. + deadLetterBehavior := NewFunctionBehavior( + func(ctx context.Context, msg Message) fn.Result[any] { + 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 := NewActor[Message, any](deadLetterActorCfg) + 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 +} + +// 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], +) ActorRef[M, R] { + + if as.ctx.Err() != nil { + // To avoid returning nil and causing a panic, we can create and + // return a reference to a dummy actor that is already stopped. + // This ensures that any calls to the returned ref will fail + // with ErrActorTerminated. + dummyCfg := ActorConfig[M, R]{ID: id} + dummyActor := NewActor(dummyCfg) + dummyActor.Stop() + return dummyActor.Ref() + } + + actorCfg := ActorConfig[M, R]{ + ID: id, + Behavior: behavior, + DLO: as.deadLetterActor, + MailboxSize: as.config.MailboxCapacity, + } + actorInstance := NewActor(actorCfg) + actorInstance.Start() + + // Add the actor instance to the system's list of stoppable actors. + // This map is protected by the system's mutex. + as.mu.Lock() + as.actors[actorInstance.id] = actorInstance + as.mu.Unlock() + + // 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() +} + +// 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 { + // 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, len(refs)-1) // Pre-allocate assuming one removal + for _, itemInSlice := range refs { // itemInSlice is of type 'any' + // 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 // Don't add to newRefs, effectively removing it. + } + } + 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]) ActorRef[M, R] { + + return RegisterWithSystem(as, id, sk, behavior) +} + +// 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([]interface{}, 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 +} diff --git a/baselib/actor/system_test.go b/baselib/actor/system_test.go new file mode 100644 index 000000000..e3e8deb9d --- /dev/null +++ b/baselib/actor/system_test.go @@ -0,0 +1,942 @@ +package actor + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/stretchr/testify/require" +) + +// TestActorSystemNewActorSystem verifies the basic initialization of an +// ActorSystem, including its default DLO. +func TestActorSystemNewActorSystem(t *testing.T) { + t.Parallel() + + as := NewActorSystem() + require.NotNil(t, as, "newActorSystem should not return nil") + require.NotNil(t, as.Receptionist(), "receptionist should not be nil") + require.NotNil(t, as.DeadLetters(), "deadLetters should not be nil") + require.Equal(t, "dead-letters", as.DeadLetters().ID(), "dLO ID mismatch") + + // Test the DLO's behavior (it should return an error for Ask). + testDLOMsg := newTestMsg("to-dlo") + future := as.DeadLetters().Ask(context.Background(), testDLOMsg) + result := future.Await(context.Background()) + + // We should get back an error for asks. + require.True( + t, result.IsErr(), "system DLO should return an error on Ask", + ) + expectedErrStr := "message undeliverable: " + testDLOMsg.MessageType() + require.EqualError( + t, result.Err(), expectedErrStr, "dLO error message mismatch", + ) + + // Shutdown the system to clean up resources. + err := as.Shutdown() + require.NoError(t, err, "actorSystem shutdown failed") +} + +// TestActorSystemRegisterWithSystem verifies actor registration, lifecycle +// management within the system. +func TestActorSystemRegisterWithSystem(t *testing.T) { + t.Parallel() + + as := NewActorSystem() + defer func() { + err := as.Shutdown() + require.NoError(t, err) + }() + + actorID := "test-actor-sys-reg" + serviceKey := NewServiceKey[*testMsg, string]("test-service") + + // Using echoBehavior from actor_test.go (implicitly available) + beh := newEchoBehavior(t, 0) + + // We'll start off by registering the actor. + actorRef := RegisterWithSystem(as, actorID, serviceKey, beh) + require.NotNil(t, actorRef, "registerWithSystem should return a valid ActorRef") + require.Equal(t, actorID, actorRef.ID(), "registered actor ID mismatch") + + // The actor should be found in the receptionist. + foundActors := FindInReceptionist(as.Receptionist(), serviceKey) + require.Len(t, foundActors, 1, "actor not found in receptionist") + require.Equal(t, actorRef, foundActors[0], "incorrect actor in receptionist") + + // Next, we'll send out a simple tell, using our reply channel to make + // sure it's actually processed. + msgData := "hello-system-actor" + replyChan := make(chan string, 1) + actorRef.Tell(context.Background(), newTestMsgWithReply(msgData, replyChan)) + + received, err := fn.RecvOrTimeout(replyChan, 100*time.Millisecond) + if err != nil { + t.Fatal("timed out waiting for actor to process message") + } + require.Equal(t, msgData, received, "actor did not process message") + + // Stop the actor through the system. + stopped := as.StopAndRemoveActor(actorID) + require.True(t, stopped, "StopAndRemoveActor failed") + + // Wait for actor to fully stop. + time.Sleep(50 * time.Millisecond) + + // Send a message to the now-stopped actor's ref. This should go to the + // system's DLO. + afterStopMsg := newTestMsg("after-stop-to-dlo") + require.NotPanics(t, func() { + actorRef.Tell(context.Background(), afterStopMsg) + }, "tell to stopped actor should not panic") +} + +// TestActorSystemShutdown verifies that all actors are stopped and the system +// context is cancelled upon shutdown. +func TestActorSystemShutdown(t *testing.T) { + t.Parallel() + + as := NewActorSystem() + + // We'll start by making 3 new actors, each with a unique ID. + numActors := 3 + actorRefs := make([]ActorRef[*testMsg, string], numActors) + for i := 0; i < numActors; i++ { + actorID := fmt.Sprintf("shutdown-test-actor-%d", i) + key := NewServiceKey[*testMsg, string]( + fmt.Sprintf("service-%d", i), + ) + beh := newEchoBehavior(t, 0) + actorRefs[i] = RegisterWithSystem(as, actorID, key, beh) + } + + // We'll now send a message to each actor to ensure that they're + // running. + for i, ref := range actorRefs { + future := ref.Ask( + context.Background(), + newTestMsg(fmt.Sprintf("ping-%d", i)), + ) + ctxAwait, cancelAwait := context.WithTimeout( + context.Background(), time.Second, + ) + res := future.Await(ctxAwait) + cancelAwait() + require.False( + t, res.IsErr(), + "actor %d failed to respond before shutdown: %v", + i, res.Err(), + ) + } + + // Next, trigger a shutdown, and assert that the done channel gets + // closed. + err := as.Shutdown() + require.NoError(t, err, "actorSystem shutdown failed") + + // Check if the system context is done using RecvOrTimeout with a zero + // timeout for a non-blocking check. + _, err = fn.RecvOrTimeout(as.ctx.Done(), time.Millisecond*100) + if err != nil { + t.Fatal("actorSystem context not cancelled after shutdown") + } + + // We'll now try to send a message to each of the actors, this should + // result in an error. + for i, ref := range actorRefs { + future := ref.Ask( + context.Background(), + newTestMsg(fmt.Sprintf("ping-after-shutdown-%d", i)), + ) + res := future.Await(context.Background()) + require.True( + t, res.IsErr(), + "actor %d Ask should fail after shutdown", i, + ) + require.ErrorIs(t, res.Err(), ErrActorTerminated) + } + + as.mu.RLock() + require.Nil(t, as.actors, "actors map should be nil after shutdown") + as.mu.RUnlock() + + // Once shutdown, we shouldn't be able to send to the DLO either. + dloRef := as.DeadLetters() + futureDLO := dloRef.Ask( + context.Background(), newTestMsg("ping-dlo-after-shutdown"), + ) + resDLO := futureDLO.Await(context.Background()) + require.True( + t, resDLO.IsErr(), "DLO Ask should fail after system shutdown", + ) + require.ErrorIs( + t, resDLO.Err(), ErrActorTerminated, + ) +} + +// TestActorSystemStopAndRemoveActor verifies specific actor stopping and +// removal. +func TestActorSystemStopAndRemoveActor(t *testing.T) { + t.Parallel() + + as := NewActorSystem() + defer func() { + err := as.Shutdown() + require.NoError(t, err) + }() + + // Make some actor IDs, then unique service keys, then use that to + // register two actors. + actor1ID := "actor-to-stop" + actor2ID := "actor-to-keep" + key1 := NewServiceKey[*testMsg, string]("service1") + key2 := NewServiceKey[*testMsg, string]("service2") + beh := newEchoBehavior(t, 0) + + ref1 := RegisterWithSystem(as, actor1ID, key1, beh) + ref2 := RegisterWithSystem(as, actor2ID, key2, beh) + + // If we remove one actor, then try to send to it, we should get an + // error. + stopped := as.StopAndRemoveActor(actor1ID) + require.True(t, stopped, "failed to stop and remove actor1") + + future1 := ref1.Ask(context.Background(), newTestMsg("ping-actor1")) + res1 := future1.Await(context.Background()) + require.True(t, res1.IsErr(), "actor1 should be stopped") + require.ErrorIs(t, res1.Err(), ErrActorTerminated) + + as.mu.RLock() + _, exists := as.actors[actor1ID] + as.mu.RUnlock() + + // The actor should no longer be found. + require.False(t, exists, "actor1 still in system's actor map") + + // Make sure that we can still send messages to the existing actor. + future2 := ref2.Ask( + context.Background(), newTestMsg("ping-actor2"), + ) + + ctxAwait2, cancelAwait2 := context.WithTimeout( + context.Background(), time.Second, + ) + res2 := future2.Await(ctxAwait2) + cancelAwait2() + + require.False( + t, res2.IsErr(), "actor2 should still be running: %v", + res2.Err(), + ) + res2.WhenOk(func(s string) { + require.Equal(t, "echo: ping-actor2", s) + }) + + stoppedNonExistent := as.StopAndRemoveActor("non-existent-actor") + require.False( + t, stoppedNonExistent, "stopping non-existent actor should "+ + "return false", + ) +} + +// TestReceptionist covers basic registration, finding, and unregistration. +func TestReceptionist(t *testing.T) { + t.Parallel() + + as := NewActorSystem() + defer func() { + err := as.Shutdown() + require.NoError(t, err) + }() + receptionist := as.Receptionist() + + key1 := NewServiceKey[*testMsg, string]("key1") + key2 := NewServiceKey[*testMsg, string]("key2") + key1Again := NewServiceKey[*testMsg, string]("key1") + + // Register 3 actor instance using the service keys we created above. + beh := newEchoBehavior(t, 0) + actor1Ref := RegisterWithSystem(as, "actor1-rec", key1, beh) + actor2Ref := RegisterWithSystem(as, "actor2-rec", key1, beh) + actor3Ref := RegisterWithSystem(as, "actor3-rec", key2, beh) + + // We should be able to find the actors we registered. + foundForKey1 := FindInReceptionist(receptionist, key1) + require.Len(t, foundForKey1, 2, "should find 2 actors for key1") + require.Contains(t, foundForKey1, actor1Ref) + require.Contains(t, foundForKey1, actor2Ref) + + foundForKey1Again := FindInReceptionist(receptionist, key1Again) + require.ElementsMatch(t, foundForKey1, foundForKey1Again) + + // Same goes for the second key we added. + foundForKey2 := FindInReceptionist(receptionist, key2) + require.Len(t, foundForKey2, 1, "should find 1 actor for key2") + require.Equal(t, actor3Ref, foundForKey2[0]) + + // We shouldn't be able to find a key we didn't add. + nonExistentKey := NewServiceKey[*testMsg, string]("non-existent") + foundForNonExistent := FindInReceptionist(receptionist, nonExistentKey) + require.Empty(t, foundForNonExistent) + + // We should be able to unregister the actors we added. + unregistered := UnregisterFromReceptionist( + receptionist, key1, actor1Ref, + ) + require.True(t, unregistered, "failed to unregister actor1Ref") + + foundForKey1AfterUnreg := FindInReceptionist(receptionist, key1) + require.Len(t, foundForKey1AfterUnreg, 1) + require.Equal(t, actor2Ref, foundForKey1AfterUnreg[0]) + + // If we try to unregister the same actor again, it should fail. + unregisteredAgain := UnregisterFromReceptionist(receptionist, key1, actor1Ref) + require.False(t, unregisteredAgain) + + unregisteredLast := UnregisterFromReceptionist(receptionist, key1, actor2Ref) + require.True(t, unregisteredLast) + foundForKey1AfterAllUnreg := FindInReceptionist(receptionist, key1) + require.Empty(t, foundForKey1AfterAllUnreg) + + receptionist.mu.RLock() + _, exists := receptionist.registrations[key1.name] + receptionist.mu.RUnlock() + require.False(t, exists, "key1 should be removed from registrations map") + + // Finally, if we use the wrong key, or one that doesn't exist, that + // should also fail. + unregisteredWrongKey := UnregisterFromReceptionist(receptionist, key1, actor3Ref) + require.False(t, unregisteredWrongKey) + unregisteredNonExistentKey := UnregisterFromReceptionist(receptionist, nonExistentKey, actor1Ref) + require.False(t, unregisteredNonExistentKey) +} + +// TestServiceKeyMethods tests Spawn and Unregister methods on ServiceKey. +func TestServiceKeyMethods(t *testing.T) { + t.Parallel() + + as := NewActorSystem() + defer func() { + err := as.Shutdown() + require.NoError(t, err) + }() + + key := NewServiceKey[*testMsg, string]("sk-service") + beh := newEchoBehavior(t, 0) + + // Attempt to spawn a new actor using the service key and desired + // behavior. + actorRef := key.Spawn(as, "actor-sk-spawn", beh) + require.NotNil(t, actorRef) + require.Equal(t, "actor-sk-spawn", actorRef.ID()) + + // We should be able to find the actor in the receptionist. + found := FindInReceptionist(as.Receptionist(), key) + require.Len(t, found, 1) + require.Equal(t, actorRef, found[0]) + + as.mu.RLock() + _, sysExists := as.actors[actorRef.ID()] + as.mu.RUnlock() + require.True(t, sysExists) + + // Next, try to unregister the actor using the service key. + success := key.Unregister(as, actorRef) + require.True(t, success, "serviceKey.Unregister failed") + + // The actor should no longer be found in the receptionist. + foundAfter := FindInReceptionist(as.Receptionist(), key) + require.Empty(t, foundAfter) + + as.mu.RLock() + _, sysExistsAfter := as.actors[actorRef.ID()] + as.mu.RUnlock() + require.False(t, sysExistsAfter) + + // If we try to send a message to the actor after unregistering it, then + // we should get an error. + future := actorRef.Ask(context.Background(), newTestMsg("ping")) + res := future.Await(context.Background()) + require.True(t, res.IsErr() && errors.Is(res.Err(), ErrActorTerminated)) + + successAgain := key.Unregister(as, actorRef) + require.False(t, successAgain) + + otherSys := NewActorSystem() // Create a different actor system + defer func() { + err := otherSys.Shutdown() + require.NoError(t, err) + }() + + // Create a dummy actor in otherSys of the correct generic type for the + // key. This actor won't be found in 'as', so Unregister should fail. + dummyBehOther := newEchoBehavior(t, 0) + dummyKeyOther := NewServiceKey[*testMsg, string]("dummy-other") + dummyActorRefOtherSys := RegisterWithSystem( + otherSys, "dummy-other-actor", dummyKeyOther, dummyBehOther, + ) + + successNonMember := key.Unregister(as, dummyActorRefOtherSys) + require.False(t, successNonMember) +} + +// TestServiceKeyUnregisterAll tests the UnregisterAll method on ServiceKey. +// It covers scenarios including basic unregistration of multiple actors, +// attempting to unregister with no actors present, unregistering actors for +// one key while leaving others intact, and the idempotency of the operation. +func TestServiceKeyUnregisterAll(t *testing.T) { + t.Parallel() + + // Common setup for all sub-tests. + as := NewActorSystem() + defer func() { + err := as.Shutdown() + require.NoError(t, err, "ActorSystem shutdown failed.") + }() + + // Common behavior for test actors used across sub-tests. + beh := newEchoBehavior(t, 0) + + t.Run("unregister all multiple actors", func(st *testing.T) { + key1 := NewServiceKey[*testMsg, string]("sk-ua-key1") + actor1Key1 := key1.Spawn(as, "actor1-k1-ua", beh) + actor2Key1 := key1.Spawn(as, "actor2-k1-ua", beh) + + // Verify they are registered in the receptionist. + foundActorsForKey1 := FindInReceptionist( + as.Receptionist(), key1, + ) + require.Len( + st, foundActorsForKey1, 2, + "actors for key1 not in receptionist initially.", + ) + + // Verify they are in the system's actor map. + as.mu.RLock() + _, actor1Key1Exists := as.actors[actor1Key1.ID()] + _, actor2Key1Exists := as.actors[actor2Key1.ID()] + as.mu.RUnlock() + require.True( + st, actor1Key1Exists, + "actor1 for key1 not in system actors map initially.", + ) + require.True( + st, actor2Key1Exists, + "actor2 for key1 not in system actors map initially.", + ) + + // Unregister all for key1. + stoppedCountKey1 := key1.UnregisterAll(as) + require.Equal( + st, 2, stoppedCountKey1, + "UnregisterAll for key1 returned incorrect count.", + ) + + // Verify they are unregistered from the receptionist. + foundActorsForKey1After := FindInReceptionist( + as.Receptionist(), key1, + ) + require.Empty( + st, foundActorsForKey1After, + "actors for key1 still in receptionist after "+ + "UnregisterAll.", + ) + + // Verify they are removed from system actors map. + as.mu.RLock() + _, actor1Key1ExistsAfter := as.actors[actor1Key1.ID()] + _, actor2Key1ExistsAfter := as.actors[actor2Key1.ID()] + as.mu.RUnlock() + require.False( + st, actor1Key1ExistsAfter, + "Actor1 for key1 still in system actors "+ + "map after UnregisterAll.", + ) + require.False( + st, actor2Key1ExistsAfter, + "Actor2 for key1 still in system actors "+ + "map after UnregisterAll.", + ) + + // Verify actors are stopped. + resultActor1Key1 := actor1Key1.Ask( + context.Background(), newTestMsg("ping-k1-a1"), + ).Await(context.Background()) + require.True( + st, resultActor1Key1.IsErr(), + "Actor1 key1 Ask should fail after UnregisterAll.", + ) + require.ErrorIs( + st, resultActor1Key1.Err(), ErrActorTerminated, + "Actor1 key1 not terminated with correct error.", + ) + + resultActor2Key1 := actor2Key1.Ask( + context.Background(), newTestMsg("ping-k1-a2"), + ).Await(context.Background()) + require.True( + st, resultActor2Key1.IsErr(), + "Actor2 key1 Ask should fail after UnregisterAll.", + ) + require.ErrorIs( + st, resultActor2Key1.Err(), ErrActorTerminated, + "Actor2 key1 not terminated with correct error.", + ) + }) + + t.Run("unregister all with no actors for the key", func(st *testing.T) { + keyEmpty := NewServiceKey[*testMsg, string]("sk-ua-key-empty") + stoppedCountEmptyKey := keyEmpty.UnregisterAll(as) + require.Equal( + st, 0, stoppedCountEmptyKey, + "UnregisterAll for empty key returned non-zero count.", + ) + + foundActorsForKeyEmpty := FindInReceptionist( + as.Receptionist(), keyEmpty, + ) + require.Empty( + st, foundActorsForKeyEmpty, + "Receptionist not empty for keyEmpty "+ + "after UnregisterAll.", + ) + }) + + t.Run("unregister all with mixed keys", func(st *testing.T) { + keyA := NewServiceKey[*testMsg, string]("sk-ua-keyA") + keyB := NewServiceKey[*testMsg, string]("sk-ua-keyB") + + // Spawn 3 actors, two of them will share the same service key. + actorA1 := keyA.Spawn(as, "actorA1-ua-mixed", beh) + actorA2 := keyA.Spawn(as, "actorA2-ua-mixed", beh) + actorB1 := keyB.Spawn(as, "actorB1-ua-mixed", beh) + + // Make sure we're able to find them in the receptionist. + require.Len( + st, FindInReceptionist(as.Receptionist(), keyA), 2, + "KeyA initial registration count mismatch.", + ) + require.Len( + st, FindInReceptionist(as.Receptionist(), keyB), 1, + "KeyB initial registration count mismatch.", + ) + + // We'll start by unregistering all actors for keyA. + stoppedCountKeyA := keyA.UnregisterAll(as) + require.Equal( + st, 2, stoppedCountKeyA, + "UnregisterAll for keyA returned incorrect count.", + ) + + // Verify keyA actors are gone from receptionist, keyB actor + // remains. + require.Empty( + st, FindInReceptionist(as.Receptionist(), keyA), + "actors for keyA still in receptionist after "+ + "UnregisterAll.", + ) + foundActorsForKeyBAfterA := FindInReceptionist( + as.Receptionist(), keyB, + ) + require.Len( + st, foundActorsForKeyBAfterA, 1, + "Actor for keyB affected by UnregisterAll on keyA.", + ) + require.Equal( + st, actorB1, foundActorsForKeyBAfterA[0], + "Wrong actor found for keyB.", + ) + + // Verify keyA actors are removed from system map, keyB actor + // remains. + as.mu.RLock() + _, actorA1ExistsAfterMixed := as.actors[actorA1.ID()] + _, actorA2ExistsAfterMixed := as.actors[actorA2.ID()] + _, actorB1ExistsAfterMixed := as.actors[actorB1.ID()] + as.mu.RUnlock() + require.False( + st, actorA1ExistsAfterMixed, + "ActorA1 still in system actors map after "+ + "mixed UnregisterAll.", + ) + require.False( + st, actorA2ExistsAfterMixed, + "ActorA2 still in system actors map after "+ + "mixed UnregisterAll.", + ) + require.True( + st, actorB1ExistsAfterMixed, + "ActorB1 removed from system actors map incorrectly.", + ) + + // Verify keyA actors are stopped, keyB actor is running. + resultActorA1Mixed := actorA1.Ask( + context.Background(), newTestMsg("ping-kA-a1"), + ).Await(context.Background()) + require.True(st, resultActorA1Mixed.IsErr()) + require.ErrorIs( + st, resultActorA1Mixed.Err(), ErrActorTerminated, + ) + + resultActorB1Mixed := actorB1.Ask( + context.Background(), newTestMsg("ping-kB-a1"), + ).Await(context.Background()) + require.False( + st, resultActorB1Mixed.IsErr(), + "ActorB1 terminated incorrectly (mixed test): %v", + resultActorB1Mixed.Err(), + ) + resultActorB1Mixed.WhenOk(func(s string) { + require.Equal(st, "echo: ping-kB-a1", s) + }) + }) + + t.Run("idempotency of UnregisterAll", func(st *testing.T) { + keyIdempotent := NewServiceKey[*testMsg, string]( + "sk-ua-key-idem", + ) + actorIdem := keyIdempotent.Spawn(as, "actor-idem-ua", beh) + + // First call should unregister and stop. + stoppedCountFirstCall := keyIdempotent.UnregisterAll(as) + require.Equal( + st, 1, stoppedCountFirstCall, + "UnregisterAll (first call) incorrect count.", + ) + + // Second call should do nothing and return 0. + stoppedCountSecondCall := keyIdempotent.UnregisterAll(as) + require.Equal( + st, 0, stoppedCountSecondCall, + "UnregisterAll (second call) incorrect count, not "+ + "idempotent.", + ) + + // Verify actor is gone from receptionist and system map, and is + // stopped. + require.Empty( + st, FindInReceptionist(as.Receptionist(), keyIdempotent), + "Actors for keyIdempotent still in receptionist "+ + "after calls.", + ) + + as.mu.RLock() + _, actorIdemExistsAfter := as.actors[actorIdem.ID()] + as.mu.RUnlock() + require.False( + st, actorIdemExistsAfter, + "ActorIdem still in system actors map after calls.", + ) + + resultActorIdem := actorIdem.Ask( + context.Background(), newTestMsg("ping-kidem-a1"), + ).Await(context.Background()) + require.True(st, resultActorIdem.IsErr()) + require.ErrorIs(st, resultActorIdem.Err(), ErrActorTerminated) + }) +} + +// routerTestHarness helps set up routers and their associated actors for testing. +// It uses an actorTestHarness internally for DLO observation for the router. +type routerTestHarness struct { + *actorTestHarness + as *ActorSystem + receptionist *Receptionist +} + +// newRouterTestHarness sets up a new harness for router testing. +// It creates an ActorSystem for actors that the router will route to, +// and uses the embedded actorTestHarness for the router's own DLO. +func newRouterTestHarness(t *testing.T) *routerTestHarness { + t.Helper() + system := NewActorSystem() + t.Cleanup(func() { + err := system.Shutdown() + require.NoError(t, err, "router test actor system shutdown failed") + }) + + // The DLO for the router itself will come from actorTestHarness. + // Actors managed by `system` (router targets) will use `system.DeadLetters()`. + return &routerTestHarness{ + actorTestHarness: newActorTestHarness(t), + as: system, + receptionist: system.Receptionist(), + } +} + +// newRouterTargetActor creates an actor, registers it with the harness's +// ActorSystem (h.as) and Receptionist under the given service key. This actor +// is intended to be a target for the router. +func (h *routerTestHarness) newRouterTargetActor(id string, + key ServiceKey[*testMsg, string], + beh ActorBehavior[*testMsg, string]) ActorRef[*testMsg, string] { + + h.t.Helper() + return RegisterWithSystem(h.as, id, key, beh) +} + +// TestRouterNewRouter verifies that a new router can be created as expected. +func TestRouterNewRouter(t *testing.T) { + t.Parallel() + h := newRouterTestHarness(t) + + key := NewServiceKey[*testMsg, string]("router-service") + strategy := NewRoundRobinStrategy[*testMsg, string]() + + router := NewRouter(h.receptionist, key, strategy, h.dlo.Ref()) + require.NotNil(t, router, "newRouter should not return nil") + require.Equal(t, "router(router-service)", router.ID(), "router ID mismatch") +} + +// countingEchoBehavior is an echo behavior that also counts how many messages +// it has processed. +type countingEchoBehavior struct { + *echoBehavior + id string + processedMsgs int64 +} + +func newCountingEchoBehavior(t *testing.T, id string) *countingEchoBehavior { + return &countingEchoBehavior{ + echoBehavior: newEchoBehavior(t, 0), + id: id, + } +} + +func (b *countingEchoBehavior) Receive(ctx context.Context, + msg *testMsg) fn.Result[string] { + + atomic.AddInt64(&b.processedMsgs, 1) + + // Include actor ID in reply for easier verification. + res := b.echoBehavior.Receive(ctx, msg) + val, err := res.Unpack() + if err == nil { + return fn.Ok(fmt.Sprintf("%s:%s", b.id, val)) + } + return res +} + +// TestRouterTellAndAskRoundRobin verifies that the router distributes messages +// in a round robin properly. +func TestRouterTellAndAskRoundRobin(t *testing.T) { + t.Parallel() + h := newRouterTestHarness(t) + + // Make a new router for the given service key and round robin strategy. + serviceKey := NewServiceKey[*testMsg, string]("rr-service") + strategy := NewRoundRobinStrategy[*testMsg, string]() + router := NewRouter(h.receptionist, serviceKey, strategy, h.dlo.Ref()) + + // We'll now register two actors with the router, each with a different + // service key. + actor1Beh := newCountingEchoBehavior(t, "actor1") + actor2Beh := newCountingEchoBehavior(t, "actor2") + _ = h.newRouterTargetActor("actor1-rr", serviceKey, actor1Beh) + _ = h.newRouterTargetActor("actor2-rr", serviceKey, actor2Beh) + + // Nxet, we'll send a mix of Tell and Ask messages to the router. + numMessages := 6 + for i := 0; i < numMessages; i++ { + msgData := fmt.Sprintf("message-%d", i) + if i%2 == 0 { + router.Tell(context.Background(), newTestMsg(msgData)) + } else { + future := router.Ask( + context.Background(), newTestMsg(msgData), + ) + ctxAwait, cancelAwait := context.WithTimeout( + context.Background(), time.Second, + ) + + result := future.Await(ctxAwait) + cancelAwait() + require.False( + t, result.IsErr(), "ask failed: %v", result.Err(), + ) + } + } + + // Wait a bit for Tell messages to be processed. + time.Sleep(100 * time.Millisecond) + + // Each actor should have processed numMessages / 2 messages. + require.EqualValues( + t, numMessages/2, atomic.LoadInt64(&actor1Beh.processedMsgs), + "actor1 processed message count mismatch", + ) + require.EqualValues( + t, numMessages/2, atomic.LoadInt64(&actor2Beh.processedMsgs), + "actor2 processed message count mismatch", + ) + + // Router's DLO should be empty. + h.assertNoDLOMessages() +} + +// TestRouterNoActorsAvailable verifies that if no actors are available for the +// message, then an error is returned. +func TestRouterNoActorsAvailable(t *testing.T) { + t.Parallel() + h := newRouterTestHarness(t) + + serviceKey := NewServiceKey[*testMsg, string]("no-actor-service") + strategy := NewRoundRobinStrategy[*testMsg, string]() + router := NewRouter(h.receptionist, serviceKey, strategy, h.dlo.Ref()) + + // We'll send a message, then assert that it goes to the DLO. + tellMsg := newTestMsg("tell-no-actor") + router.Tell(context.Background(), tellMsg) + h.assertDLOMessage(tellMsg) + + // If we use an ask instead, then we should get an error. + askMsg := newTestMsg("ask-no-actor") + future := router.Ask(context.Background(), askMsg) + result := future.Await(context.Background()) + + require.True( + t, result.IsErr(), "ask should fail when no actors are available", + ) + require.ErrorIs(t, result.Err(), ErrNoActorsAvailable, "error mismatch") +} + +// TestRouterTellAskContextCancellation verifies that if the context is +// canceled, then sending aborts. +func TestRouterTellAskContextCancellation(t *testing.T) { + t.Parallel() + h := newRouterTestHarness(t) + + serviceKey := NewServiceKey[*testMsg, string]("ctx-cancel-service") + strategy := NewRoundRobinStrategy[*testMsg, string]() + router := NewRouter(h.receptionist, serviceKey, strategy, h.dlo.Ref()) + + // Use a regular echo actor, but we'll control context for Tell/Ask. + targetActorBeh := newEchoBehavior(t, 50*time.Millisecond) + _ = h.newRouterTargetActor("target-ctx", serviceKey, targetActorBeh) + + // Next, we'll send a Tell message with a context that will be cancelled + // before we even send. + ctxTell, cancelTell := context.WithCancel(context.Background()) + cancelTell() + router.Tell(ctxTell, newTestMsg("tell-ctx-cancelled")) + + // The Message should be dropped by actorRefImpl.Tell if ctx is + // cancelled. Router's DLO should not receive it from this path. + h.assertNoDLOMessages() + + // Next, we'll do the same for Ask. This time, we should get an error. + ctxAsk, cancelAsk := context.WithCancel(context.Background()) + cancelAsk() + futureAsk := router.Ask(ctxAsk, newTestMsg("ask-ctx-cancelled")) + resultAsk := futureAsk.Await(context.Background()) + + require.True( + t, resultAsk.IsErr(), "ask with cancelled context should fail", + ) + require.ErrorIs( + t, resultAsk.Err(), context.Canceled, + "error should be context.Canceled", + ) +} + +// TestRouterDynamicActorRegistration tests that we're able to dynamically add +// and remove actors from the router. +func TestRouterDynamicActorRegistration(t *testing.T) { + t.Parallel() + h := newRouterTestHarness(t) + + serviceKey := NewServiceKey[*testMsg, string]("dynamic-service") + strategy := NewRoundRobinStrategy[*testMsg, string]() + router := NewRouter(h.receptionist, serviceKey, strategy, h.dlo.Ref()) + + // If we try to send a mesasge to the router before any actors are + // added, we should get an error. + futureNoActor := router.Ask(context.Background(), newTestMsg("ping-no-actors")) + resNoActor := futureNoActor.Await(context.Background()) + require.ErrorIs(t, resNoActor.Err(), ErrNoActorsAvailable) + + actor1Beh := newCountingEchoBehavior(t, "actor1") + actor1Ref := h.newRouterTargetActor("actor1-dynamic", serviceKey, actor1Beh) + + // At this point, we have a new actor added, but we'll try to send a + // message to a different actor ID. This should go to the router's DLO. + futureActor1 := router.Ask(context.Background(), newTestMsg("ping-actor1")) + ctxAwaitA1, cancelAwaitA1 := context.WithTimeout(context.Background(), time.Second) + resActor1 := futureActor1.Await(ctxAwaitA1) + cancelAwaitA1() + require.False(t, resActor1.IsErr(), "ask to actor1 failed: %v", resActor1.Err()) + resActor1.WhenOk(func(s string) { + require.Equal(t, "actor1:echo: ping-actor1", s) + }) + + actor2Beh := newCountingEchoBehavior(t, "actor2") + actor2Ref := h.newRouterTargetActor( + "actor2-dynamic", serviceKey, actor2Beh, + ) + + // Now that we've added two actors above, we should round robin between + // them when sending. + ctxAwaitDA1, cancelAwaitDA1 := context.WithTimeout( + context.Background(), time.Second, + ) + router.Ask(context.Background(), newTestMsg("dynamic-ask1")).Await( + ctxAwaitDA1, + ) + cancelAwaitDA1() + + ctxAwaitDA2, cancelAwaitDA2 := context.WithTimeout(context.Background(), time.Second) + router.Ask(context.Background(), newTestMsg("dynamic-ask2")).Await(ctxAwaitDA2) + cancelAwaitDA2() + + time.Sleep(50 * time.Millisecond) + + // actor1 should have processed 2 messages (ping-actor1, dynamic-ask1), + require.EqualValues(t, 2, atomic.LoadInt64(&actor1Beh.processedMsgs)) + require.EqualValues(t, 1, atomic.LoadInt64(&actor2Beh.processedMsgs)) + + // Next, we'll unregister the first actor ref. + unregistered := UnregisterFromReceptionist( + h.receptionist, serviceKey, actor1Ref, + ) + require.True(t, unregistered) + + // All the messages should now go to the second actor. + for i := 0; i < 2; i++ { + msgData := fmt.Sprintf("to-actor2-%d", i) + future := router.Ask(context.Background(), newTestMsg(msgData)) + ctxAwaitLoop, cancelAwaitLoop := context.WithTimeout( + context.Background(), time.Second, + ) + + res := future.Await(ctxAwaitLoop) + cancelAwaitLoop() + + require.False( + t, res.IsErr(), "ask to actor2 failed: %v", res.Err(), + ) + res.WhenOk(func(s string) { + require.Equal(t, "actor2:echo: "+msgData, s) + }) + } + + // Actor 1 shouldn't have got any of the messages, they should go to + // actor 2. + require.EqualValues(t, 2, atomic.LoadInt64(&actor1Beh.processedMsgs)) + require.EqualValues(t, 1+2, atomic.LoadInt64(&actor2Beh.processedMsgs)) + + // Next, we'll unregister the second actor ref. + unregistered2 := UnregisterFromReceptionist( + h.receptionist, serviceKey, actor2Ref, + ) + require.True(t, unregistered2) + + // If we try to send another message, it should go to the DL. + tellMsg := newTestMsg("dynamic-tell-no-actors") + router.Tell(context.Background(), tellMsg) + h.assertDLOMessage(tellMsg) +} diff --git a/baselib/example/example_actors.go b/baselib/example/example_actors.go new file mode 100644 index 000000000..4824cc76e --- /dev/null +++ b/baselib/example/example_actors.go @@ -0,0 +1,95 @@ +package baselib_test + +import ( + "context" + "fmt" + + "github.com/lightninglabs/darepo-client/baselib/actor" + "github.com/lightninglabs/darepo-client/baselib/protofsm" + "github.com/lightningnetwork/lnd/fn/v2" +) + +// ============================================================================ +// Actor Services +// ============================================================================ + +// ReviewService handles document review requests. + +// ReviewServiceKey is the service key for the review service. +var ReviewServiceKey = actor.NewServiceKey[ReviewMsg, ReviewResp]("review-service") + +// ReviewMsg requests document review. +type ReviewMsg struct { + actor.BaseMessage + DocumentID string + Author string + ReplyTo actor.TellOnlyRef[protofsm.ActorMessage[DocEvent]] +} + +// MessageType returns the message type. +func (m ReviewMsg) MessageType() string { + return "ReviewRequest" +} + +// ReviewResp is the response from review service. +type ReviewResp struct { + Success bool +} + +// ReviewServiceBehavior simulates async review process. +type ReviewServiceBehavior struct{} + +// Receive processes review requests. +func (r *ReviewServiceBehavior) Receive(ctx context.Context, + msg ReviewMsg) fn.Result[ReviewResp] { + + fmt.Printf("ReviewService: Processing document %s by %s\n", + msg.DocumentID, msg.Author) + + // Send confirmation that review started. + msg.ReplyTo.Tell(ctx, protofsm.ActorMessage[DocEvent]{ + Event: EventReviewStarted{}, + }) + + // Simulate review decision (approve in this example). + msg.ReplyTo.Tell(ctx, protofsm.ActorMessage[DocEvent]{ + Event: EventApproved{ + Reviewer: "ReviewBot", + }, + }) + + return fn.Ok(ReviewResp{Success: true}) +} + +// NotificationService handles notification delivery. + +// NotifyServiceKey is the service key for the notification service. +var NotifyServiceKey = actor.NewServiceKey[NotifyMsg, NotifyResp]("notify-service") + +// NotifyMsg requests notification delivery. +type NotifyMsg struct { + actor.BaseMessage + Message string +} + +// MessageType returns the message type. +func (m NotifyMsg) MessageType() string { + return "Notify" +} + +// NotifyResp is the response from notification service. +type NotifyResp struct { + Success bool +} + +// NotificationServiceBehavior delivers notifications. +type NotificationServiceBehavior struct{} + +// Receive processes notification requests. +func (n *NotificationServiceBehavior) Receive(ctx context.Context, + msg NotifyMsg) fn.Result[NotifyResp] { + + fmt.Printf("NotificationService: %s\n", msg.Message) + + return fn.Ok(NotifyResp{Success: true}) +} diff --git a/baselib/example/example_protofsm.go b/baselib/example/example_protofsm.go new file mode 100644 index 000000000..97b2f93a9 --- /dev/null +++ b/baselib/example/example_protofsm.go @@ -0,0 +1,300 @@ +package baselib_test + +import ( + "context" + "fmt" + + "github.com/lightninglabs/darepo-client/baselib/actor" + "github.com/lightninglabs/darepo-client/baselib/protofsm" + "github.com/lightningnetwork/lnd/fn/v2" +) + +// This file contains an example document approval workflow implementation +// that demonstrates how to integrate a state machine with the actor system. +// +// The workflow models: Init -> AwaitingReview -> Approved/Rejected + +// ============================================================================ +// Event Types (Sealed Interface) +// ============================================================================ + +// DocEvent represents all possible events in the document workflow FSM. +type DocEvent interface { + isDocEventSealed() +} + +// EventSubmitDocument is sent to start the approval workflow. +type EventSubmitDocument struct { + DocumentID string + Author string +} + +func (EventSubmitDocument) isDocEventSealed() {} + +// EventReviewStarted is sent when the review service accepts the request. +type EventReviewStarted struct{} + +func (EventReviewStarted) isDocEventSealed() {} + +// EventApproved is sent when the document is approved. +type EventApproved struct { + Reviewer string +} + +func (EventApproved) isDocEventSealed() {} + +// EventRejected is sent when the document is rejected. +type EventRejected struct { + Reviewer string + Reason string +} + +func (EventRejected) isDocEventSealed() {} + +// EventResume is sent when resuming from storage. +type EventResume struct{} + +func (EventResume) isDocEventSealed() {} + +// ============================================================================ +// Outbox Events (Routed to Actor Services) +// ============================================================================ + +// DocOutboxEvent is the sealed interface for events routed to actors. +type DocOutboxEvent interface { + protofsm.ActorOutboxEvent + isDocOutboxEventSealed() +} + +// OutboxRequestReview requests document review from ReviewService. +type OutboxRequestReview struct { + protofsm.RoutedOutboxEvent[ReviewMsg, ReviewResp] +} + +// NewOutboxRequestReview creates a new review request outbox event. +func NewOutboxRequestReview(documentID string, author string, + fsmRef actor.TellOnlyRef[protofsm.ActorMessage[DocEvent]]) OutboxRequestReview { + + return OutboxRequestReview{ + RoutedOutboxEvent: protofsm.NewTellOutboxEvent( + ReviewServiceKey, + ReviewMsg{ + DocumentID: documentID, + Author: author, + ReplyTo: fsmRef, + }, + ), + } +} + +func (OutboxRequestReview) isDocOutboxEventSealed() {} + +// OutboxNotify sends notification via NotificationService. +type OutboxNotify struct { + protofsm.RoutedOutboxEvent[NotifyMsg, NotifyResp] +} + +// NewOutboxNotify creates a new notification outbox event. +func NewOutboxNotify(message string) OutboxNotify { + return OutboxNotify{ + RoutedOutboxEvent: protofsm.NewTellOutboxEvent( + NotifyServiceKey, + NotifyMsg{Message: message}, + ), + } +} + +func (OutboxNotify) isDocOutboxEventSealed() {} + +// ============================================================================ +// State Types +// ============================================================================ + +// DocState represents all possible states in the document workflow FSM. +type DocState interface { + protofsm.State[DocEvent, DocOutboxEvent, *DocEnvironment] + isDocStateSealed() +} + +// DocEnvironment holds the FSM execution context. +type DocEnvironment struct { + actorRef actor.TellOnlyRef[protofsm.ActorMessage[DocEvent]] +} + +// SetTellOnlyRef sets the actor reference for the environment. +func (e *DocEnvironment) SetTellOnlyRef(ref actor.TellOnlyRef[protofsm.ActorMessage[DocEvent]]) { + e.actorRef = ref +} + +// GetTellOnlyRef returns the actor reference from the environment. +func (e *DocEnvironment) GetTellOnlyRef() actor.TellOnlyRef[protofsm.ActorMessage[DocEvent]] { + return e.actorRef +} + +// Compile-time check for TellRefEnv. +var _ protofsm.TellRefEnv[DocEvent] = (*DocEnvironment)(nil) + +// StateInit is the initial state. +type StateInit struct{} + +func (StateInit) isDocStateSealed() {} + +func (StateInit) IsTerminal() bool { + return false +} + +func (StateInit) String() string { + return "Init" +} + +// ProcessEvent processes events in the Init state. +func (s *StateInit) ProcessEvent(ctx context.Context, event DocEvent, + env *DocEnvironment) (*protofsm.StateTransition[DocEvent, DocOutboxEvent, *DocEnvironment], error) { + + switch e := event.(type) { + case EventSubmitDocument: + fmt.Printf("Document %s submitted by %s\n", e.DocumentID, e.Author) + + nextState := &StateAwaitingReview{ + documentID: e.DocumentID, + author: e.Author, + } + + return &protofsm.StateTransition[DocEvent, DocOutboxEvent, *DocEnvironment]{ + NextState: nextState, + NewEvents: fn.Some(protofsm.EmittedEvent[DocEvent, DocOutboxEvent]{ + Outbox: []DocOutboxEvent{ + NewOutboxRequestReview( + e.DocumentID, e.Author, env.actorRef, + ), + }, + }), + }, nil + + default: + return nil, fmt.Errorf("unexpected event %T in StateInit", e) + } +} + +// StateAwaitingReview waits for review decision. +type StateAwaitingReview struct { + documentID string + author string +} + +func (StateAwaitingReview) isDocStateSealed() {} + +func (StateAwaitingReview) IsTerminal() bool { + return false +} + +func (StateAwaitingReview) String() string { + return "AwaitingReview" +} + +// ProcessEvent processes events in the AwaitingReview state. +func (s *StateAwaitingReview) ProcessEvent(ctx context.Context, event DocEvent, + env *DocEnvironment) (*protofsm.StateTransition[DocEvent, DocOutboxEvent, *DocEnvironment], error) { + + switch e := event.(type) { + case EventResume: + // Re-emit review request on resume. + return &protofsm.StateTransition[DocEvent, DocOutboxEvent, *DocEnvironment]{ + NextState: s, + NewEvents: fn.Some(protofsm.EmittedEvent[DocEvent, DocOutboxEvent]{ + Outbox: []DocOutboxEvent{ + NewOutboxRequestReview( + s.documentID, s.author, env.actorRef, + ), + }, + }), + }, nil + + case EventReviewStarted: + fmt.Printf("Review started for document %s\n", s.documentID) + + // Stay in same state, just acknowledge. + return &protofsm.StateTransition[DocEvent, DocOutboxEvent, *DocEnvironment]{ + NextState: s, + }, nil + + case EventApproved: + fmt.Printf("Document %s approved by %s\n", s.documentID, e.Reviewer) + + nextState := &StateApproved{} + notifyMsg := fmt.Sprintf( + "Document %s approved by %s", s.documentID, e.Reviewer, + ) + + return &protofsm.StateTransition[DocEvent, DocOutboxEvent, *DocEnvironment]{ + NextState: nextState, + NewEvents: fn.Some(protofsm.EmittedEvent[DocEvent, DocOutboxEvent]{ + Outbox: []DocOutboxEvent{ + NewOutboxNotify(notifyMsg), + }, + }), + }, nil + + case EventRejected: + fmt.Printf("Document %s rejected by %s: %s\n", + s.documentID, e.Reviewer, e.Reason) + + nextState := &StateRejected{} + notifyMsg := fmt.Sprintf( + "Document %s rejected by %s: %s", + s.documentID, e.Reviewer, e.Reason, + ) + + return &protofsm.StateTransition[DocEvent, DocOutboxEvent, *DocEnvironment]{ + NextState: nextState, + NewEvents: fn.Some(protofsm.EmittedEvent[DocEvent, DocOutboxEvent]{ + Outbox: []DocOutboxEvent{ + NewOutboxNotify(notifyMsg), + }, + }), + }, nil + + default: + return nil, fmt.Errorf("unexpected event %T in StateAwaitingReview", e) + } +} + +// StateApproved is a terminal state. +type StateApproved struct{} + +func (StateApproved) isDocStateSealed() {} + +func (StateApproved) IsTerminal() bool { + return true +} + +func (StateApproved) String() string { + return "Approved" +} + +// ProcessEvent processes events in the Approved state. +func (s *StateApproved) ProcessEvent(ctx context.Context, event DocEvent, + env *DocEnvironment) (*protofsm.StateTransition[DocEvent, DocOutboxEvent, *DocEnvironment], error) { + + return nil, fmt.Errorf("no events expected in terminal state") +} + +// StateRejected is a terminal state. +type StateRejected struct{} + +func (StateRejected) isDocStateSealed() {} + +func (StateRejected) IsTerminal() bool { + return true +} + +func (StateRejected) String() string { + return "Rejected" +} + +// ProcessEvent processes events in the Rejected state. +func (s *StateRejected) ProcessEvent(ctx context.Context, event DocEvent, + env *DocEnvironment) (*protofsm.StateTransition[DocEvent, DocOutboxEvent, *DocEnvironment], error) { + + return nil, fmt.Errorf("no events expected in terminal state") +} diff --git a/baselib/example/example_test.go b/baselib/example/example_test.go new file mode 100644 index 000000000..43c49eb18 --- /dev/null +++ b/baselib/example/example_test.go @@ -0,0 +1,106 @@ +package baselib_test + +import ( + "context" + "fmt" + "time" + + "github.com/btcsuite/btclog/v2" + "github.com/lightninglabs/darepo-client/baselib/actor" + "github.com/lightninglabs/darepo-client/baselib/protofsm" +) + +// ExampleActorStateMachine demonstrates integrating a state machine with +// actor services. The FSM emits outbox events that are routed to the +// ReviewService and NotificationService actors. +func ExampleActorStateMachine() { + ctx := context.Background() + + // Create actor system. + system := actor.NewActorSystemWithConfig(actor.SystemConfig{ + MailboxCapacity: 10, + }) + defer func() { + _ = system.Shutdown() + }() + + // Register ReviewService actor. + reviewBehavior := &ReviewServiceBehavior{} + actor.RegisterWithSystem( + system, "review-service", ReviewServiceKey, reviewBehavior, + ) + + // Register NotificationService actor. + notifyBehavior := &NotificationServiceBehavior{} + actor.RegisterWithSystem( + system, "notify-service", NotifyServiceKey, notifyBehavior, + ) + + // Create FSM environment. + env := &DocEnvironment{} + + // Create FSM config. + cfg := protofsm.StateMachineCfg[DocEvent, DocOutboxEvent, *DocEnvironment]{ + Logger: btclog.Disabled, + InitialState: &StateInit{}, + Env: env, + } + + // Spawn FSM as actor. + fsmRef := protofsm.NewSystemsActorStateMachine( + ctx, cfg, system, "document-workflow", + ) + + // Submit a document for review. + fmt.Println("=== Submitting Document ===") + resp1 := fsmRef.Ask(ctx, protofsm.ActorMessage[DocEvent]{ + Event: EventSubmitDocument{ + DocumentID: "DOC-123", + Author: "Alice", + }, + }).Await(ctx) + + if resp1.IsErr() { + fmt.Printf("Error: %v\n", resp1.Err()) + return + } + + // Give async actors time to process the full workflow: + // 1. ReviewService receives request + // 2. ReviewService sends EventReviewStarted back to FSM + // 3. ReviewService sends EventApproved back to FSM + // 4. FSM transitions to Approved and emits OutboxNotify + // 5. NotificationService receives and processes notification + fmt.Println("\n=== Processing Review ===") + time.Sleep(300 * time.Millisecond) + + // Query current state (using StateQuery flag). + fmt.Println("\n=== Checking Final State ===") + resp2 := fsmRef.Ask(ctx, protofsm.ActorMessage[DocEvent]{ + StateQuery: true, + }).Await(ctx) + + if resp2.IsOk() { + state2, _ := resp2.Unpack() + fmt.Printf("Final state: %s\n", state2.CurrentState.String()) + fmt.Printf("Is terminal: %v\n", state2.CurrentState.IsTerminal()) + } + + fmt.Println("\n=== Workflow Complete ===") + + // Output: + // === Submitting Document === + // Document DOC-123 submitted by Alice + // + // === Processing Review === + // ReviewService: Processing document DOC-123 by Alice + // Review started for document DOC-123 + // Document DOC-123 approved by ReviewBot + // NotificationService: Document DOC-123 approved by ReviewBot + // + // === Checking Final State === + // Final state: Approved + // Is terminal: true + // + // === Workflow Complete === +} diff --git a/baselib/go.mod b/baselib/go.mod new file mode 100644 index 000000000..5f25cab72 --- /dev/null +++ b/baselib/go.mod @@ -0,0 +1,174 @@ +module github.com/lightninglabs/darepo-client/baselib + +require ( + github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 + github.com/btcsuite/btcd/btcec/v2 v2.3.4 + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 + github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b + github.com/lightningnetwork/lnd v0.20.0-beta + github.com/lightningnetwork/lnd/fn/v2 v2.0.9 + github.com/stretchr/testify v1.10.0 + pgregory.net/rapid v1.2.0 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/aead/siphash v1.0.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/btcsuite/btcd/btcutil v1.1.5 // indirect + github.com/btcsuite/btcd/btcutil/psbt v1.1.8 // indirect + github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c // indirect + github.com/btcsuite/btcwallet v0.16.17 // indirect + github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 // indirect + github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 // indirect + github.com/btcsuite/btcwallet/wallet/txsizes v1.2.5 // indirect + github.com/btcsuite/btcwallet/walletdb v1.5.1 // indirect + github.com/btcsuite/btcwallet/wtxmgr v1.5.6 // indirect + github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect + github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect + github.com/btcsuite/winsvc v1.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/containerd/continuity v0.3.0 // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/decred/dcrd/lru v1.1.2 // indirect + github.com/docker/cli v28.1.1+incompatible // indirect + github.com/docker/docker v28.1.1+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fergusstrange/embedded-postgres v1.25.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.3.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang-migrate/migrate/v4 v4.17.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.14.3 // indirect + github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgtype v1.14.4 // indirect + github.com/jackc/pgx/v4 v4.18.3 // indirect + github.com/jackc/pgx/v5 v5.7.4 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jessevdk/go-flags v1.4.0 // indirect + github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/jrick/logrotate v1.1.2 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/kkdai/bstream v1.0.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect + github.com/lightninglabs/neutrino v0.16.1 // indirect + github.com/lightninglabs/neutrino/cache v1.1.2 // indirect + github.com/lightningnetwork/lnd/clock v1.1.1 // indirect + github.com/lightningnetwork/lnd/healthcheck v1.2.6 // indirect + github.com/lightningnetwork/lnd/kvdb v1.4.16 // indirect + github.com/lightningnetwork/lnd/queue v1.1.1 // indirect + github.com/lightningnetwork/lnd/sqldb v1.0.11 // indirect + github.com/lightningnetwork/lnd/ticker v1.1.1 // indirect + github.com/lightningnetwork/lnd/tlv v1.3.2 // indirect + github.com/lightningnetwork/lnd/tor v1.1.6 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/miekg/dns v1.1.43 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.14 // indirect + github.com/ory/dockertest/v3 v3.10.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.11.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.26.0 // indirect + github.com/prometheus/procfs v0.6.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sirupsen/logrus v1.9.2 // indirect + github.com/soheilhy/cmux v0.1.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect + go.etcd.io/bbolt v1.4.3 // indirect + go.etcd.io/etcd/api/v3 v3.5.12 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect + go.etcd.io/etcd/client/v2 v2.305.12 // indirect + go.etcd.io/etcd/client/v3 v3.5.12 // indirect + go.etcd.io/etcd/pkg/v3 v3.5.12 // indirect + go.etcd.io/etcd/raft/v3 v3.5.12 // indirect + go.etcd.io/etcd/server/v3 v3.5.12 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.49.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.29.10 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect +) + +go 1.24.9 diff --git a/baselib/go.sum b/baselib/go.sum new file mode 100644 index 000000000..e86c3569a --- /dev/null +++ b/baselib/go.sum @@ -0,0 +1,794 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 h1:8n9k3I7e8DkpdQ5YAP4j8ly/LSsbe6qX9vmVbrUGvVw= +github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6/go.mod h1:OmM4kFtB0klaG/ZqT86rQiyw/1iyXlJgc3UHClPhhbs= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= +github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= +github.com/btcsuite/btcd/btcutil/psbt v1.1.8 h1:4voqtT8UppT7nmKQkXV+T9K8UyQjKOn2z/ycpmJK8wg= +github.com/btcsuite/btcd/btcutil/psbt v1.1.8/go.mod h1:kA6FLH/JfUx++j9pYU0pyu+Z8XGBQuuTmuKYUf6q7/U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c h1:4HxD1lBUGUddhzgaNgrCPsFWd7cGYNpeFUgd9ZIgyM0= +github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= +github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b h1:MQ+Q6sDy37V1wP1Yu79A5KqJutolqUGwA99UZWQDWZM= +github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcwallet v0.16.17 h1:1N6lHznRdcjDopBvcofxaIHknArkJ/EcVKgLKfGL4Dg= +github.com/btcsuite/btcwallet v0.16.17/go.mod h1:YO+W745BAH8n/Rpgj68QsLR6eLlgM4W2do4RejT0buo= +github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 h1:Rr0njWI3r341nhSPesKQ2JF+ugDSzdPoeckS75SeDZk= +github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5/go.mod h1:+tXJ3Ym0nlQc/iHSwW1qzjmPs3ev+UVWMbGgfV1OZqU= +github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 h1:YEO+Lx1ZJJAtdRrjuhXjWrYsmAk26wLTlNzxt2q0lhk= +github.com/btcsuite/btcwallet/wallet/txrules v1.2.2/go.mod h1:4v+grppsDpVn91SJv+mZT7B8hEV4nSmpREM4I8Uohws= +github.com/btcsuite/btcwallet/wallet/txsizes v1.2.5 h1:93o5Xz9dYepBP4RMFUc9RGIFXwqP2volSWRkYJFrNtI= +github.com/btcsuite/btcwallet/wallet/txsizes v1.2.5/go.mod h1:lQ+e9HxZ85QP7r3kdxItkiMSloSLg1PEGis5o5CXUQw= +github.com/btcsuite/btcwallet/walletdb v1.5.1 h1:HgMhDNCrtEFPC+8q0ei5DQ5U9Tl4RCspA22DEKXlopI= +github.com/btcsuite/btcwallet/walletdb v1.5.1/go.mod h1:jk/hvpLFINF0C1kfTn0bfx2GbnFT+Nvnj6eblZALfjs= +github.com/btcsuite/btcwallet/wtxmgr v1.5.6 h1:Zwvr/rrJYdOLqdBCSr4eICEstnEA+NBUvjIWLkrXaYI= +github.com/btcsuite/btcwallet/wtxmgr v1.5.6/go.mod h1:lzVbDkk/jRao2ib5kge46aLZW1yFc8RFNycdYpnsmZA= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/decred/dcrd/lru v1.1.2 h1:KdCzlkxppuoIDGEvCGah1fZRicrDH36IipvlB1ROkFY= +github.com/decred/dcrd/lru v1.1.2/go.mod h1:gEdCVgXs1/YoBvFWt7Scgknbhwik3FgVSzlnCcXL2N8= +github.com/dhui/dktest v0.4.0 h1:z05UmuXZHO/bgj/ds2bGMBu8FI4WA+Ag/m3ghL+om7M= +github.com/dhui/dktest v0.4.0/go.mod h1:v/Dbz1LgCBOi2Uki2nUqLBGa83hWBGFMu5MrgMDCc78= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= +github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= +github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fergusstrange/embedded-postgres v1.25.0 h1:sa+k2Ycrtz40eCRPOzI7Ry7TtkWXXJ+YRsxpKMDhxK0= +github.com/fergusstrange/embedded-postgres v1.25.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= +github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= +github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= +github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= +github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/jrick/logrotate v1.1.2 h1:6ePk462NCX7TfKtNp5JJ7MbA2YIslkpfgP03TlTYMN0= +github.com/jrick/logrotate v1.1.2/go.mod h1:f9tdWggSVK3iqavGpyvegq5IhNois7KXmasU6/N96OQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= +github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= +github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= +github.com/lightninglabs/neutrino v0.16.1 h1:5Kz4ToxncEVkpKC6fwUjXKtFKJhuxlG3sBB3MdJTJjs= +github.com/lightninglabs/neutrino v0.16.1/go.mod h1:L+5UAccpUdyM7yDgmQySgixf7xmwBgJtOfs/IP26jCs= +github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g= +github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo= +github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 h1:6D3LrdagJweLLdFm1JNodZsBk6iU4TTsBBFLQ4yiXfI= +github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9/go.mod h1:EDqJ3MuZIbMq0QI1czTIKDJ/GS8S14RXPwapHw8cw6w= +github.com/lightningnetwork/lnd v0.20.0-beta h1:ML+jgJ3UKDGJdUf0m73ZeR/szJKWVtHxpQP+yFC79b8= +github.com/lightningnetwork/lnd v0.20.0-beta/go.mod h1:8hc55AnE3mMSJ/UAEJZgmhgNCcH0yWaPg0olpxhhp4M= +github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= +github.com/lightningnetwork/lnd/clock v1.1.1/go.mod h1:mGnAhPyjYZQJmebS7aevElXKTFDuO+uNFFfMXK1W8xQ= +github.com/lightningnetwork/lnd/fn/v2 v2.0.9 h1:ZytG4ltPac/sCyg1EJDn10RGzPIDJeyennUMRdOw7Y8= +github.com/lightningnetwork/lnd/fn/v2 v2.0.9/go.mod h1:aPUJHJ31S+Lgoo8I5SxDIjnmeCifqujaiTXKZqpav3w= +github.com/lightningnetwork/lnd/healthcheck v1.2.6 h1:1sWhqr93GdkWy4+6U7JxBfcyZIE78MhIHTJZfPx7qqI= +github.com/lightningnetwork/lnd/healthcheck v1.2.6/go.mod h1:Mu02um4CWY/zdTOvFje7WJgJcHyX2zq/FG3MhOAiGaQ= +github.com/lightningnetwork/lnd/kvdb v1.4.16 h1:9BZgWdDfjmHRHLS97cz39bVuBAqMc4/p3HX1xtUdbDI= +github.com/lightningnetwork/lnd/kvdb v1.4.16/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM= +github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= +github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= +github.com/lightningnetwork/lnd/sqldb v1.0.11 h1:X8J3OvdIhJVniQG78Qsp3niErl1zdGMTPvzgiLMWOOo= +github.com/lightningnetwork/lnd/sqldb v1.0.11/go.mod h1:oOdZ7vjmAUmI9He+aFHTunnxKVefHZAfJttZdz16hSg= +github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM= +github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= +github.com/lightningnetwork/lnd/tlv v1.3.2 h1:MO4FCk7F4k5xPMqVZF6Nb/kOpxlwPrUQpYjmyKny5s0= +github.com/lightningnetwork/lnd/tlv v1.3.2/go.mod h1:pJuiBj1ecr1WWLOtcZ+2+hu9Ey25aJWFIsjmAoPPnmc= +github.com/lightningnetwork/lnd/tor v1.1.6 h1:WHUumk7WgU6BUFsqHuqszI9P6nfhMeIG+rjJBlVE6OE= +github.com/lightningnetwork/lnd/tor v1.1.6/go.mod h1:qSRB8llhAK+a6kaTPWOLLXSZc6Hg8ZC0mq1sUQ/8JfI= +github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= +github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= +github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= +github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c= +go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4= +go.etcd.io/etcd/client/pkg/v3 v3.5.12 h1:EYDL6pWwyOsylrQyLp2w+HkQ46ATiOvoEdMarindU2A= +go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4= +go.etcd.io/etcd/client/v2 v2.305.12 h1:0m4ovXYo1CHaA/Mp3X/Fak5sRNIWf01wk/X1/G3sGKI= +go.etcd.io/etcd/client/v2 v2.305.12/go.mod h1:aQ/yhsxMu+Oht1FOupSr60oBvcS9cKXHrzBpDsPTf9E= +go.etcd.io/etcd/client/v3 v3.5.12 h1:v5lCPXn1pf1Uu3M4laUE2hp/geOTc5uPcYYsNe1lDxg= +go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw= +go.etcd.io/etcd/pkg/v3 v3.5.12 h1:OK2fZKI5hX/+BTK76gXSTyZMrbnARyX9S643GenNGb8= +go.etcd.io/etcd/pkg/v3 v3.5.12/go.mod h1:UVwg/QIMoJncyeb/YxvJBJCE/NEwtHWashqc8A1nj/M= +go.etcd.io/etcd/raft/v3 v3.5.12 h1:7r22RufdDsq2z3STjoR7Msz6fYH8tmbkdheGfwJNRmU= +go.etcd.io/etcd/raft/v3 v3.5.12/go.mod h1:ERQuZVe79PI6vcC3DlKBukDCLja/L7YMu29B74Iwj4U= +go.etcd.io/etcd/server/v3 v3.5.12 h1:EtMjsbfyfkwZuA2JlKOiBfuGkFCekv5H178qjXypbG8= +go.etcd.io/etcd/server/v3 v3.5.12/go.mod h1:axB0oCjMy+cemo5290/CutIjoxlfA6KVYKD1w0uue10= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 h1:PzIubN4/sjByhDRHLviCjJuweBXWFZWhghjg7cS28+M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 h1:AB/lmRny7e2pLhFEYIbl5qkDAUt2h0ZRO4wGPhZf+ik= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= +modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= +modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= +modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.29.10 h1:3u93dz83myFnMilBGCOLbr+HjklS6+5rJLx4q86RDAg= +modernc.org/sqlite v1.29.10/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/baselib/protofsm/actor_wrapper.go b/baselib/protofsm/actor_wrapper.go new file mode 100644 index 000000000..15f3e3d4c --- /dev/null +++ b/baselib/protofsm/actor_wrapper.go @@ -0,0 +1,189 @@ +package protofsm + +import ( + "context" + "fmt" + + "github.com/lightninglabs/darepo-client/baselib/actor" + "github.com/lightningnetwork/lnd/fn/v2" +) + +// ActorMessage wraps an Event, in order to create a new message that can be +// used with the actor package. +type ActorMessage[Event any] struct { + actor.BaseMessage + + // Event is the event that is being sent to the actor. + Event Event + + // StateQuery indicates querying for the current state of the FSM. + StateQuery bool +} + +// ActorResponse is the response type for FSM actor messages. +type ActorResponse[InternalEvent any, OutboxEvent any, Env any] struct { + // CurrentState is the current state of the FSM. + CurrentState State[InternalEvent, OutboxEvent, Env] +} + +// MessageType returns the type of the message. +// +// NOTE: This implements the actor.Message interface. +func (a ActorMessage[Event]) MessageType() string { + return fmt.Sprintf("ActorMessage(%T)", a.Event) +} + +// ActorOutboxEvent defines the interface that outbox events for the actor-based +// state machine must implement, the dispatch method can be used to deliver the +// event to the actor system or router. +type ActorOutboxEvent interface { + Dispatch(ctx context.Context, system *actor.ActorSystem) error +} + +// DeliveryMode indicates how the routed event should be delivered. +type DeliveryMode int + +const ( + // DeliveryModeTell indicates a fire-and-forget delivery mode. + DeliveryModeTell DeliveryMode = iota + // DeliveryModeAsk indicates an ask delivery mode, which waits for a + // response. + DeliveryModeAsk +) + +// RoutedOutboxEvent is a helper that delivers an outbox event to actors +// registered under a specific service key. +type RoutedOutboxEvent[M actor.Message, R any] struct { + key actor.ServiceKey[M, R] + msg M + mode DeliveryMode +} + +// NewTellOutboxEvent creates a fire-and-forget routed event. +func NewTellOutboxEvent[M actor.Message, R any](key actor.ServiceKey[M, R], msg M) RoutedOutboxEvent[M, R] { + return RoutedOutboxEvent[M, R]{ + key: key, + msg: msg, + mode: DeliveryModeTell, + } +} + +// NewAskOutboxEvent creates an ask routed event. +func NewAskOutboxEvent[M actor.Message, R any](key actor.ServiceKey[M, R], msg M) RoutedOutboxEvent[M, R] { + return RoutedOutboxEvent[M, R]{ + key: key, + msg: msg, + mode: DeliveryModeAsk, + } +} + +// Dispatch sends the event to the actor(s) registered under the service key. +func (e RoutedOutboxEvent[M, R]) Dispatch(ctx context.Context, system *actor.ActorSystem) error { + // Create a router for the service key. + router := actor.NewRouter( + system.Receptionist(), e.key, actor.NewRoundRobinStrategy[M, R](), + system.DeadLetters(), + ) + + switch e.mode { + case DeliveryModeTell: + router.Tell(ctx, e.msg) + return nil + + case DeliveryModeAsk: + res := router.Ask(ctx, e.msg).Await(ctx) + if _, err := res.Unpack(); err != nil { + return err + } + return nil + + default: + return fmt.Errorf("unknown delivery mode %v", e.mode) + } +} + +// ActorStateMachine is a wrapper around a state machine that implements the +// actor.Actor interface, the state machine is only driven by incoming messages. +type ActorStateMachine[InternalEvent any, OutboxEvent ActorOutboxEvent, Env any] struct { + sm *StateMachine[InternalEvent, OutboxEvent, Env] + system *actor.ActorSystem + currentState State[InternalEvent, OutboxEvent, Env] +} + +// TellRefEnv is an environment that can hold a tell-only actor reference. +// This let's the ActorStateMachine constructor set the reference after creation. +type TellRefEnv[InternalEvent any] interface { + SetTellOnlyRef(actor.TellOnlyRef[ActorMessage[InternalEvent]]) + GetTellOnlyRef() actor.TellOnlyRef[ActorMessage[InternalEvent]] +} + +// FullRefEnv is an environment that can hold a full actor reference. +// This let's the ActorStateMachine constructor set the reference after creation. +type FullRefEnv[InternalEvent any, OutboxEvent ActorOutboxEvent, Env any] interface { + SetActorRef(actor.ActorRef[ActorMessage[InternalEvent], ActorResponse[InternalEvent, OutboxEvent, Env]]) + GetActorRef() actor.ActorRef[ActorMessage[InternalEvent], ActorResponse[InternalEvent, OutboxEvent, Env]] +} + +// SystemActorsStateMachine registers a new state machine actor and returns its reference. +func NewSystemsActorStateMachine[InternalEvent any, OutboxEvent ActorOutboxEvent, Env Environment]( + ctx context.Context, cfg StateMachineCfg[InternalEvent, OutboxEvent, Env], system *actor.ActorSystem, + id string) actor.ActorRef[ActorMessage[InternalEvent], ActorResponse[InternalEvent, OutboxEvent, Env]] { + + machine := NewStateMachine(cfg) + sm := &ActorStateMachine[InternalEvent, OutboxEvent, Env]{ + sm: &machine, + system: system, + currentState: cfg.InitialState, + } + + ref := actor.RegisterWithSystem( + system, id, actor.NewServiceKey[ActorMessage[InternalEvent], ActorResponse[InternalEvent, OutboxEvent, Env]](id), + sm, + ) + extraInfo := "" + if envAny, ok := any(cfg.Env).(TellRefEnv[InternalEvent]); ok { + envAny.SetTellOnlyRef(ref) + extraInfo = "(tell ref env)" + } + if envAny, ok := any(cfg.Env).(FullRefEnv[InternalEvent, OutboxEvent, Env]); ok { + envAny.SetActorRef(ref) + extraInfo = "(full ref env)" + } + + cfg.Logger.DebugS(ctx, "Setting up FSM %s", extraInfo) + + return ref +} + +// Receive processes an incoming actor message and drives the state machine. +// This method implements the actor.Actor interface. any new outbox events +// generated by the state machine are dispatched to their targets within +// the actors actor system. +func (sm *ActorStateMachine[InternalEvent, OutboxEvent, Env]) Receive(ctx context.Context, + e ActorMessage[InternalEvent]) fn.Result[ActorResponse[InternalEvent, OutboxEvent, Env]] { + + // If this is a state query, return the current state. + if e.StateQuery { + return fn.Ok(ActorResponse[InternalEvent, OutboxEvent, Env]{ + CurrentState: sm.currentState, + }) + } + + newState, outBoxEvents, err := sm.sm.applyEvents(ctx, sm.currentState, e.Event) + if err != nil { + return fn.NewResult(ActorResponse[InternalEvent, OutboxEvent, Env]{}, err) + } + + sm.currentState = newState + + for _, out := range outBoxEvents { + if err := out.Dispatch(ctx, sm.system); err != nil { + sm.sm.cfg.ErrorReporter.ReportError(err) + return fn.NewResult(ActorResponse[InternalEvent, OutboxEvent, Env]{}, err) + } + } + + return fn.Ok(ActorResponse[InternalEvent, OutboxEvent, Env]{ + CurrentState: sm.currentState, + }) +} diff --git a/baselib/protofsm/state_machine.go b/baselib/protofsm/state_machine.go new file mode 100644 index 000000000..c166951b1 --- /dev/null +++ b/baselib/protofsm/state_machine.go @@ -0,0 +1,518 @@ +package protofsm + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/btcsuite/btclog/v2" + "github.com/lightninglabs/darepo-client/baselib/actor" + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/lnutils" +) + +const ( + // pollInterval is the interval at which we'll poll the SendWhen + // predicate if specified. + pollInterval = time.Millisecond * 100 +) + +var ( + // ErrStateMachineShutdown occurs when trying to feed an event to a + // StateMachine that has been asked to Stop. + ErrStateMachineShutdown = fmt.Errorf("StateMachine is shutting down") +) + +// EmittedEvent is a special type that can be emitted by a state transition. +// This can container internal events which are to be routed back to the state, +// or external events which are to be sent to the daemon. +type EmittedEvent[InboxEvent any, OutboxEvent any] struct { + // InternalEvent is an optional internal event that is to be routed + // back to the target state. This enables state to trigger one or many + // state transitions without a new external event. + InternalEvent []InboxEvent + + // Outbox is an optional set of events that are accumulated during event + // processing and returned to the caller for processing into the main + // state machine. This enables nested state machines to emit events that + // bubble up to their parent. + Outbox []OutboxEvent +} + +// StateTransition is a state transition type. It denotes the next state to go +// to, and also the set of events to emit. +type StateTransition[InternalEvent any, OutboxEvent any, Env Environment] struct { + // NextState is the next state to transition to. + NextState State[InternalEvent, OutboxEvent, Env] + + // NewEvents is the set of events to emit. + NewEvents fn.Option[EmittedEvent[InternalEvent, OutboxEvent]] +} + +// Environment is an abstract interface that represents the environment that +// the state machine will execute using. From the PoV of the main state machine +// executor, we just care about being able to clean up any resources that were +// allocated by the environment. +type Environment interface { +} + +// State defines an abstract state along, namely its state transition function +// that takes as input an event and an environment, and returns a state +// transition (next state, and set of events to emit). As state can also either +// be terminal, or not, a terminal event causes state execution to halt. +type State[InternalEvent any, OutboxEvent any, Env Environment] interface { + // ProcessEvent takes an event and an environment, and returns a new + // state transition. This will be iteratively called until either a + // terminal state is reached, or no further internal events are + // emitted. + ProcessEvent(ctx context.Context, event InternalEvent, env Env) ( + *StateTransition[InternalEvent, OutboxEvent, Env], error) + + // IsTerminal returns true if this state is terminal, and false + // otherwise. + IsTerminal() bool + + // String returns a human readable string that represents the state. + String() string +} + +// stateQuery is used by outside callers to query the internal state of the +// state machine. +type stateQuery[InternalEvent any, OutboxEvent any, Env Environment] struct { + // CurrentState is a channel that will be sent the current state of the + // state machine. + CurrentState chan State[InternalEvent, OutboxEvent, Env] +} + +// syncEventRequest is used to send an event to the state machine synchronously, +// waiting for the event processing to complete and returning the accumulated +// outbox events. +type syncEventRequest[InternalEvent any, OutboxEvent any] struct { + // event is the event to process. + event InternalEvent + + // promise is used to signal completion and return the accumulated + // outbox events or an error. + promise actor.Promise[[]OutboxEvent] +} + +// StateMachine represents an abstract FSM that is able to process new incoming +// events and drive a state machine to termination. This implementation uses +// type params to abstract over the types of events and environment. Events +// trigger new state transitions, that use the environment to perform some +// action. +// +// TODO(roasbeef): terminal check, daemon event execution, init? +type StateMachine[InternalEvent any, OutboxEvent any, + Env Environment] struct { + cfg StateMachineCfg[InternalEvent, OutboxEvent, Env] + + log btclog.Logger + + // events is the channel that will be used to send new events to the + // FSM. + events chan InternalEvent + + // syncEvents is the channel that will be used to send synchronous event + // requests to the FSM, returning the accumulated outbox events. + syncEvents chan syncEventRequest[InternalEvent, OutboxEvent] + + // newStateEvents is an EventDistributor that will be used to notify + // any relevant callers of new state transitions that occur. + newStateEvents *fn.EventDistributor[State[ + InternalEvent, OutboxEvent, Env]] + + // stateQuery is a channel that will be used by outside callers to + // query the internal state machine state. + stateQuery chan stateQuery[InternalEvent, OutboxEvent, Env] + + gm fn.GoroutineManager + quit chan struct{} + + // startOnce and stopOnce are used to ensure that the state machine is + // only started and stopped once. + startOnce sync.Once + stopOnce sync.Once + + // running is a flag that indicates if the state machine is currently + // running. + running atomic.Bool +} + +// ErrorReporter is an interface that's used to report errors that occur during +// state machine execution. +type ErrorReporter interface { + // ReportError is a method that's used to report an error that occurred + // during state machine execution. + ReportError(err error) +} + +// StateMachineCfg is a configuration struct that's used to create a new state +// machine. +type StateMachineCfg[InternalEvent any, OutboxEvent any, Env Environment] struct { + // Logger is used for logging. + Logger btclog.Logger + + // ErrorReporter is used to report errors that occur during state + // transitions. + ErrorReporter ErrorReporter + + // InitialState is the initial state of the state machine. + InitialState State[InternalEvent, OutboxEvent, Env] + + // Env is the environment that the state machine will use to execute. + Env Env + + // CustomPollInterval is an optional custom poll interval that can be + // used to set a quicker interval for tests. + CustomPollInterval fn.Option[time.Duration] +} + +// NewStateMachine creates a new state machine given a set of daemon adapters, +// an initial state, an environment, and an event to process as if emitted at +// the onset of the state machine. Such an event can be used to set up tracking +// state such as a txid confirmation event. +func NewStateMachine[InternalEvent any, OutboxEvent any, Env Environment]( + cfg StateMachineCfg[InternalEvent, OutboxEvent, Env]) StateMachine[ + InternalEvent, OutboxEvent, Env] { + + return StateMachine[InternalEvent, OutboxEvent, Env]{ + cfg: cfg, + log: cfg.Logger, + events: make(chan InternalEvent, 1), + syncEvents: make(chan syncEventRequest[InternalEvent, OutboxEvent], 1), + stateQuery: make(chan stateQuery[InternalEvent, OutboxEvent, Env]), + gm: *fn.NewGoroutineManager(), + newStateEvents: fn.NewEventDistributor[State[ + InternalEvent, OutboxEvent, Env]](), + quit: make(chan struct{}), + } +} + +// Start starts the state machine. This will spawn a goroutine that will drive +// the state machine to completion. +func (s *StateMachine[InternalEvent, OutboxEvent, Env]) Start(ctx context.Context) { + s.startOnce.Do(func() { + _ = s.gm.Go(ctx, func(ctx context.Context) { + s.driveMachine(ctx) + }) + + s.running.Store(true) + }) +} + +// Stop stops the state machine. This will block until the state machine has +// reached a stopping point. +func (s *StateMachine[InternalEvent, OutboxEvent, Env]) Stop() { + s.stopOnce.Do(func() { + close(s.quit) + s.gm.Stop() + + s.running.Store(false) + }) +} + +// SendEvent sends a new event to the state machine. +// +// TODO(roasbeef): bool if processed? +func (s *StateMachine[InternalEvent, OutboxEvent, Env]) SendEvent(ctx context.Context, + event InternalEvent) { + s.log.Debugf("Sending event %T", event) + + select { + case s.events <- event: + case <-ctx.Done(): + return + case <-s.quit: + return + } +} + +// AskEvent sends a new event to the state machine using the Ask pattern +// (request-response), waiting for the event to be fully processed. It +// returns a Future that will be resolved with the accumulated outbox events +// from all state transitions triggered by this event, including nested +// internal events. The Future's Await method will return fn.Result[[]OutboxEvent] +// containing either the accumulated outbox events or an error if processing +// failed. +func (s *StateMachine[InternalEvent, OutboxEvent, Env]) AskEvent( + ctx context.Context, event InternalEvent) actor.Future[[]OutboxEvent] { + + s.log.Debugf("Asking event %T", event) + + // Create a promise to signal completion and return results. + promise := actor.NewPromise[[]OutboxEvent]() + + req := syncEventRequest[InternalEvent, OutboxEvent]{ + event: event, + promise: promise, + } + + // Check for context cancellation or shutdown first to avoid races. + select { + case <-ctx.Done(): + promise.Complete( + fn.Errf[[]OutboxEvent]("context cancelled: %w", + ctx.Err()), + ) + + return promise.Future() + + case <-s.quit: + promise.Complete(fn.Err[[]OutboxEvent](ErrStateMachineShutdown)) + + return promise.Future() + + default: + } + + // Send the request to the state machine. If we can't send it due to + // context cancellation or shutdown, complete the promise with an error. + select { + // Successfully sent, the promise will be completed by driveMachine. + case s.syncEvents <- req: + + case <-ctx.Done(): + promise.Complete(fn.Errf[[]OutboxEvent]("context cancelled: %w", + ctx.Err())) + + case <-s.quit: + promise.Complete(fn.Err[[]OutboxEvent](ErrStateMachineShutdown)) + } + + return promise.Future() +} + +// Receive processes a message and returns a Result containing the accumulated +// outbox events from the state machine. The provided context is the actor's +// internal context, which can be used to detect actor shutdown requests. +// +// This method uses the AskEvent pattern to wait for the event to be fully +// processed and collect any outbox events emitted during state transitions. +// This enables the actor system to propagate events from nested state machines +// up through the actor hierarchy. +// +// NOTE: This implements the actor.ActorBehavior interface. +func (s *StateMachine[InternalEvent, OutboxEvent, Env]) Receive( + ctx context.Context, e ActorMessage[InternalEvent]) fn.Result[[]OutboxEvent] { + + // Use AskEvent to process the event and get the outbox events back. + future := s.AskEvent(ctx, e.Event) + + // Await the result which will contain the accumulated outbox events + // from all state transitions triggered by this event. + return future.Await(ctx) +} + +// CurrentState returns the current state of the state machine. +func (s *StateMachine[InternalEvent, OutboxEvent, Env]) CurrentState() ( + State[InternalEvent, OutboxEvent, Env], error) { + + query := stateQuery[InternalEvent, OutboxEvent, Env]{ + CurrentState: make(chan State[InternalEvent, OutboxEvent, Env], 1), + } + + if !fn.SendOrQuit(s.stateQuery, query, s.quit) { + return nil, ErrStateMachineShutdown + } + + return fn.RecvOrTimeout(query.CurrentState, time.Second) +} + +// StateSubscriber represents an active subscription to be notified of new +// state transitions. +type StateSubscriber[InternalEvent any, OutboxEvent any, Env Environment] *fn.EventReceiver[State[InternalEvent, OutboxEvent, Env]] + +// RegisterStateEvents registers a new event listener that will be notified of +// new state transitions. +func (s *StateMachine[InternalEvent, OutboxEvent, Env]) RegisterStateEvents() StateSubscriber[ + InternalEvent, OutboxEvent, Env] { + + subscriber := fn.NewEventReceiver[State[InternalEvent, OutboxEvent, Env]](10) + + // TODO(roasbeef): instead give the state and the input event? + + s.newStateEvents.RegisterSubscriber(subscriber) + + return subscriber +} + +// RemoveStateSub removes the target state subscriber from the set of active +// subscribers. +func (s *StateMachine[InternalEvent, OutboxEvent, Env]) RemoveStateSub(sub StateSubscriber[ + InternalEvent, OutboxEvent, Env]) { + + _ = s.newStateEvents.RemoveSubscriber(sub) +} + +// IsRunning returns true if the state machine is currently running. +func (s *StateMachine[InternalEvent, OutboxEvent, Env]) IsRunning() bool { + return s.running.Load() +} + +// applyEvents applies a new event to the state machine. This will continue +// until no further events are emitted by the state machine. Along the way, +// we'll also ensure to execute any daemon events that are emitted. The +// function returns the final state, any accumulated outbox events, and an +// error if one occurred. +func (s *StateMachine[InternalEvent, OutboxEvent, Env]) applyEvents( + ctx context.Context, currentState State[InternalEvent, OutboxEvent, Env], + newEvent InternalEvent) (State[InternalEvent, OutboxEvent, Env], + []OutboxEvent, error) { + + eventQueue := fn.NewQueue(newEvent) + + // outbox accumulates all outbox events from state transitions during + // the entire event processing chain. + var outbox []OutboxEvent + + // Given the next event to handle, we'll process the event, then add + // any new emitted internal events to our event queue. This continues + // until we reach a terminal state, or we run out of internal events to + // process. + // + //nolint:ll + for nextEvent := eventQueue.Dequeue(); nextEvent.IsSome(); nextEvent = eventQueue.Dequeue() { + err := fn.MapOptionZ(nextEvent, func(event InternalEvent) error { + s.log.DebugS(ctx, "Processing event", + "event", lnutils.SpewLogClosure(event)) + + // Apply the state transition function of the current + // state given this new event and our existing env. + transition, err := currentState.ProcessEvent( + ctx, event, s.cfg.Env, + ) + if err != nil { + return err + } + + newEvents := transition.NewEvents + err = fn.MapOptionZ(newEvents, func(events EmittedEvent[ + InternalEvent, OutboxEvent]) error { + // Next, we'll add any new emitted events to our + // event queue. + for _, inEvent := range events.InternalEvent { + s.log.DebugS(ctx, "Adding new internal event to queue", + "event", lnutils.SpewLogClosure(inEvent)) + + eventQueue.Enqueue(inEvent) + } + + // Accumulate any outbox events from this state + // transition. + outbox = append(outbox, events.Outbox...) + + return nil + }) + if err != nil { + return err + } + + s.log.InfoS(ctx, "State transition", + btclog.Fmt("from_state", "%v", currentState), + btclog.Fmt("to_state", "%v", transition.NextState)) + + // With our events processed, we'll now update our + // internal state. + currentState = transition.NextState + + // Notify our subscribers of the new state transition. + // + // TODO(roasbeef): will only give us the outer state? + // * let FSMs choose which state to emit? + s.newStateEvents.NotifySubscribers(currentState) + + return nil + }) + if err != nil { + return currentState, nil, err + } + } + + return currentState, outbox, nil +} + +// driveMachine is the main event loop of the state machine. It accepts any new +// incoming events, and then drives the state machine forward until it reaches +// a terminal state. +func (s *StateMachine[InternalEvent, OutboxEvent, Env]) driveMachine( + ctx context.Context) { + s.log.DebugS(ctx, "Starting state machine") + + currentState := s.cfg.InitialState + + // We just started driving the state machine, so we'll notify our + // subscribers of this starting state. + s.newStateEvents.NotifySubscribers(currentState) + + for { + select { + // We have a new external event, so we'll drive the state + // machine forward until we either run out of internal events, + // or we reach a terminal state. + case newEvent := <-s.events: + newState, _, err := s.applyEvents( + ctx, currentState, newEvent, + ) + if err != nil { + s.cfg.ErrorReporter.ReportError(err) + + s.log.ErrorS(ctx, "Unable to apply event", err) + + // An error occurred, so we'll tear down the + // entire state machine as we can't proceed. + go s.Stop() + + return + } + + currentState = newState + + // We have a synchronous event request that expects the + // accumulated outbox events to be returned via the promise. + case syncReq := <-s.syncEvents: + newState, outbox, err := s.applyEvents( + ctx, currentState, syncReq.event, + ) + if err != nil { + s.cfg.ErrorReporter.ReportError(err) + + s.log.ErrorS(ctx, "Unable to apply sync event", + err) + + // Complete the promise with the error. + // + // TODO(roasbeef): distinguish between error + // types? state vs processing + syncReq.promise.Complete(fn.Err[[]OutboxEvent](err)) + + // An error occurred, so we'll tear down the + // entire state machine as we can't proceed. + go s.Stop() + + return + } + + currentState = newState + + // Complete the promise with the accumulated outbox + // events. + syncReq.promise.Complete(fn.Ok(outbox)) + + // An outside caller is querying our state, so we'll return the + // latest state. + case stateQuery := <-s.stateQuery: + if !fn.SendOrQuit( + stateQuery.CurrentState, currentState, s.quit, + ) { + + return + } + + case <-s.gm.Done(): + return + } + } +}