go-proxy/pool/strategy.go
Virgil 3376cea600 fix(pool): restore failover and stratum job handling
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 11:13:41 +00:00

197 lines
4.3 KiB
Go

package pool
import (
"sync"
"time"
"dappco.re/go/core/proxy"
)
// FailoverStrategy wraps an ordered slice of PoolConfig entries.
// It connects to the first enabled pool and fails over in order on error.
// On reconnect it always retries from the primary first.
//
// strategy := pool.NewFailoverStrategy(cfg.Pools, listener, cfg)
// strategy.Connect()
type FailoverStrategy struct {
pools []proxy.PoolConfig
current int
client *StratumClient
listener StratumListener
cfg *proxy.Config
closed bool
running bool
mu sync.Mutex
}
// StrategyFactory creates a new FailoverStrategy for a given StratumListener.
// Used by splitters to create per-mapper strategies without coupling to Config.
//
// factory := pool.NewStrategyFactory(cfg)
// strategy := factory(listener) // each mapper calls this
type StrategyFactory func(listener StratumListener) Strategy
// Strategy is the interface the splitters use to submit shares and check pool state.
type Strategy interface {
Connect()
Submit(jobID, nonce, result, algo string) int64
Disconnect()
IsActive() bool
}
// NewStrategyFactory captures the pool list and retry settings for later mapper creation.
//
// factory := pool.NewStrategyFactory(cfg)
func NewStrategyFactory(cfg *proxy.Config) StrategyFactory {
return func(listener StratumListener) Strategy {
if cfg == nil {
return NewFailoverStrategy(nil, listener, nil)
}
return NewFailoverStrategy(cfg.Pools, listener, cfg)
}
}
// NewFailoverStrategy stores the pool list and listener.
//
// strategy := pool.NewFailoverStrategy(cfg.Pools, listener, cfg)
func NewFailoverStrategy(pools []proxy.PoolConfig, listener StratumListener, cfg *proxy.Config) *FailoverStrategy {
return &FailoverStrategy{
pools: append([]proxy.PoolConfig(nil), pools...),
listener: listener,
cfg: cfg,
}
}
// Connect dials the current pool. On failure, advances to the next pool.
//
// strategy.Connect()
func (s *FailoverStrategy) Connect() {
s.mu.Lock()
s.closed = false
s.mu.Unlock()
s.connectFrom(0)
}
func (s *FailoverStrategy) connectFrom(start int) {
s.mu.Lock()
if s.running || s.closed {
s.mu.Unlock()
return
}
s.running = true
s.mu.Unlock()
defer func() {
s.mu.Lock()
s.running = false
s.mu.Unlock()
}()
pools := s.pools
if s.cfg != nil && len(s.cfg.Pools) > 0 {
pools = s.cfg.Pools
}
if len(pools) == 0 {
return
}
retries := 1
pause := time.Duration(0)
if s.cfg != nil {
if s.cfg.Retries > 0 {
retries = s.cfg.Retries
}
if s.cfg.RetryPause > 0 {
pause = time.Duration(s.cfg.RetryPause) * time.Second
}
}
for attempt := 0; attempt < retries; attempt++ {
for offset := 0; offset < len(pools); offset++ {
index := (start + offset) % len(pools)
poolConfig := pools[index]
if !poolConfig.Enabled {
continue
}
client := NewStratumClient(poolConfig, s)
if errorValue := client.Connect(); errorValue == nil {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
client.Disconnect()
return
}
s.client = client
s.current = index
s.mu.Unlock()
client.Login()
return
}
}
if pause > 0 && attempt < retries-1 {
time.Sleep(pause)
}
}
}
func (s *FailoverStrategy) Submit(jobID string, nonce string, result string, algo string) int64 {
s.mu.Lock()
client := s.client
s.mu.Unlock()
if client == nil {
return 0
}
return client.Submit(jobID, nonce, result, algo)
}
func (s *FailoverStrategy) Disconnect() {
s.mu.Lock()
s.closed = true
client := s.client
s.client = nil
s.mu.Unlock()
if client != nil {
client.Disconnect()
}
}
func (s *FailoverStrategy) IsActive() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.client != nil && s.client.active
}
func (s *FailoverStrategy) OnJob(job proxy.Job) {
if s.listener != nil {
s.listener.OnJob(job)
}
}
func (s *FailoverStrategy) OnResultAccepted(sequence int64, accepted bool, errorMessage string) {
if s.listener != nil {
s.listener.OnResultAccepted(sequence, accepted, errorMessage)
}
}
func (s *FailoverStrategy) OnDisconnect() {
s.mu.Lock()
client := s.client
s.client = nil
closed := s.closed
s.mu.Unlock()
if s.listener != nil {
s.listener.OnDisconnect()
}
if closed {
return
}
if client != nil {
client.active = false
}
go func() {
time.Sleep(10 * time.Millisecond)
s.connectFrom(0)
}()
}