XMRig/TTMiner mining engine with profile management, stats collection,
circuit breakers, event system, supervisor, and SQLite persistence.
P2P node service stubbed (moved to core/go-p2p).
Ported from github.com/Snider/Mining/pkg/{mining,database,logging}
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
277 lines
5.9 KiB
Go
277 lines
5.9 KiB
Go
package database
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// setupRaceTestDB creates a fresh database for race testing
|
|
func setupRaceTestDB(t *testing.T) func() {
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "race_test.db")
|
|
|
|
cfg := Config{
|
|
Enabled: true,
|
|
Path: dbPath,
|
|
RetentionDays: 7,
|
|
}
|
|
|
|
if err := Initialize(cfg); err != nil {
|
|
t.Fatalf("Failed to initialize database: %v", err)
|
|
}
|
|
|
|
return func() {
|
|
Close()
|
|
os.Remove(dbPath)
|
|
}
|
|
}
|
|
|
|
// TestConcurrentHashrateInserts verifies that concurrent inserts
|
|
// don't cause race conditions
|
|
func TestConcurrentHashrateInserts(t *testing.T) {
|
|
cleanup := setupRaceTestDB(t)
|
|
defer cleanup()
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// 10 goroutines inserting points concurrently
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(minerIndex int) {
|
|
defer wg.Done()
|
|
minerName := "miner" + string(rune('A'+minerIndex))
|
|
minerType := "xmrig"
|
|
|
|
for j := 0; j < 100; j++ {
|
|
point := HashratePoint{
|
|
Timestamp: time.Now().Add(time.Duration(-j) * time.Second),
|
|
Hashrate: 1000 + minerIndex*100 + j,
|
|
}
|
|
err := InsertHashratePoint(nil, minerName, minerType, point, ResolutionHigh)
|
|
if err != nil {
|
|
t.Errorf("Insert error for %s: %v", minerName, err)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify data was inserted
|
|
for i := 0; i < 10; i++ {
|
|
minerName := "miner" + string(rune('A'+i))
|
|
history, err := GetHashrateHistory(minerName, ResolutionHigh, time.Now().Add(-2*time.Minute), time.Now())
|
|
if err != nil {
|
|
t.Errorf("Failed to get history for %s: %v", minerName, err)
|
|
}
|
|
if len(history) == 0 {
|
|
t.Errorf("Expected history for %s, got none", minerName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestConcurrentInsertAndQuery verifies that concurrent reads and writes
|
|
// don't cause race conditions
|
|
func TestConcurrentInsertAndQuery(t *testing.T) {
|
|
cleanup := setupRaceTestDB(t)
|
|
defer cleanup()
|
|
|
|
var wg sync.WaitGroup
|
|
stop := make(chan struct{})
|
|
|
|
// Writer goroutine
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; ; i++ {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
point := HashratePoint{
|
|
Timestamp: time.Now(),
|
|
Hashrate: 1000 + i,
|
|
}
|
|
InsertHashratePoint(nil, "concurrent-test", "xmrig", point, ResolutionHigh)
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Multiple reader goroutines
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 50; j++ {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
GetHashrateHistory("concurrent-test", ResolutionHigh, time.Now().Add(-time.Hour), time.Now())
|
|
time.Sleep(2 * time.Millisecond)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Let it run for a bit
|
|
time.Sleep(200 * time.Millisecond)
|
|
close(stop)
|
|
wg.Wait()
|
|
|
|
// Test passes if no race detector warnings
|
|
}
|
|
|
|
// TestConcurrentInsertAndCleanup verifies that cleanup doesn't race
|
|
// with ongoing inserts
|
|
func TestConcurrentInsertAndCleanup(t *testing.T) {
|
|
cleanup := setupRaceTestDB(t)
|
|
defer cleanup()
|
|
|
|
var wg sync.WaitGroup
|
|
stop := make(chan struct{})
|
|
|
|
// Continuous inserts
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; ; i++ {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
// Insert some old data and some new data
|
|
oldPoint := HashratePoint{
|
|
Timestamp: time.Now().AddDate(0, 0, -10), // 10 days old
|
|
Hashrate: 500 + i,
|
|
}
|
|
InsertHashratePoint(nil, "cleanup-test", "xmrig", oldPoint, ResolutionHigh)
|
|
|
|
newPoint := HashratePoint{
|
|
Timestamp: time.Now(),
|
|
Hashrate: 1000 + i,
|
|
}
|
|
InsertHashratePoint(nil, "cleanup-test", "xmrig", newPoint, ResolutionHigh)
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Periodic cleanup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 10; i++ {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
Cleanup(7) // 7 day retention
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Let it run
|
|
time.Sleep(200 * time.Millisecond)
|
|
close(stop)
|
|
wg.Wait()
|
|
|
|
// Test passes if no race detector warnings
|
|
}
|
|
|
|
// TestConcurrentStats verifies that GetHashrateStats can be called
|
|
// concurrently without race conditions
|
|
func TestConcurrentStats(t *testing.T) {
|
|
cleanup := setupRaceTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Insert some test data
|
|
minerName := "stats-test"
|
|
for i := 0; i < 100; i++ {
|
|
point := HashratePoint{
|
|
Timestamp: time.Now().Add(time.Duration(-i) * time.Second),
|
|
Hashrate: 1000 + i*10,
|
|
}
|
|
InsertHashratePoint(nil, minerName, "xmrig", point, ResolutionHigh)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Multiple goroutines querying stats
|
|
for i := 0; i < 20; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 50; j++ {
|
|
stats, err := GetHashrateStats(minerName)
|
|
if err != nil {
|
|
t.Errorf("Stats error: %v", err)
|
|
}
|
|
if stats != nil && stats.TotalPoints == 0 {
|
|
// This is fine, data might be in flux
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Test passes if no race detector warnings
|
|
}
|
|
|
|
// TestConcurrentGetAllStats verifies that GetAllMinerStats can be called
|
|
// concurrently without race conditions
|
|
func TestConcurrentGetAllStats(t *testing.T) {
|
|
cleanup := setupRaceTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Insert data for multiple miners
|
|
for m := 0; m < 5; m++ {
|
|
minerName := "all-stats-" + string(rune('A'+m))
|
|
for i := 0; i < 50; i++ {
|
|
point := HashratePoint{
|
|
Timestamp: time.Now().Add(time.Duration(-i) * time.Second),
|
|
Hashrate: 1000 + m*100 + i,
|
|
}
|
|
InsertHashratePoint(nil, minerName, "xmrig", point, ResolutionHigh)
|
|
}
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Multiple goroutines querying all stats
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 30; j++ {
|
|
_, err := GetAllMinerStats()
|
|
if err != nil {
|
|
t.Errorf("GetAllMinerStats error: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Concurrent inserts
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 50; i++ {
|
|
point := HashratePoint{
|
|
Timestamp: time.Now(),
|
|
Hashrate: 2000 + i,
|
|
}
|
|
InsertHashratePoint(nil, "all-stats-new", "xmrig", point, ResolutionHigh)
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
// Test passes if no race detector warnings
|
|
}
|