Files
AetherForge/server/internal/pool/manager.go

210 lines
5.1 KiB
Go

package pool
import (
"fmt"
"log"
"sync"
"time"
)
// Manager maintains Stratum connections keyed by forged pool + wallet settings.
type Manager struct {
mu sync.RWMutex
pools map[string]*Proxy
onJob func(job *Job)
onErr func(err error)
reconnectDelay time.Duration
verboseTraffic bool
}
func NewManager(onJob func(job *Job), onErr func(err error)) *Manager {
return &Manager{
pools: make(map[string]*Proxy),
onJob: onJob,
onErr: onErr,
}
}
func (m *Manager) SetReconnectDelay(seconds int) {
if seconds > 0 {
m.mu.Lock()
m.reconnectDelay = time.Duration(seconds) * time.Second
m.mu.Unlock()
}
}
func (m *Manager) SetVerboseTraffic(enabled bool) {
m.mu.Lock()
m.verboseTraffic = enabled
for _, p := range m.pools {
p.SetVerboseTraffic(enabled)
}
m.mu.Unlock()
}
func poolKey(cfg *Config) string {
return fmt.Sprintf("%s:%d:tls=%v:wallet=%s", cfg.Host, cfg.Port, cfg.UseTLS, cfg.Wallet)
}
// EnsurePoolWithBackups connects to cfg and registers backup pool configs for
// automatic failover on reconnect. If the primary cfg is unreachable it tries
// each backup in order at initial connect time as well — restoring the behaviour
// that existed before the reconnect-rotation refactor.
func (m *Manager) EnsurePoolWithBackups(cfg *Config, backups []Config) (*Proxy, error) {
// Try primary first.
p, err := m.EnsurePool(cfg)
if err == nil {
// Primary connected — register backups for later reconnect rotation.
if len(backups) > 0 {
p.SetBackupConfigs(backups)
}
return p, nil
}
// Primary failed — work through the backup list.
log.Printf("[PoolManager] Primary pool unreachable (%v), trying %d backup(s)…", err, len(backups))
for i, bp := range backups {
if bp.Host == "" || bp.Port <= 0 {
continue
}
bpCopy := bp // local copy so we can take its address
p2, err2 := m.EnsurePool(&bpCopy)
if err2 == nil {
log.Printf("[PoolManager] Connected to backup pool #%d (%s:%d)", i+1, bp.Host, bp.Port)
// Register remaining backups (skip the one we just connected to).
remaining := make([]Config, 0, len(backups))
remaining = append(remaining, *cfg) // original primary becomes a backup
for j, b := range backups {
if j != i {
remaining = append(remaining, b)
}
}
p2.SetBackupConfigs(remaining)
return p2, nil
}
log.Printf("[PoolManager] Backup pool #%d (%s:%d) also failed: %v", i+1, bp.Host, bp.Port, err2)
}
return nil, fmt.Errorf("all pool endpoints unreachable (primary: %w)", err)
}
// EnsurePool returns a connected proxy for the forged pool settings, starting one if needed.
func (m *Manager) EnsurePool(cfg *Config) (*Proxy, error) {
if cfg == nil || cfg.Host == "" {
return nil, fmt.Errorf("pool host is required")
}
if cfg.Wallet == "" {
return nil, fmt.Errorf("pool wallet is required")
}
if cfg.Port <= 0 {
cfg.Port = 3333
}
if cfg.Password == "" {
cfg.Password = "x"
}
key := poolKey(cfg)
m.mu.RLock()
if p, ok := m.pools[key]; ok && p.IsConnected() {
m.mu.RUnlock()
return p, nil
}
m.mu.RUnlock()
m.mu.Lock()
defer m.mu.Unlock()
if p, ok := m.pools[key]; ok {
if p.IsConnected() {
return p, nil
}
p.Stop()
delete(m.pools, key)
}
p := NewProxy(cfg)
p.SetCallbacks(m.onJob, nil, m.onErr)
m.mu.RLock()
delay := m.reconnectDelay
verbose := m.verboseTraffic
m.mu.RUnlock()
p.SetVerboseTraffic(verbose)
if delay > 0 {
p.SetReconnectDelay(delay)
}
if err := p.Start(); err != nil {
return nil, err
}
m.pools[key] = p
log.Printf("[PoolManager] Started pool %s for wallet %s…", key, truncateWallet(cfg.Wallet, 12))
return p, nil
}
// GetPool returns an existing proxy for forged settings without starting a new one.
func (m *Manager) GetPool(cfg *Config) *Proxy {
if cfg == nil || cfg.Host == "" || cfg.Wallet == "" {
return nil
}
if cfg.Port <= 0 {
cfg.Port = 3333
}
key := poolKey(cfg)
m.mu.RLock()
defer m.mu.RUnlock()
return m.pools[key]
}
func truncateWallet(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}
// PoolStatus describes a forged upstream Stratum connection.
type PoolStatus struct {
Key string `json:"key"`
Host string `json:"host"`
Port int `json:"port"`
UseTLS bool `json:"use_tls"`
Wallet string `json:"wallet"`
Connected bool `json:"connected"`
Status string `json:"status"` // green, yellow, red
}
func poolStatusLevel(connected bool, hasJob bool) string {
if !connected {
return "red"
}
if hasJob {
return "green"
}
return "yellow"
}
// ListStatus returns connection status for all managed pool proxies.
func (m *Manager) ListStatus() []PoolStatus {
m.mu.RLock()
defer m.mu.RUnlock()
out := make([]PoolStatus, 0, len(m.pools))
for key, p := range m.pools {
cfg := p.Config()
connected := p.IsConnected()
job := p.GetCurrentJob()
hasJob := job != nil && job.Blob != ""
status := poolStatusLevel(connected, hasJob)
out = append(out, PoolStatus{
Key: key,
Host: cfg.Host,
Port: cfg.Port,
UseTLS: cfg.UseTLS,
Wallet: truncateWallet(cfg.Wallet, 16),
Connected: connected,
Status: status,
})
}
return out
}