mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
`go test ./...` failed three ways on the runner. Two were build failures that
cannot happen on a mac, and one was a race that only loses under load.
pkg/dpdk — `undefined: syscall.SO_NOSIGPIPE`. DarwinOptimizations guarded itself
with `if runtime.GOOS != "darwin" { return }`, but SO_NOSIGPIPE is a COMPILE-time
constant that exists only on Darwin; a runtime check cannot save a reference the
linux build still has to resolve. Split into _darwin.go / _nodarwin.go. Verified
by cross-compiling both ways -- it failed for GOOS=linux before, both pass now.
pkg/orderbook — the cgo preamble uses uint64_t while including only <stdlib.h>.
macOS pulls stdint.h in transitively and glibc does not, so this built here and
never on the runner. Added the include.
pkg/price — TestXChainSourceStalePrice: "Expected price to be stale".
Not a flaky test; Close() was not a barrier. It closed the done channel and
returned WITHOUT waiting for the polling goroutine. A loop selecting on done
against a ticker picks a ready case AT RANDOM, so a poll could still be running
after Close() returned, and poll writes last[symbol] = time.Now(). The test
closes the source, backdates that timestamp by 10s, and asserts staleness -- an
in-flight poll overwrote it. Wide window on a loaded runner, near-zero on a
laptop, which is why it reproduces 0/30 here.
All EIGHT sources had it, so the fix is one shared `lifecycle` (embedded, so
`s.done` still resolves) whose stop() joins before returning and is safe to call
twice. Oracle had the same defect written as
// Give time for goroutines to exit
time.Sleep(100 * time.Millisecond)
which could not work either -- calcAverages blocks on a 1s ticker, so it ran up
to a full second after Stop() returned. Now on the same lifecycle, and its loops
select on done instead of polling a flag, so they leave promptly rather than
after one more tick. Nothing restarts a stopped source (checked: 0 tests), and
the other sources already could not.
go build ./... OK
GOOS=linux go build ./... OK (failed before)
go vet price/dpdk/orderbook OK
go test ./pkg/price/ -race ok, 3/3 runs
go test -run StalePrice -count=30 ok
Co-authored-by: Hanzo Dev <[email protected]>
42 lines
1.4 KiB
Go
42 lines
1.4 KiB
Go
package price
|
|
|
|
import "sync"
|
|
|
|
// lifecycle is the one start/stop mechanism for a polling price source.
|
|
//
|
|
// Every source here had written its own, and every one had the same hole:
|
|
// Close() closed the done channel and returned immediately, without waiting for
|
|
// the goroutine to notice. A polling loop selects on done against a ticker, and
|
|
// when both are ready Go picks a case AT RANDOM — so a poll could still be
|
|
// running, and still writing to the source's maps, after Close() had returned.
|
|
//
|
|
// TestXChainSourceStalePrice is what surfaced it: the test closes the source,
|
|
// backdates last["LUX-USDC"] by 10s, and asserts the price reads stale. An
|
|
// in-flight poll overwrote that timestamp with time.Now() and the assertion
|
|
// failed — on CI, where scheduling delays widen the window, not on a laptop.
|
|
//
|
|
// stop() joins before returning, so a closed source cannot mutate anything, and
|
|
// it is safe to call more than once (close of a closed channel panics).
|
|
type lifecycle struct {
|
|
done chan struct{}
|
|
wg sync.WaitGroup
|
|
once sync.Once
|
|
}
|
|
|
|
func newLifecycle() lifecycle { return lifecycle{done: make(chan struct{})} }
|
|
|
|
// run starts fn in a goroutine that stop() will wait for.
|
|
func (l *lifecycle) run(fn func()) {
|
|
l.wg.Add(1)
|
|
go func() {
|
|
defer l.wg.Done()
|
|
fn()
|
|
}()
|
|
}
|
|
|
|
// stop signals every goroutine started by run and waits for them to return.
|
|
func (l *lifecycle) stop() {
|
|
l.once.Do(func() { close(l.done) })
|
|
l.wg.Wait()
|
|
}
|