fix(splitter): honour reuse timeout and stale jobs

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-04 19:15:14 +00:00
parent 4a281e6e25
commit c250a4d6f2
5 changed files with 65 additions and 3 deletions

View file

@ -290,6 +290,7 @@ func (m *NonceMapper) OnResultAccepted(sequence int64, accepted bool, errorMessa
job := m.storage.job
prevJob := m.storage.prevJob
m.storage.mu.Unlock()
_ = m.storage.IsValidJobID(ctx.JobID)
expired := ctx.JobID != "" && ctx.JobID == prevJob.JobID && ctx.JobID != job.JobID
m.mu.Unlock()
if !ok || miner == nil {
@ -400,7 +401,17 @@ func (s *NonceStorage) IsValidJobID(id string) bool {
}
s.mu.Lock()
defer s.mu.Unlock()
return id != "" && (id == s.job.JobID || id == s.prevJob.JobID)
if id == "" {
return false
}
if id == s.job.JobID {
return true
}
if id == s.prevJob.JobID && s.prevJob.JobID != "" {
s.expired++
return true
}
return false
}
// SlotCount returns free, dead, and active counts.

View file

@ -20,6 +20,7 @@ type NonceStorage struct {
miners map[int64]*proxy.Miner // minerID → Miner pointer for active miners
job proxy.Job // current job from pool
prevJob proxy.Job // previous job (for stale submit validation)
cursor int // search starts here (round-robin allocation)
expired uint64
cursor int // search starts here (round-robin allocation)
mu sync.Mutex
}

View file

@ -24,3 +24,22 @@ func TestNonceStorage_AddAndRemove(t *testing.T) {
t.Fatalf("unexpected slot counts: free=%d dead=%d active=%d", free, dead, active)
}
}
func TestNonceStorage_IsValidJobID_Ugly(t *testing.T) {
storage := NewNonceStorage()
storage.job = proxy.Job{JobID: "job-2"}
storage.prevJob = proxy.Job{JobID: "job-1"}
if !storage.IsValidJobID("job-2") {
t.Fatalf("expected current job to be valid")
}
if !storage.IsValidJobID("job-1") {
t.Fatalf("expected previous job to remain valid")
}
if storage.expired != 1 {
t.Fatalf("expected one expired job validation, got %d", storage.expired)
}
if storage.IsValidJobID("") {
t.Fatalf("expected empty job id to be invalid")
}
}

View file

@ -53,10 +53,11 @@ func (s *SimpleSplitter) OnLogin(event *proxy.LoginEvent) {
}
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
if s.cfg.ReuseTimeout > 0 {
for id, mapper := range s.idle {
if mapper.strategy != nil && mapper.strategy.IsActive() {
if mapper.strategy != nil && mapper.strategy.IsActive() && !mapper.idleAt.IsZero() && now.Sub(mapper.idleAt) <= time.Duration(s.cfg.ReuseTimeout)*time.Second {
delete(s.idle, id)
mapper.miner = event.Miner
mapper.idleAt = time.Time{}

View file

@ -2,6 +2,7 @@ package simple
import (
"testing"
"time"
"dappco.re/go/proxy"
"dappco.re/go/proxy/pool"
@ -24,6 +25,7 @@ func TestSimpleSplitter_OnLogin_Good(t *testing.T) {
id: 7,
strategy: activeStrategy{},
currentJob: job,
idleAt: time.Now(),
}
splitter.idle[mapper.id] = mapper
@ -36,3 +38,31 @@ func TestSimpleSplitter_OnLogin_Good(t *testing.T) {
t.Fatalf("expected current job to be restored on reuse, got %q", got)
}
}
func TestSimpleSplitter_OnLogin_Ugly(t *testing.T) {
splitter := NewSimpleSplitter(&proxy.Config{ReuseTimeout: 30}, nil, func(listener pool.StratumListener) pool.Strategy {
return activeStrategy{}
})
miner := &proxy.Miner{}
expired := &SimpleMapper{
id: 7,
strategy: activeStrategy{},
idleAt: time.Now().Add(-time.Minute),
}
splitter.idle[expired.id] = expired
splitter.OnLogin(&proxy.LoginEvent{Miner: miner})
if miner.RouteID() == expired.id {
t.Fatalf("expected expired mapper not to be reclaimed")
}
if miner.RouteID() != 0 {
t.Fatalf("expected a new mapper to be allocated, got route id %d", miner.RouteID())
}
if len(splitter.active) != 1 {
t.Fatalf("expected one active mapper, got %d", len(splitter.active))
}
if len(splitter.idle) != 1 {
t.Fatalf("expected expired mapper to remain idle until GC, got %d idle mappers", len(splitter.idle))
}
}