Files
vm/rpc/notifier.go
zeekayandHanzo Dev 3e79aad01a answer an unset notifier Log before the forwarding loop starts
NewNotificationForwarder spawns the forwarding goroutine itself, so a
caller that leaves Log unset has no window to set it, and
forwardNotification logs before it does anything else. The nil
dereference lands on a goroutine whose panic no caller can recover, so
it takes the process rather than failing one VM.

start() is the one place the loop begins, so defaulting Log there covers
both ways to leave it unset — the exported field and the constructor
argument. Noop discards what it is given, so a caller that wanted
silence still gets it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-02 04:02:48 -07:00

152 lines
3.9 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"context"
"fmt"
"sync"
"github.com/luxfi/log"
)
// Message represents a notification message type
type Message uint32
const (
_ Message = iota
// PendingTxs indicates pending transactions notification
PendingTxs
// StateSyncDone indicates state sync has completed
StateSyncDone
)
func (m Message) String() string {
switch m {
case PendingTxs:
return "PendingTxs"
case StateSyncDone:
return "StateSyncDone"
default:
return fmt.Sprintf("Unknown(%d)", m)
}
}
// Subscription is a function that blocks until either the given context is cancelled, or a message is returned.
// It is used to receive messages from a VM such as Pending transactions, state sync completion, etc.
// The function returns the message received, or an error if the context is cancelled.
type Subscription func(ctx context.Context) (Message, error)
type Notifier interface {
Notify(context.Context, Message) error
}
// NotificationForwarder is a component that listens for notifications from a Subscription,
// and forwards them to a Notifier.
// When CheckForEvent is called mid-subscription, it retries the subscription.
// After Notify is called, it waits for CheckForEvent to be called before subscribing again.
type NotificationForwarder struct {
Engine Notifier
Subscribe Subscription
Log log.Logger
lock sync.Mutex
executing sync.WaitGroup
execCtx context.Context
haltExecution context.CancelFunc
abortContext context.CancelFunc
}
func NewNotificationForwarder(
engine Notifier,
subscribe Subscription,
log log.Logger,
) *NotificationForwarder {
nf := &NotificationForwarder{
Engine: engine,
Subscribe: subscribe,
Log: log,
}
nf.start()
return nf
}
func (nf *NotificationForwarder) start() {
// The loop below logs before it does anything else, and it runs on a
// goroutine whose panic no caller can recover. Log is exported and the
// constructor also takes one, so there are two ways to leave it unset;
// answering it here covers both, because this is the one place the loop
// begins. Noop discards what it is given, so a caller that wanted silence
// still gets it.
if nf.Log == nil {
nf.Log = log.Noop()
}
nf.executing.Add(1)
nf.execCtx, nf.haltExecution = context.WithCancel(context.Background())
go nf.run()
}
func (nf *NotificationForwarder) run() {
defer nf.executing.Done()
for nf.execCtx.Err() == nil {
nf.forwardNotification()
}
}
func (nf *NotificationForwarder) forwardNotification() {
ctx := nf.setAndGetContext()
defer nf.cancelContext()
nf.Log.Debug("Subscribing to notifications")
msg, err := nf.Subscribe(ctx)
if err != nil {
nf.Log.Debug("Failed subscribing to notifications", "error", err)
return
}
nf.Log.Debug("Received notification", "msg", msg)
if err := nf.Engine.Notify(ctx, msg); err != nil {
nf.Log.Debug("Failed notifying engine", "error", err)
return
}
// Wait for the context to be cancelled before proceeding to the next subscription,
// in order to subscribe after a block was accepted or a state sync was completed.
<-ctx.Done()
}
// CheckForEvent cancels any outstanding WaitForEvent calls and schedules a new WaitForEvent call.
func (nf *NotificationForwarder) CheckForEvent() {
nf.cancelContext()
}
func (nf *NotificationForwarder) cancelContext() {
nf.lock.Lock()
defer nf.lock.Unlock()
if nf.abortContext != nil {
nf.abortContext()
}
}
func (nf *NotificationForwarder) setAndGetContext() context.Context {
ctx, cancel := context.WithCancel(nf.execCtx)
nf.lock.Lock()
defer nf.lock.Unlock()
nf.abortContext = cancel
return ctx
}
// Close cancels any outstanding WaitForEvent calls and waits for them to return.
// After Close returns, no future WaitForEvent calls will be made by the notification forwarder.
func (nf *NotificationForwarder) Close() {
defer nf.executing.Wait()
nf.haltExecution()
}