diff --git a/splitter/nicehash/impl.go b/splitter/nicehash/impl.go index a96f7a8..001f466 100644 --- a/splitter/nicehash/impl.go +++ b/splitter/nicehash/impl.go @@ -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. diff --git a/splitter/nicehash/storage.go b/splitter/nicehash/storage.go index da36e3a..a97d24d 100644 --- a/splitter/nicehash/storage.go +++ b/splitter/nicehash/storage.go @@ -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 } diff --git a/splitter/nicehash/storage_test.go b/splitter/nicehash/storage_test.go index 9805a0a..ab5da9a 100644 --- a/splitter/nicehash/storage_test.go +++ b/splitter/nicehash/storage_test.go @@ -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") + } +} diff --git a/splitter/simple/impl.go b/splitter/simple/impl.go index f76fdd8..e7b945f 100644 --- a/splitter/simple/impl.go +++ b/splitter/simple/impl.go @@ -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{} diff --git a/splitter/simple/impl_test.go b/splitter/simple/impl_test.go index 78e3557..5fac90e 100644 --- a/splitter/simple/impl_test.go +++ b/splitter/simple/impl_test.go @@ -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)) + } +}