ceremonyclient/lifecycle/signaler.go
Cassandra Heart c797d482f9
v2.1.0.5 (#457)
* wip: conversion of hotstuff from flow into Q-oriented model

* bulk of tests

* remaining non-integration tests

* add integration test, adjust log interface, small tweaks

* further adjustments, restore full pacemaker shape

* add component lifecycle management+supervisor

* further refinements

* resolve timeout hanging

* mostly finalized state for consensus

* bulk of engine swap out

* lifecycle-ify most types

* wiring nearly complete, missing needed hooks for proposals

* plugged in, vetting message validation paths

* global consensus, plugged in and verified

* app shard now wired in too

* do not decode empty keys.yml (#456)

* remove obsolete engine.maxFrames config parameter (#454)

* default to Info log level unless debug is enabled (#453)

* respect config's  "logging" section params, remove obsolete single-file logging (#452)

* Trivial code cleanup aiming to reduce Go compiler warnings (#451)

* simplify range traversal

* simplify channel read for single select case

* delete rand.Seed() deprecated in Go 1.20 and no-op as of Go 1.24

* simplify range traversal

* simplify channel read for single select case

* remove redundant type from array

* simplify range traversal

* simplify channel read for single select case

* RC slate

* finalize 2.1.0.5

* Update comments in StrictMonotonicCounter

Fix comment formatting and clarify description.

---------

Co-authored-by: Black Swan <3999712+blacks1ne@users.noreply.github.com>
2025-11-11 05:00:17 -06:00

113 lines
3.3 KiB
Go

package lifecycle
import (
"context"
"fmt"
"log"
"os"
"runtime"
"go.uber.org/atomic"
)
// Signaler sends the error out.
type Signaler struct {
errChan chan error
errThrown *atomic.Bool
}
func NewSignaler() (*Signaler, <-chan error) {
errChan := make(chan error, 1)
return &Signaler{
errChan: errChan,
errThrown: atomic.NewBool(false),
}, errChan
}
// Throw is a narrow drop-in replacement for panic, log.Fatal, log.Panic, etc
// anywhere there's something connected to the error channel. It only sends
// the first error it is called with to the error channel, and logs subsequent
// errors as unhandled.
func (s *Signaler) Throw(err error) {
defer runtime.Goexit()
if s.errThrown.CompareAndSwap(false, true) {
s.errChan <- err
close(s.errChan)
} else {
// TODO: we simply log the unhandled fatal to stderr for now, but we should
// probably allow the user to customize the logger / logging format used
log.New(os.Stderr, "", log.LstdFlags).Println(
fmt.Errorf("unhandled fatal: %w", err),
)
}
}
// SignalerContext is a constrained interface to provide a drop-in replacement
// for context.Context including in interfaces that compose it.
type SignalerContext interface {
context.Context
Throw(err error) // delegates to the signaler
}
// SignalerContextKey represents the key type for retrieving a SignalerContext
// from a value `context.Context`.
type SignalerContextKey struct{}
// private, to force context derivation / WithSignaler
type signalerCtx struct {
context.Context
*Signaler
}
// WithSignaler is the One True Way of getting a SignalerContext.
func WithSignaler(parent context.Context) (SignalerContext, <-chan error) {
sig, errChan := NewSignaler()
return &signalerCtx{parent, sig}, errChan
}
// WithSignalerContext wraps `SignalerContext` using `context.WithValue` so it
// can later be used with `Throw`.
func WithSignalerContext(
parent context.Context,
ctx SignalerContext,
) context.Context {
return context.WithValue(parent, SignalerContextKey{}, ctx)
}
// Throw enables throwing a fatal error using any context.Context.
//
// If we have an SignalerContext, we can directly ctx.Throw.
// But a lot of library methods expect context.Context, & we want to pass the
// same w/o boilerplate. Moreover, we could have built with:
//
// context.WithCancel(lifecycle.WithSignaler(ctx, sig)),
//
// "downcasting" to context.Context. Yet, we can still type-assert and recover.
//
// Throw can be a drop-in replacement anywhere we have a context.Context likely
// to support signals. IT WILL PANIC IF THE CONTEXT DOES NOT SUPPORT SIGNALS
func Throw(ctx context.Context, err error) {
signalerAbleContext, ok := ctx.Value(SignalerContextKey{}).(SignalerContext)
if ok {
signalerAbleContext.Throw(err)
} else {
// Be spectacular on how this does not -but should- handle fatals:
log.Fatalf(
"fatal error: signaler not found for context, please implement! Unhandled fatal error: %v",
err,
)
}
}
// WithSignallerAndCancel returns an fatal context, the cancel function for the
// context, and the error channel for the context.
func WithSignallerAndCancel(ctx context.Context) (
SignalerContext,
context.CancelFunc,
<-chan error,
) {
parent, cancel := context.WithCancel(ctx)
fatalCtx, errCh := WithSignaler(parent)
return fatalCtx, cancel, errCh
}