actor: add BackpressureMailbox and custom mailbox support
What changed, and why it matters
This commit adds a new optional mailbox type and factory hooks to the actor framework. It is a feature/refactoring change: it lets callers choose a backpressure-aware mailbox that can deliberately drop messages under load. There is no indication of a security bug being fixed, no patch to existing behavior beyond adding options, and no disclosed vulnerability.
No security action required. Treat as normal feature review; if adopted in production paths, evaluate whether load shedding semantics are acceptable for the specific actor workload.
Security signals we found
No security-relevant signals: change is a feature addition, not a vulnerability fix.
No bounds of trust changed; new mailbox is opt-in via factory injection.
No sensitive data exposure, authentication, authorization, or cryptographic changes.
No incident or CVE references present in commit or supplied materials.
Evidence from the diff
The patch introduces BackpressureMailbox backed by queue.BackpressureQueue, a MailboxFactory type, and ActorOption functional options (WithMailboxFactory, WithMailboxSize). It wires these into NewActor, RegisterWithSystem, and ServiceKey.Spawn. Ask/Tell paths are updated to distinguish actor-context cancellation, caller-context cancellation, and silent backpressure drops (returning ErrMessageDropped). The change is additive and opt-in; the default ChannelMailbox behavior is unchanged.
Changed components
actor/actor.goactor/backpressure_mailbox.goactor/backpressure_mailbox_test.goactor/interface.goactor/system.gogo.modgo.sumactor/go.modactor/go.sumInspect captured patch +632 / −12
diff --git a/actor/actor.go b/actor/actor.go
index f75b4bb..6dddab0 100644
--- a/actor/actor.go
+++ b/actor/actor.go
@@ -7,6 +7,12 @@ import (
"github.com/lightningnetwork/lnd/fn/v2"
)
+// MailboxFactory is a function type that creates a Mailbox implementation.
+// It receives the actor's context and the desired capacity, allowing custom
+// mailbox implementations (e.g., BackpressureMailbox) to be injected.
+type MailboxFactory[M Message, R any] func(ctx context.Context,
+ capacity int) Mailbox[M, R]
+
// 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.
@@ -24,6 +30,10 @@ type ActorConfig[M Message, R any] struct {
// MailboxSize defines the buffer capacity of the actor's mailbox.
MailboxSize int
+
+ // MailboxFactory is an optional factory for creating the actor's
+ // mailbox. If nil, a default ChannelMailbox will be used.
+ MailboxFactory MailboxFactory[M, R]
}
// envelope wraps a message with its associated promise. This allows the sender
@@ -93,8 +103,14 @@ func NewActor[M Message, R any](cfg ActorConfig[M, R]) (*Actor[M, R],
mailboxCapacity = 1
}
- // Create mailbox - could be injected via config in the future.
- mailbox := NewChannelMailbox[M, R](ctx, mailboxCapacity)
+ // Create the mailbox using the factory if provided, otherwise use
+ // the default ChannelMailbox.
+ var mailbox Mailbox[M, R]
+ if cfg.MailboxFactory != nil {
+ mailbox = cfg.MailboxFactory(ctx, mailboxCapacity)
+ } else {
+ mailbox = NewChannelMailbox[M, R](ctx, mailboxCapacity)
+ }
actor := &Actor[M, R]{
id: cfg.ID,
@@ -194,7 +210,9 @@ func (ref *actorRefImpl[M, R]) Tell(ctx context.Context, msg M) {
if ref.actor.ctx.Err() != nil {
ref.trySendToDLO(msg)
}
- // Otherwise it was the caller's context that cancelled.
+ // Otherwise the message was either dropped by backpressure
+ // (load shedding) or the caller's context was cancelled.
+ // Both are intentionally silent — no DLO routing.
}
}
@@ -228,10 +246,15 @@ func (ref *actorRefImpl[M, R]) Ask(ctx context.Context, msg M) Future[R] {
// Use mailbox Send method which internally checks both contexts.
if !ref.actor.mailbox.Send(ctx, env) {
// Determine the error based on what failed.
- if ref.actor.ctx.Err() != nil {
+ switch {
+ case ref.actor.ctx.Err() != nil:
promise.Complete(fn.Err[R](ErrActorTerminated))
- } else {
+ case ctx.Err() != nil:
promise.Complete(fn.Err[R](ctx.Err()))
+ default:
+ // Neither context is done — the mailbox's
+ // backpressure mechanism dropped the message.
+ promise.Complete(fn.Err[R](ErrMessageDropped))
}
}
diff --git a/actor/backpressure_mailbox.go b/actor/backpressure_mailbox.go
new file mode 100644
index 0000000..016b457
--- /dev/null
+++ b/actor/backpressure_mailbox.go
@@ -0,0 +1,166 @@
+package actor
+
+import (
+ "context"
+ "iter"
+ "sync"
+ "sync/atomic"
+
+ "github.com/lightningnetwork/lnd/queue"
+)
+
+// BackpressureMailbox implements the Mailbox interface using a
+// queue.BackpressureQueue as its core buffer. The BackpressureQueue's drop
+// predicate is consulted on every Send/TrySend, allowing RED-style load
+// shedding before the mailbox is full.
+type BackpressureMailbox[M Message, R any] struct {
+ // queue is the underlying backpressure-aware buffer.
+ queue *queue.BackpressureQueue[envelope[M, R]]
+
+ // closed tracks whether the mailbox has been closed.
+ closed atomic.Bool
+
+ // mu protects Send/TrySend operations to prevent send-on-closed-channel
+ // panics. Close() acquires write lock, Send/TrySend acquire read lock.
+ mu sync.RWMutex
+
+ // closeOnce ensures Close() executes exactly once.
+ closeOnce sync.Once
+
+ // actorCtx is the actor's context for lifecycle management.
+ actorCtx context.Context
+}
+
+// NewBackpressureMailbox creates a new mailbox backed by a BackpressureQueue.
+// The shouldDrop function is called with the current queue depth on every send
+// attempt; if it returns true the message is silently dropped.
+func NewBackpressureMailbox[M Message, R any](
+ actorCtx context.Context,
+ capacity int,
+ shouldDrop queue.DropCheckFunc,
+) *BackpressureMailbox[M, R] {
+
+ if capacity <= 0 {
+ capacity = 1
+ }
+
+ pred := queue.AsDropPredicate[envelope[M, R]](shouldDrop)
+
+ return &BackpressureMailbox[M, R]{
+ queue: queue.NewBackpressureQueue(capacity, pred),
+ actorCtx: actorCtx,
+ }
+}
+
+// Send attempts to send an envelope to the mailbox. The BackpressureQueue's
+// drop predicate is consulted first; if it decides to drop, false is returned
+// immediately. Otherwise the send blocks until the envelope is accepted, the
+// caller's context is cancelled, or the actor's context is cancelled.
+func (m *BackpressureMailbox[M, R]) Send(ctx context.Context,
+ env envelope[M, R]) bool {
+
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
+ if m.IsClosed() {
+ return false
+ }
+
+ // Create a context that is cancelled when either the caller's context
+ // or the actor's context is done, so that the blocking Enqueue
+ // respects both.
+ merged, cancel := context.WithCancel(ctx)
+ stop := context.AfterFunc(m.actorCtx, cancel)
+ defer stop()
+ defer cancel()
+
+ err := m.queue.Enqueue(merged, env)
+
+ return err == nil
+}
+
+// TrySend attempts a non-blocking send. Returns false if the drop predicate
+// rejects the message, the queue is at capacity, or the mailbox is closed.
+func (m *BackpressureMailbox[M, R]) TrySend(env envelope[M, R]) bool {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
+ if m.IsClosed() {
+ return false
+ }
+
+ return m.queue.TryEnqueue(env)
+}
+
+// Receive returns an iterator that yields envelopes from the mailbox until
+// the mailbox is closed, the provided context is cancelled, or the actor's
+// context is cancelled.
+func (m *BackpressureMailbox[M, R]) Receive(
+ ctx context.Context) iter.Seq[envelope[M, R]] {
+
+ return func(yield func(envelope[M, R]) bool) {
+ ch := m.queue.ReceiveChan()
+ for {
+ select {
+ case env, ok := <-ch:
+ if !ok {
+ return
+ }
+
+ if !yield(env) {
+ return
+ }
+
+ case <-ctx.Done():
+ return
+
+ case <-m.actorCtx.Done():
+ return
+ }
+ }
+ }
+}
+
+// Close closes the mailbox, preventing new messages from being sent. Any
+// remaining messages can still be consumed via Drain.
+func (m *BackpressureMailbox[M, R]) Close() {
+ m.closeOnce.Do(func() {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ m.closed.Store(true)
+
+ m.queue.Close()
+ })
+}
+
+// IsClosed returns true if the mailbox has been closed.
+func (m *BackpressureMailbox[M, R]) IsClosed() bool {
+ return m.closed.Load()
+}
+
+// Drain returns an iterator that yields all remaining messages in the mailbox
+// after it has been closed.
+func (m *BackpressureMailbox[M, R]) Drain() iter.Seq[envelope[M, R]] {
+ return func(yield func(envelope[M, R]) bool) {
+ if !m.IsClosed() {
+ return
+ }
+
+ ch := m.queue.ReceiveChan()
+ for {
+ select {
+ case env, ok := <-ch:
+ if !ok {
+ return
+ }
+
+ if !yield(env) {
+ return
+ }
+ default:
+ return
+ }
+ }
+ }
+}
diff --git a/actor/backpressure_mailbox_test.go b/actor/backpressure_mailbox_test.go
new file mode 100644
index 0000000..e8ff560
--- /dev/null
+++ b/actor/backpressure_mailbox_test.go
@@ -0,0 +1,393 @@
+package actor
+
+import (
+ "context"
+ "sync"
+ "testing"
+
+ "github.com/lightningnetwork/lnd/queue"
+ "github.com/stretchr/testify/require"
+)
+
+// Compile-time assertion that BackpressureMailbox satisfies the Mailbox
+// interface.
+var _ Mailbox[TestMessage, int] = (*BackpressureMailbox[TestMessage, int])(nil)
+
+// TestBackpressureMailboxDropsWhenThresholdReached verifies that
+// BackpressureMailbox drops messages when shouldDrop returns true.
+func TestBackpressureMailboxDropsWhenThresholdReached(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.Background()
+ const capacity = 10
+ const dropThreshold = 5
+
+ shouldDrop := queue.DropCheckFunc(func(queueLen int) bool {
+ return queueLen >= dropThreshold
+ })
+
+ mbox := NewBackpressureMailbox[TestMessage, int](
+ ctx, capacity, shouldDrop,
+ )
+
+ // Fill up to the drop threshold — these should all succeed.
+ for i := range dropThreshold {
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: i},
+ }
+ ok := mbox.Send(ctx, env)
+ require.True(t, ok, "message %d should be accepted", i)
+ }
+
+ // Next message should be dropped by the predicate.
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: 99},
+ }
+ ok := mbox.Send(ctx, env)
+ require.False(t, ok, "message at threshold should be dropped")
+}
+
+// TestBackpressureMailboxTrySendDrops verifies TrySend also respects the drop
+// predicate.
+func TestBackpressureMailboxTrySendDrops(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.Background()
+ const capacity = 10
+ const dropThreshold = 3
+
+ shouldDrop := queue.DropCheckFunc(func(queueLen int) bool {
+ return queueLen >= dropThreshold
+ })
+
+ mbox := NewBackpressureMailbox[TestMessage, int](
+ ctx, capacity, shouldDrop,
+ )
+
+ // Fill to threshold.
+ for i := range dropThreshold {
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: i},
+ }
+ ok := mbox.TrySend(env)
+ require.True(t, ok, "message %d should be accepted", i)
+ }
+
+ // TrySend should now be rejected.
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: 99},
+ }
+ ok := mbox.TrySend(env)
+ require.False(t, ok, "TrySend at threshold should be dropped")
+}
+
+// TestBackpressureMailboxNeverDropPassesThrough verifies that a never-drop
+// predicate lets all messages through (up to channel capacity).
+func TestBackpressureMailboxNeverDropPassesThrough(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.Background()
+ const capacity = 5
+
+ neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
+ return false
+ })
+
+ mbox := NewBackpressureMailbox[TestMessage, int](
+ ctx, capacity, neverDrop,
+ )
+
+ // Fill the entire capacity.
+ for i := range capacity {
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: i},
+ }
+ ok := mbox.Send(ctx, env)
+ require.True(t, ok, "message %d should be accepted", i)
+ }
+}
+
+// TestBackpressureMailboxDelegatesReceive verifies that Receive yields messages
+// from the underlying BackpressureQueue.
+func TestBackpressureMailboxDelegatesReceive(t *testing.T) {
+ t.Parallel()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ const capacity = 5
+ neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
+ return false
+ })
+ mbox := NewBackpressureMailbox[TestMessage, int](
+ ctx, capacity, neverDrop,
+ )
+
+ // Send two messages.
+ for i := range 2 {
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: i},
+ }
+ mbox.Send(ctx, env)
+ }
+
+ // Close so Receive iterator terminates after draining.
+ mbox.Close()
+
+ var count int
+ for range mbox.Receive(ctx) {
+ count++
+ }
+
+ require.Equal(t, 2, count, "should receive 2 messages")
+}
+
+// TestBackpressureMailboxDelegatesDrain verifies that Drain yields remaining
+// messages after close.
+func TestBackpressureMailboxDelegatesDrain(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.Background()
+ const capacity = 5
+ neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
+ return false
+ })
+ mbox := NewBackpressureMailbox[TestMessage, int](
+ ctx, capacity, neverDrop,
+ )
+
+ // Send messages and close.
+ for i := range 3 {
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: i},
+ }
+ mbox.Send(ctx, env)
+ }
+ mbox.Close()
+
+ require.True(t, mbox.IsClosed())
+
+ var count int
+ for range mbox.Drain() {
+ count++
+ }
+
+ require.Equal(t, 3, count, "should drain 3 messages")
+}
+
+// TestBackpressureMailboxSendRespectsActorCtx verifies that Send returns false
+// when the actor context is cancelled.
+func TestBackpressureMailboxSendRespectsActorCtx(t *testing.T) {
+ t.Parallel()
+
+ actorCtx, actorCancel := context.WithCancel(context.Background())
+
+ const capacity = 1
+ neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
+ return false
+ })
+ mbox := NewBackpressureMailbox[TestMessage, int](
+ actorCtx, capacity, neverDrop,
+ )
+
+ // Fill the mailbox to capacity.
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: 1},
+ }
+ ok := mbox.Send(context.Background(), env)
+ require.True(t, ok)
+
+ // Cancel the actor context. The next blocking send should fail.
+ actorCancel()
+
+ env2 := envelope[TestMessage, int]{
+ message: TestMessage{Value: 2},
+ }
+ ok = mbox.Send(context.Background(), env2)
+ require.False(t, ok, "send should fail when actor context is cancelled")
+}
+
+// TestBackpressureMailboxReceiveAfterClose verifies that calling Receive after
+// Close does not panic and yields no messages (the channel is already drained).
+func TestBackpressureMailboxReceiveAfterClose(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.Background()
+ const capacity = 5
+ neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
+ return false
+ })
+ mbox := NewBackpressureMailbox[TestMessage, int](
+ ctx, capacity, neverDrop,
+ )
+
+ mbox.Close()
+
+ // First Receive after close should return immediately (closed channel).
+ var count int
+ for range mbox.Receive(ctx) {
+ count++
+ }
+ require.Equal(t, 0, count, "no messages expected")
+
+ // Second Receive must not panic.
+ for range mbox.Receive(ctx) {
+ count++
+ }
+ require.Equal(t, 0, count, "still no messages expected")
+}
+
+// TestBackpressureMailboxDrainAfterDrain verifies that calling Drain twice
+// after Close does not panic.
+func TestBackpressureMailboxDrainAfterDrain(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.Background()
+ const capacity = 5
+ neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
+ return false
+ })
+ mbox := NewBackpressureMailbox[TestMessage, int](
+ ctx, capacity, neverDrop,
+ )
+
+ // Send one message and close.
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: 1},
+ }
+ mbox.Send(ctx, env)
+ mbox.Close()
+
+ // First drain should yield the message.
+ var count int
+ for range mbox.Drain() {
+ count++
+ }
+ require.Equal(t, 1, count, "should drain 1 message")
+
+ // Second drain must not panic and should yield nothing.
+ count = 0
+ for range mbox.Drain() {
+ count++
+ }
+ require.Equal(t, 0, count, "second drain should yield nothing")
+}
+
+// TestBackpressureMailboxConcurrentSendClose tests concurrent Send/TrySend and
+// Close operations to ensure no race conditions or panics occur.
+func TestBackpressureMailboxConcurrentSendClose(t *testing.T) {
+ t.Parallel()
+
+ const (
+ numSenders = 50
+ capacity = 20
+ )
+
+ ctx := context.Background()
+ neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
+ return false
+ })
+
+ mbox := NewBackpressureMailbox[TestMessage, int](
+ ctx, capacity, neverDrop,
+ )
+
+ var wg sync.WaitGroup
+
+ // Launch many goroutines that continuously call Send/TrySend.
+ for i := range numSenders {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ for j := range 100 {
+ env := envelope[TestMessage, int]{
+ message: TestMessage{
+ Value: i*100 + j,
+ },
+ }
+ // Send must not panic regardless of
+ // whether Close has been called.
+ mbox.Send(ctx, env)
+ }
+ }()
+
+ // Launch a goroutine that also calls TrySend
+ // concurrently.
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ for j := range 500 {
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: j},
+ }
+ mbox.TrySend(env)
+ }
+ }()
+ }
+
+ // Drain messages concurrently to free buffer space so Send
+ // goroutines make progress and don't all block.
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ ch := mbox.queue.ReceiveChan()
+ for range ch {
+ }
+ }()
+
+ // Close the mailbox while senders are still active.
+ mbox.Close()
+
+ // Wait for all goroutines to finish. If the RWMutex protocol
+ // is broken, this test will panic with "send on closed channel"
+ // or the race detector will flag a data race.
+ wg.Wait()
+
+ require.True(t, mbox.IsClosed())
+
+ // After Close, all subsequent sends must return false.
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: -1},
+ }
+ require.False(t, mbox.Send(ctx, env))
+ require.False(t, mbox.TrySend(env))
+}
+
+// TestBackpressureMailboxConcurrentMultiClose verifies that calling Close
+// from multiple goroutines simultaneously does not panic.
+func TestBackpressureMailboxConcurrentMultiClose(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.Background()
+ neverDrop := queue.DropCheckFunc(func(queueLen int) bool {
+ return false
+ })
+
+ mbox := NewBackpressureMailbox[TestMessage, int](
+ ctx, 10, neverDrop,
+ )
+
+ // Send a few messages first.
+ for i := range 5 {
+ env := envelope[TestMessage, int]{
+ message: TestMessage{Value: i},
+ }
+ mbox.Send(ctx, env)
+ }
+
+ // Close from many goroutines simultaneously.
+ var wg sync.WaitGroup
+ for range 20 {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ mbox.Close()
+ }()
+ }
+ wg.Wait()
+
+ require.True(t, mbox.IsClosed())
+}
diff --git a/actor/go.mod b/actor/go.mod
index 77ffce8..c762776 100644
--- a/actor/go.mod
+++ b/actor/go.mod
@@ -5,6 +5,7 @@ go 1.25.5
require (
github.com/btcsuite/btclog/v2 v2.0.1-0.20250602222548-9967d19bb084
github.com/lightningnetwork/lnd/fn/v2 v2.0.8
+ github.com/lightningnetwork/lnd/queue v1.1.1
github.com/stretchr/testify v1.8.1
pgregory.net/rapid v1.2.0
)
@@ -12,8 +13,15 @@ require (
require (
github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/lightningnetwork/lnd/ticker v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect
golang.org/x/sync v0.7.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+
+replace github.com/lightningnetwork/lnd/queue => ../queue
+
+replace github.com/lightningnetwork/lnd/ticker v1.0.0 => ../ticker
+
+replace github.com/lightningnetwork/lnd/fn/v2 => ../fn
diff --git a/actor/go.sum b/actor/go.sum
index bcd10dc..be484c5 100644
--- a/actor/go.sum
+++ b/actor/go.sum
@@ -5,8 +5,6 @@ github.com/btcsuite/btclog/v2 v2.0.1-0.20250602222548-9967d19bb084/go.mod h1:XIt
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/lightningnetwork/lnd/fn/v2 v2.0.8 h1:r2SLz7gZYQPVc3IZhU82M66guz3Zk2oY+Rlj9QN5S3g=
-github.com/lightningnetwork/lnd/fn/v2 v2.0.8/go.mod h1:TOzwrhjB/Azw1V7aa8t21ufcQmdsQOQMDtxVOQWNl8s=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/actor/interface.go b/actor/interface.go
index 6ddbe8f..acc7bf8 100644
--- a/actor/interface.go
+++ b/actor/interface.go
@@ -2,6 +2,7 @@ package actor
import (
"context"
+ "errors"
"fmt"
"github.com/lightningnetwork/lnd/fn/v2"
@@ -11,6 +12,10 @@ import (
// actor was terminated or in the process of shutting down.
var ErrActorTerminated = fmt.Errorf("actor terminated")
+// ErrMessageDropped indicates that a message was dropped by the mailbox's
+// backpressure mechanism (e.g., RED-style load shedding).
+var ErrMessageDropped = errors.New("message dropped by backpressure")
+
// ErrEmptyActorID is returned when an actor is created with an empty ID.
var ErrEmptyActorID = fmt.Errorf("actor ID must not be empty")
diff --git a/actor/system.go b/actor/system.go
index 5e765de..18d8a38 100644
--- a/actor/system.go
+++ b/actor/system.go
@@ -116,13 +116,34 @@ func NewActorSystemWithConfig(config SystemConfig) *ActorSystem {
return system
}
+// ActorOption is a functional option for customizing actor creation.
+type ActorOption[M Message, R any] func(*ActorConfig[M, R])
+
+// WithMailboxFactory returns an ActorOption that sets a custom mailbox factory.
+func WithMailboxFactory[M Message, R any](
+ f MailboxFactory[M, R]) ActorOption[M, R] {
+
+ return func(cfg *ActorConfig[M, R]) {
+ cfg.MailboxFactory = f
+ }
+}
+
+// WithMailboxSize returns an ActorOption that overrides the default mailbox
+// size.
+func WithMailboxSize[M Message, R any](size int) ActorOption[M, R] {
+ return func(cfg *ActorConfig[M, R]) {
+ cfg.MailboxSize = size
+ }
+}
+
// 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], error) {
+ behavior ActorBehavior[M, R],
+ opts ...ActorOption[M, R]) (ActorRef[M, R], error) {
actorCfg := ActorConfig[M, R]{
ID: id,
@@ -130,6 +151,10 @@ func RegisterWithSystem[M Message, R any](as *ActorSystem, id string,
DLO: as.deadLetterActor,
MailboxSize: as.config.MailboxCapacity,
}
+
+ for _, opt := range opts {
+ opt(&actorCfg)
+ }
// Check for duplicate actor ID before creating the actor.
as.mu.Lock()
if _, exists := as.actors[id]; exists {
@@ -299,9 +324,10 @@ func NewServiceKey[M Message, R any](name string) ServiceKey[M, R] {
// 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], error) {
+ behavior ActorBehavior[M, R],
+ opts ...ActorOption[M, R]) (ActorRef[M, R], error) {
- return RegisterWithSystem(as, id, sk, behavior)
+ return RegisterWithSystem(as, id, sk, behavior, opts...)
}
// Unregister removes an actor reference associated with this service key from
diff --git a/go.mod b/go.mod
index 1c39b46..3aae8ac 100644
--- a/go.mod
+++ b/go.mod
@@ -207,6 +207,9 @@ require (
// TODO(gijs): remove once new actor package is released.
replace github.com/lightningnetwork/lnd/actor => ./actor
+// TODO(gijs): remove once new queue package is released.
+replace github.com/lightningnetwork/lnd/queue => ./queue
+
// TODO(elle): remove once the gossip V2 sqldb changes have been made.
replace github.com/lightningnetwork/lnd/sqldb => ./sqldb
diff --git a/go.sum b/go.sum
index 90e0598..e99d976 100644
--- a/go.sum
+++ b/go.sum
@@ -384,8 +384,6 @@ github.com/lightningnetwork/lnd/healthcheck v1.2.6 h1:1sWhqr93GdkWy4+6U7JxBfcyZI
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/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=
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.