mirror of
https://github.com/luxfi/vm.git
synced 2026-10-30 15:13:40 +00:00
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]>
152 lines
3.9 KiB
Go
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()
|
|
}
|