go-proxy/splitter/nicehash/splitter.go
Virgil e41ad7ef2e feat(proxy): dispatch submits and drain shutdown
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 11:18:35 +00:00

216 lines
4.7 KiB
Go

// Package nicehash implements the NiceHash nonce-splitting mode.
//
// It partitions the 32-bit nonce space among miners by fixing one byte (byte 39
// of the blob). Each upstream pool connection (NonceMapper) owns a 256-slot table.
// Up to 256 miners share one pool connection. The 257th miner triggers creation
// of a new NonceMapper with a new pool connection.
//
// s := nicehash.NewNonceSplitter(cfg, eventBus, strategyFactory)
// s.Connect()
package nicehash
import (
"sync"
"time"
"dappco.re/go/core/proxy"
"dappco.re/go/core/proxy/pool"
)
// NonceSplitter is the Splitter implementation for NiceHash mode.
// It manages a dynamic slice of NonceMapper upstreams, creating new ones on demand.
//
// s := nicehash.NewNonceSplitter(cfg, eventBus, strategyFactory)
// s.Connect()
type NonceSplitter struct {
mappers []*NonceMapper
cfg *proxy.Config
events *proxy.EventBus
strategyFactory pool.StrategyFactory
mu sync.RWMutex
}
// NewNonceSplitter creates the NiceHash splitter.
//
// s := nicehash.NewNonceSplitter(cfg, bus, factory)
func NewNonceSplitter(cfg *proxy.Config, events *proxy.EventBus, factory pool.StrategyFactory) *NonceSplitter {
return &NonceSplitter{
cfg: cfg,
events: events,
strategyFactory: factory,
mappers: make([]*NonceMapper, 0, 1),
}
}
func (s *NonceSplitter) Connect() {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.mappers) > 0 {
return
}
mapper := s.newMapperLocked()
s.mappers = append(s.mappers, mapper)
mapper.strategy.Connect()
}
func (s *NonceSplitter) OnLogin(event *proxy.LoginEvent) {
if event == nil || event.Miner == nil {
return
}
s.mu.Lock()
defer s.mu.Unlock()
for _, mapper := range s.mappers {
if mapper.Add(event.Miner) {
mapper.events = s.events
event.Miner.SetMapperID(mapper.id)
event.Miner.SetNiceHashEnabled(true)
if currentJob := mapper.storage.CurrentJob(); currentJob != nil && currentJob.IsValid() {
event.Miner.PrimeJob(*currentJob)
}
return
}
}
mapper := s.newMapperLocked()
s.mappers = append(s.mappers, mapper)
mapper.strategy.Connect()
if mapper.Add(event.Miner) {
mapper.events = s.events
event.Miner.SetMapperID(mapper.id)
event.Miner.SetNiceHashEnabled(true)
if currentJob := mapper.storage.CurrentJob(); currentJob != nil && currentJob.IsValid() {
event.Miner.PrimeJob(*currentJob)
}
}
}
func (s *NonceSplitter) OnSubmit(event *proxy.SubmitEvent) {
if event == nil || event.Miner == nil {
return
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, mapper := range s.mappers {
if mapper.id == event.Miner.MapperID() {
mapper.Submit(event)
return
}
}
}
func (s *NonceSplitter) OnClose(event *proxy.CloseEvent) {
if event == nil || event.Miner == nil {
return
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, mapper := range s.mappers {
if mapper.id == event.Miner.MapperID() {
mapper.Remove(event.Miner)
return
}
}
}
func (s *NonceSplitter) Tick(ticks uint64) {
if ticks%60 == 0 {
s.GC()
}
}
func (s *NonceSplitter) GC() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().UTC()
filtered := s.mappers[:0]
for _, mapper := range s.mappers {
_, _, active := mapper.storage.SlotCount()
if active == 0 && mapper.IdleDuration(now) >= 60*time.Second {
if mapper.strategy != nil {
mapper.strategy.Disconnect()
}
continue
}
filtered = append(filtered, mapper)
}
s.mappers = filtered
}
func (s *NonceSplitter) Upstreams() proxy.UpstreamStats {
s.mu.RLock()
defer s.mu.RUnlock()
var stats proxy.UpstreamStats
for _, mapper := range s.mappers {
stats.Total++
switch {
case mapper.suspended > 0:
stats.Error++
case mapper.IsActive():
stats.Active++
default:
stats.Sleep++
}
}
return stats
}
func (s *NonceSplitter) PendingCount() int {
s.mu.RLock()
mappers := append([]*NonceMapper(nil), s.mappers...)
s.mu.RUnlock()
pending := 0
for _, mapper := range mappers {
if mapper == nil {
continue
}
mapper.mu.Lock()
pending += len(mapper.pending)
mapper.mu.Unlock()
}
return pending
}
func (s *NonceSplitter) Disconnect() {
s.mu.Lock()
mappers := s.mappers
s.mappers = nil
s.mu.Unlock()
for _, mapper := range mappers {
if mapper == nil {
continue
}
mapper.mu.Lock()
strategy := mapper.strategy
mapper.strategy = nil
mapper.active = false
mapper.suspended = 0
mapper.mu.Unlock()
if strategy != nil {
strategy.Disconnect()
}
}
}
func (s *NonceSplitter) newMapperLocked() *NonceMapper {
mapperID := int64(len(s.mappers) + 1)
mapper := NewNonceMapper(mapperID, s.cfg, nil)
mapper.events = s.events
var strategy pool.Strategy
if s.strategyFactory != nil {
strategy = s.strategyFactory(mapper)
}
mapper.strategy = strategy
return mapper
}