actor: introduce generic Mailbox interface with iter.Seq support
What changed, and why it matters
This commit adds a new internal message-queue abstraction for LND's actor system. It is purely a refactoring/new-code change: it introduces a generic Mailbox interface and a channel-based implementation, with no existing callers shown and no obvious security bug. The code is defensive (uses mutexes and atomic flags to avoid send-on-closed-channel panics) and does not change any externally reachable behavior.
No security action required. Review as normal code-quality change; if auditing, confirm callers correctly handle false returns from Send/TrySend and that Drain/Receive iterators do not leak goroutines when yield returns false.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch creates actor/mailbox.go defining a generic Mailbox[M,R] interface and a ChannelMailbox implementation. It replaces the prior direct channel usage with an abstraction supporting context-aware Send/TrySend, iter.Seq-based Receive/Drain, and Close. The implementation uses sync.RWMutex + atomic.Bool + sync.Once to prevent send-on-closed-channel races and to respect both caller and actor contexts. No callers, tests, or existing behavior are modified in this commit. There is no evidence of a vulnerability, backdoor, or security fix.
Changed components
actor/mailbox.goInspect captured patch +176 / −0
diff --git a/actor/mailbox.go b/actor/mailbox.go
new file mode 100644
index 0000000..4d15f43
--- /dev/null
+++ b/actor/mailbox.go
@@ -0,0 +1,176 @@
+package actor
+
+import (
+ "context"
+ "iter"
+ "sync"
+ "sync/atomic"
+)
+
+// Mailbox represents the message queue for an actor. It provides methods for
+// sending messages and receiving them via an iterator pattern.
+type Mailbox[M Message, R any] interface {
+ // Send attempts to send an envelope to the mailbox with context-based
+ // cancellation. Returns true if sent successfully, false if the
+ // context was cancelled or the mailbox is closed.
+ Send(ctx context.Context, env envelope[M, R]) bool
+
+ // TrySend attempts to send without blocking. Returns true if the
+ // envelope was sent, false if the mailbox is full or closed.
+ TrySend(env envelope[M, R]) bool
+
+ // Receive returns an iterator for consuming messages from the mailbox.
+ // The iterator will yield messages until the mailbox is closed or the
+ // context is cancelled.
+ Receive(ctx context.Context) iter.Seq[envelope[M, R]]
+
+ // Close closes the mailbox, preventing new messages from being sent.
+ // Any remaining messages can still be consumed via Receive.
+ Close()
+
+ // IsClosed returns true if the mailbox has been closed.
+ IsClosed() bool
+
+ // Drain returns an iterator that yields all remaining messages in the
+ // mailbox after it has been closed. This is useful for cleanup.
+ Drain() iter.Seq[envelope[M, R]]
+}
+
+// ChannelMailbox is a channel-based implementation of the Mailbox interface.
+type ChannelMailbox[M Message, R any] struct {
+ ch chan envelope[M, R]
+ 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
+}
+
+// NewChannelMailbox creates a new channel-based mailbox with the specified
+// buffer capacity and actor context.
+func NewChannelMailbox[M Message, R any](actorCtx context.Context,
+ capacity int) *ChannelMailbox[M, R] {
+
+ if capacity <= 0 {
+ capacity = 1
+ }
+ return &ChannelMailbox[M, R]{
+ ch: make(chan envelope[M, R], capacity),
+ actorCtx: actorCtx,
+ }
+}
+
+// Send implements Mailbox.Send with context-aware blocking send.
+func (m *ChannelMailbox[M, R]) Send(ctx context.Context,
+ env envelope[M, R]) bool {
+
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
+ if m.IsClosed() {
+ return false
+ }
+
+ select {
+ case m.ch <- env:
+ return true
+ case <-ctx.Done():
+ return false
+ case <-m.actorCtx.Done():
+ // Actor is shutting down.
+ return false
+ }
+}
+
+// TrySend implements Mailbox.TrySend with non-blocking send.
+func (m *ChannelMailbox[M, R]) TrySend(env envelope[M, R]) bool {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
+ if m.IsClosed() {
+ return false
+ }
+
+ select {
+ case m.ch <- env:
+ return true
+ default:
+ return false
+ }
+}
+
+// Receive implements Mailbox.Receive using iter.Seq pattern.
+func (m *ChannelMailbox[M, R]) Receive(
+ ctx context.Context) iter.Seq[envelope[M, R]] {
+ return func(yield func(envelope[M, R]) bool) {
+ for {
+ select {
+ case env, ok := <-m.ch:
+ if !ok {
+ return
+ }
+
+ if !yield(env) {
+ return
+ }
+
+ case <-ctx.Done():
+ return
+
+ case <-m.actorCtx.Done():
+ return
+ }
+ }
+ }
+}
+
+// Close implements Mailbox.Close.
+func (m *ChannelMailbox[M, R]) Close() {
+ m.closeOnce.Do(func() {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ m.closed.Store(true)
+
+ close(m.ch)
+ })
+}
+
+// IsClosed implements Mailbox.IsClosed.
+func (m *ChannelMailbox[M, R]) IsClosed() bool {
+ return m.closed.Load()
+}
+
+// Drain implements Mailbox.Drain for cleanup after close.
+func (m *ChannelMailbox[M, R]) Drain() iter.Seq[envelope[M, R]] {
+ return func(yield func(envelope[M, R]) bool) {
+ // Only drain if closed.
+ if !m.IsClosed() {
+ return
+ }
+
+ // Drain all remaining messages from the channel.
+ for {
+ select {
+ case env, ok := <-m.ch:
+ // Channel closed, nothing left to drain.
+ if !ok {
+ return
+ }
+
+ if !yield(env) {
+ return
+ }
+ default:
+ // Channel empty, done draining.
+ return
+ }
+ }
+ }
+}
Why this scored 12/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.