From 9e906d11f9f0cc56dc7143b8d73e0196a2ac8397 Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 11:21:38 +0000 Subject: [PATCH] fix(proxy): keep runtime state local Preserve bind addresses on reload and track active miners per Proxy instance. Co-Authored-By: Virgil --- proxy.go | 30 +++++++++--------- proxy_runtime.go | 16 +++++----- proxy_runtime_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 proxy_runtime_test.go diff --git a/proxy.go b/proxy.go index f5e60c6..686176c 100644 --- a/proxy.go +++ b/proxy.go @@ -13,6 +13,7 @@ package proxy import ( "net/http" "sync" + "sync/atomic" "time" ) @@ -22,20 +23,21 @@ import ( // p, result := proxy.New(cfg) // if result.OK { p.Start() } type Proxy struct { - config *Config - customDiff *CustomDiff - rateLimiter *RateLimiter - splitter Splitter - stats *Stats - workers *Workers - events *EventBus - miners map[int64]*Miner - minerMu sync.RWMutex - servers []*Server - httpServer *http.Server - ticker *time.Ticker - watcher *ConfigWatcher - done chan struct{} + config *Config + customDiff *CustomDiff + rateLimiter *RateLimiter + splitter Splitter + stats *Stats + workers *Workers + events *EventBus + currentMiners atomic.Uint64 + miners map[int64]*Miner + minerMu sync.RWMutex + servers []*Server + httpServer *http.Server + ticker *time.Ticker + watcher *ConfigWatcher + done chan struct{} } // Splitter is the interface both NonceSplitter and SimpleSplitter satisfy. diff --git a/proxy_runtime.go b/proxy_runtime.go index d4059b5..1502f1b 100644 --- a/proxy_runtime.go +++ b/proxy_runtime.go @@ -3,12 +3,9 @@ package proxy import ( "crypto/tls" "net" - "sync/atomic" "time" ) -var proxyMinerCount atomic.Uint64 - type splitterShutdown interface { PendingCount() int Disconnect() @@ -51,7 +48,7 @@ func New(cfg *Config) (*Proxy, error) { proxyValue.minerMu.Unlock() } stats.connections.Add(1) - current := proxyMinerCount.Add(1) + current := proxyValue.currentMiners.Add(1) for { maximum := stats.maxMiners.Load() if current <= maximum || stats.maxMiners.CompareAndSwap(maximum, current) { @@ -65,8 +62,8 @@ func New(cfg *Config) (*Proxy, error) { delete(proxyValue.miners, event.Miner.ID()) proxyValue.minerMu.Unlock() } - if proxyMinerCount.Load() > 0 { - proxyMinerCount.Add(^uint64(0)) + if proxyValue.currentMiners.Load() > 0 { + proxyValue.currentMiners.Add(^uint64(0)) } }) events.Subscribe(EventAccept, stats.OnAccept) @@ -224,8 +221,10 @@ func (p *Proxy) Reload(cfg *Config) { p.config = cfg } else { sourcePath := p.config.sourcePath + bind := append([]BindAddr(nil), p.config.Bind...) *p.config = *cfg p.config.sourcePath = sourcePath + p.config.Bind = bind } if p.customDiff != nil { p.customDiff.SetGlobalDiff(p.config.CustomDiff) @@ -266,7 +265,10 @@ func (p *Proxy) Miners() []*Miner { } func (p *Proxy) CurrentMiners() uint64 { - return proxyMinerCount.Load() + if p == nil { + return 0 + } + return p.currentMiners.Load() } func (p *Proxy) MaxMiners() uint64 { diff --git a/proxy_runtime_test.go b/proxy_runtime_test.go new file mode 100644 index 0000000..3b52f5e --- /dev/null +++ b/proxy_runtime_test.go @@ -0,0 +1,71 @@ +package proxy + +import "testing" + +func TestProxy_Reload_Good(t *testing.T) { + cfg := &Config{ + Mode: "noop", + Bind: []BindAddr{{Host: "127.0.0.1", Port: 3333}}, + Pools: []PoolConfig{{URL: "pool-a:3333", Enabled: true}}, + CustomDiff: 100, + Workers: WorkersDisabled, + } + proxyValue, errorValue := New(cfg) + if errorValue != nil { + t.Fatal(errorValue) + } + + reloadCfg := &Config{ + Mode: "simple", + Bind: []BindAddr{{Host: "0.0.0.0", Port: 4444}}, + Pools: []PoolConfig{{URL: "pool-b:4444", Enabled: true}}, + CustomDiff: 250, + Workers: WorkersByUser, + } + + proxyValue.Reload(reloadCfg) + + if len(proxyValue.config.Bind) != 1 || proxyValue.config.Bind[0].Port != 3333 { + t.Fatalf("expected bind addresses to remain unchanged, got %+v", proxyValue.config.Bind) + } + if len(proxyValue.config.Pools) != 1 || proxyValue.config.Pools[0].URL != "pool-b:4444" { + t.Fatalf("expected pools to reload, got %+v", proxyValue.config.Pools) + } + if proxyValue.config.CustomDiff != 250 { + t.Fatalf("expected custom diff to reload, got %d", proxyValue.config.CustomDiff) + } + if proxyValue.customDiff == nil || proxyValue.customDiff.globalDiff != 250 { + t.Fatalf("expected live custom diff to update, got %+v", proxyValue.customDiff) + } +} + +func TestProxy_CurrentMiners_Good(t *testing.T) { + cfg := &Config{ + Mode: "noop", + Bind: []BindAddr{{Host: "127.0.0.1", Port: 3333}}, + Pools: []PoolConfig{{URL: "pool-a:3333", Enabled: true}}, + Workers: WorkersDisabled, + } + firstProxy, errorValue := New(cfg) + if errorValue != nil { + t.Fatal(errorValue) + } + secondProxy, errorValue := New(cfg) + if errorValue != nil { + t.Fatal(errorValue) + } + + miner := &Miner{} + firstProxy.events.Dispatch(Event{Type: EventLogin, Miner: miner}) + if got := firstProxy.CurrentMiners(); got != 1 { + t.Fatalf("expected first proxy miner count 1, got %d", got) + } + if got := secondProxy.CurrentMiners(); got != 0 { + t.Fatalf("expected second proxy miner count 0, got %d", got) + } + + firstProxy.events.Dispatch(Event{Type: EventClose, Miner: miner}) + if got := firstProxy.CurrentMiners(); got != 0 { + t.Fatalf("expected first proxy miner count to return to 0, got %d", got) + } +}