Improve portable launch, forge persistence, and operator auth UX.
Persist build extra_files for Build Manager history, print dashboard login on every start, add libp2p for Mesh P2P forge, defer WebSocket until login, and split devrun.bat from LAUNCH.bat with USB deck auto-detection.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package maintenance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
@@ -15,21 +17,47 @@ var retentionTickInterval = 6 * time.Hour
|
||||
// runRetentionFn is the work function invoked by StartRetentionJobs (overridable in tests).
|
||||
var runRetentionFn = runRetention
|
||||
|
||||
var (
|
||||
retentionMu sync.Mutex
|
||||
retentionCancel context.CancelFunc
|
||||
)
|
||||
|
||||
// StartRetentionJobs purges old stats and build artifacts on an interval.
|
||||
func StartRetentionJobs(database *db.Database, dataDir string, statsHours, buildDays int) {
|
||||
if statsHours <= 0 && buildDays <= 0 {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
retentionMu.Lock()
|
||||
retentionCancel = cancel
|
||||
retentionMu.Unlock()
|
||||
|
||||
go func() {
|
||||
runRetentionFn(database, dataDir, statsHours, buildDays)
|
||||
ticker := time.NewTicker(retentionTickInterval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
runRetentionFn(database, dataDir, statsHours, buildDays)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
runRetentionFn(database, dataDir, statsHours, buildDays)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// StopRetentionJobs stops the background retention loop started by StartRetentionJobs.
|
||||
func StopRetentionJobs() {
|
||||
retentionMu.Lock()
|
||||
cancel := retentionCancel
|
||||
retentionCancel = nil
|
||||
retentionMu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func runRetention(database *db.Database, dataDir string, statsHours, buildDays int) {
|
||||
if statsHours > 0 {
|
||||
cutoff := time.Now().Add(-time.Duration(statsHours) * time.Hour)
|
||||
|
||||
@@ -44,6 +44,7 @@ func seedHashrateSample(t *testing.T, d *db.Database, agentID string, ts time.Ti
|
||||
|
||||
func TestStartRetentionJobs_NoOpWhenDisabled(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
t.Cleanup(StopRetentionJobs)
|
||||
StartRetentionJobs(d, t.TempDir(), 0, 0)
|
||||
// Disabled config must not start a goroutine that mutates data.
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
@@ -53,6 +54,7 @@ func TestStartRetentionJobs_RunsImmediately(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
seedHashrateSample(t, d, "a1", time.Now().Add(-48*time.Hour), 100)
|
||||
|
||||
t.Cleanup(StopRetentionJobs)
|
||||
StartRetentionJobs(d, t.TempDir(), 24, 0)
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
@@ -81,8 +83,8 @@ func TestStartRetentionJobs_TickerInterval(t *testing.T) {
|
||||
runRetentionFn = noopRetention
|
||||
t.Cleanup(func() {
|
||||
retentionTickInterval = prev
|
||||
// StartRetentionJobs has no stop handle; leave a noop so the leaked goroutine is harmless.
|
||||
runRetentionFn = func(database *db.Database, dataDir string, statsHours, buildDays int) {}
|
||||
runRetentionFn = runRetention
|
||||
StopRetentionJobs()
|
||||
})
|
||||
|
||||
StartRetentionJobs(d, t.TempDir(), 1, 0)
|
||||
@@ -97,6 +99,42 @@ func TestStartRetentionJobs_TickerInterval(t *testing.T) {
|
||||
t.Fatalf("expected at least 2 retention passes (immediate + tick), got %d", passes)
|
||||
}
|
||||
|
||||
func TestStopRetentionJobs_StopsBackgroundLoop(t *testing.T) {
|
||||
prev := retentionTickInterval
|
||||
retentionTickInterval = 40 * time.Millisecond
|
||||
t.Cleanup(func() {
|
||||
retentionTickInterval = prev
|
||||
runRetentionFn = runRetention
|
||||
StopRetentionJobs()
|
||||
})
|
||||
|
||||
d := openTestDB(t)
|
||||
var passes int32
|
||||
runRetentionFn = func(database *db.Database, dataDir string, statsHours, buildDays int) {
|
||||
atomic.AddInt32(&passes, 1)
|
||||
}
|
||||
|
||||
StartRetentionJobs(d, t.TempDir(), 1, 0)
|
||||
|
||||
deadline := time.Now().Add(250 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
if atomic.LoadInt32(&passes) >= 2 {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if atomic.LoadInt32(&passes) < 2 {
|
||||
t.Fatalf("expected at least 2 passes before stop, got %d", passes)
|
||||
}
|
||||
|
||||
before := atomic.LoadInt32(&passes)
|
||||
StopRetentionJobs()
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
if got := atomic.LoadInt32(&passes); got != before {
|
||||
t.Fatalf("expected no retention passes after stop, before=%d after=%d", before, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRetention_PurgesHashrateSamples(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
seedHashrateSample(t, d, "a1", time.Now().Add(-48*time.Hour), 100)
|
||||
|
||||
Reference in New Issue
Block a user