Mining/pkg/mining/ttminer_stats.go
snider 1351dc7562 fix: Address race conditions and network blocking issues
Critical fixes:
- Release mutex before HTTP calls in GetStats() to prevent blocking
- Fix m.cmd race between Stop() and Wait() goroutine by capturing locally
- Add context support to GetStats() for proper request cancellation

High priority fixes:
- Add existence check in collectMinerStats() before operating on miners
- Add mutex-protected httpClient getter/setter for thread-safe test mocking

Changes:
- Miner interface now requires context.Context for GetStats()
- Stats HTTP requests timeout after 5 seconds (was 30s client default)
- All callers updated to pass context (service uses request context)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 01:55:24 +00:00

81 lines
2.1 KiB
Go

package mining
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
)
// GetStats retrieves performance metrics from the TT-Miner API.
func (m *TTMiner) GetStats(ctx context.Context) (*PerformanceMetrics, error) {
// Read state under RLock, then release before HTTP call
m.mu.RLock()
if !m.Running {
m.mu.RUnlock()
return nil, errors.New("miner is not running")
}
if m.API == nil || m.API.ListenPort == 0 {
m.mu.RUnlock()
return nil, errors.New("miner API not configured or port is zero")
}
host := m.API.ListenHost
port := m.API.ListenPort
m.mu.RUnlock()
// Create request with context and timeout
reqCtx, cancel := context.WithTimeout(ctx, statsTimeout)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, "GET", fmt.Sprintf("http://%s:%d/summary", host, port), nil)
if err != nil {
return nil, err
}
// HTTP call outside the lock to avoid blocking other operations
resp, err := getHTTPClient().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get stats: unexpected status code %d", resp.StatusCode)
}
var summary TTMinerSummary
if err := json.NewDecoder(resp.Body).Decode(&summary); err != nil {
return nil, err
}
// Store the full summary in the miner struct (requires lock)
m.mu.Lock()
m.FullStats = &summary
m.mu.Unlock()
// Calculate total hashrate from all GPUs
var totalHashrate float64
if len(summary.Hashrate.Total) > 0 {
totalHashrate = summary.Hashrate.Total[0]
} else {
// Sum individual GPU hashrates
for _, gpu := range summary.GPUs {
totalHashrate += gpu.Hashrate
}
}
// For TT-Miner, we use the connection difficulty as both current and avg
// since TT-Miner doesn't expose per-share difficulty data
diffCurrent := summary.Connection.Diff
return &PerformanceMetrics{
Hashrate: int(totalHashrate),
Shares: summary.Results.SharesGood,
Rejected: summary.Results.SharesTotal - summary.Results.SharesGood,
Uptime: summary.Uptime,
Algorithm: summary.Algo,
AvgDifficulty: diffCurrent, // Use pool diff as approximation
DiffCurrent: diffCurrent,
}, nil
}