Add ZMQ hashblock subscriber for sub-second chain refresh

New internal/zmqmon package subscribes to bitcoind's hashblock
ZMQ topic and emits TipEvents. Uses github.com/go-zeromq/zmq4
(pure Go, builds with CGO_ENABLED=0). Exponential backoff on
connection failure so bitcoind restarts don't kill the
subscriber permanently; event channel drops rather than blocks
if the consumer is slow (signals are advisory, not logs).

Aggregator.Run now takes a <-chan TipEvent; when a tip arrives
it fires an immediate refresh() outside the normal ticker
cadence. With a 5s poll interval and 0.5-1s ZMQ latency from
bitcoind, dashboards now reflect new tips roughly 4x faster.

Endpoint comes from BITCOIN_ZMQ_BLOCK — empty disables ZMQ
entirely and the aggregator just runs on the ticker alone.
This commit is contained in:
satoshi
2026-04-14 11:14:52 +03:00
parent 01829746ac
commit b786f489a1
5 changed files with 156 additions and 5 deletions
+11 -2
View File
@@ -13,6 +13,7 @@ import (
"github.com/kamadopool/kamado-api/internal/bitcoind"
"github.com/kamadopool/kamado-api/internal/ckpool"
"github.com/kamadopool/kamado-api/internal/store"
"github.com/kamadopool/kamado-api/internal/zmqmon"
)
// Snapshot is the merged view served to the UI. All fields are safe to
@@ -81,8 +82,9 @@ func New(ck *ckpool.Client, rpc *bitcoind.RPC, interval time.Duration, log *slog
// Run blocks until ctx is cancelled, refreshing the snapshot every Interval.
// It runs one immediate refresh at startup so readers don't see an empty
// snapshot after ctx launches the goroutine. Persisted block history is
// loaded from the store before the first refresh.
func (a *Aggregator) Run(ctx context.Context) {
// loaded from the store before the first refresh. If tipEvents is non-nil,
// each received tip triggers an immediate refresh outside the poll cadence.
func (a *Aggregator) Run(ctx context.Context, tipEvents <-chan zmqmon.TipEvent) {
a.loadPersistedBlocks()
a.refresh(ctx)
t := time.NewTicker(a.Interval)
@@ -93,6 +95,13 @@ func (a *Aggregator) Run(ctx context.Context) {
return
case <-t.C:
a.refresh(ctx)
case ev, ok := <-tipEvents:
if !ok {
tipEvents = nil
continue
}
a.Log.Debug("zmq tip, refreshing", "hash", ev.Hash)
a.refresh(ctx)
}
}
}