Files
bft/common/timeout_handler.go
zeekayandHanzo Dev d678c2d213 transform: publish upstream simplex as luxfi/bft on luxfi/log
Reapplies the fork's whole delta on top of the merge, as one reviewable diff
rather than 43 conflict resolutions. Nothing here changes consensus behaviour.

- module github.com/luxfi/bft, Go 1.26.4; root package simplex -> bft, which
  would otherwise collide with the simplex/ subpackage under this module name.
- go.uber.org/zap -> github.com/luxfi/log, so common.Logger takes ...log.Field.
  zap.Error maps to log.Err: log.Error is luxfi/log's package-level logging
  function, not a field constructor, and the compiler is the only thing that
  catches that. Needs luxfi/log v1.6.1 for the Stringers constructor.
- avalanchego/ -> host/. The package holds NodeID, VMBlock, Bitmask and size
  limits — the host node's types, vendored to avoid importing a whole node — so
  it is named for what it holds. Renaming it luxd/ would have swapped one brand
  for another and said just as little.
- testutil/logger.go reimplemented without zapcore. Upstream layers a
  keyword-filtering Core, a debug-swallowing Encoder and a Tee because that is
  zap's extension model; luxfi/log has no such seam, so every policy is applied
  in one emit. That also fixes two defects the layering caused:
  SilenceExceptKeywords rebuilt the logger and discarded an installed Intercept
  hook, and the two sinks read different env vars (LOG_LEVEL vs log_level) so
  they disagreed about suppressing debug. Hooks run outside the lock, so a hook
  that logs fails the test instead of deadlocking it. Fatal panics rather than
  exiting, because exiting takes the suite down with no attribution.
- simplex/monitor_test.go keeps a local logger: testutil imports simplex, so an
  internal test cannot import testutil back without closing a cycle.
- README keeps the name Simplex throughout — it is the protocol's own name and
  the subject of reference [1]. Only the deployment target changed. The HyperSDK
  sentence still says Avalanche because that sentence is true.
- Drops the fork's Makefile: it duplicated unit-tests.sh and fuzz.sh, and ran the
  suite with -timeout 30s when simplex alone needs over 100s.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 04:21:17 -07:00

152 lines
2.9 KiB
Go

// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package common
import (
"maps"
"sync"
"time"
"github.com/luxfi/log"
)
type timeoutRunner[T comparable] func(ids []T)
type TimeoutHandler[T comparable] struct {
// helpful for logging
name string
// how often to run through the tasks
runInterval time.Duration
// function to run tasks
taskRunner timeoutRunner[T]
lock sync.Mutex
ticks chan time.Time
close chan struct{}
// maps id to a task
tasks map[T]struct{}
now time.Time
log Logger
running sync.WaitGroup
}
// NewTimeoutHandler returns a TimeoutHandler and starts a new goroutine that
// listens for ticks and executes TimeoutTasks.
func NewTimeoutHandler[T comparable](log Logger, name string, startTime time.Time, runInterval time.Duration, taskRunner timeoutRunner[T]) *TimeoutHandler[T] {
t := &TimeoutHandler[T]{
name: name,
now: startTime,
tasks: make(map[T]struct{}),
ticks: make(chan time.Time, 1),
close: make(chan struct{}),
runInterval: runInterval,
taskRunner: taskRunner,
log: log,
}
t.running.Add(1)
go t.run(startTime)
return t
}
func (t *TimeoutHandler[T]) run(startTime time.Time) {
defer t.running.Done()
lastTickTime := startTime
for t.shouldRun() {
select {
case now := <-t.ticks:
if now.Sub(lastTickTime) < t.runInterval {
continue
}
lastTickTime = now
// update the current time
t.lock.Lock()
t.now = now
t.lock.Unlock()
t.maybeRunTasks()
case <-t.close:
return
}
}
}
func (t *TimeoutHandler[T]) maybeRunTasks() {
t.lock.Lock()
ids := make([]T, 0, len(t.tasks))
for id := range t.tasks {
ids = append(ids, id)
}
t.lock.Unlock()
if len(ids) == 0 {
return
}
t.log.Debug("Running task ids", log.Any("task ids", ids), log.String("name", t.name))
t.taskRunner(ids)
}
func (t *TimeoutHandler[T]) shouldRun() bool {
select {
case <-t.close:
return false
default:
return true
}
}
func (t *TimeoutHandler[T]) Tick(now time.Time) {
select {
case t.ticks <- now:
t.lock.Lock()
t.now = now
t.lock.Unlock()
default:
t.log.Debug("Dropping tick in timeouthandler", log.String("name", t.name))
}
}
func (t *TimeoutHandler[T]) AddTask(id T) {
t.lock.Lock()
defer t.lock.Unlock()
t.tasks[id] = struct{}{}
t.log.Debug("Adding timeout task", log.Any("id", id), log.String("name", t.name))
}
func (t *TimeoutHandler[T]) RemoveTask(ID T) {
t.lock.Lock()
defer t.lock.Unlock()
if _, ok := t.tasks[ID]; !ok {
return
}
t.log.Debug("Removing timeout task", log.Any("id", ID), log.String("name", t.name))
delete(t.tasks, ID)
}
func (t *TimeoutHandler[T]) RemoveOldTasks(shouldRemove func(id T, _ struct{}) bool) {
t.lock.Lock()
defer t.lock.Unlock()
maps.DeleteFunc(t.tasks, shouldRemove)
}
func (t *TimeoutHandler[T]) Close() {
defer t.running.Wait()
select {
case <-t.close:
return
default:
close(t.close)
}
}