Mining/pkg/database/hashrate.go
Claude 0d1b20e177
ax(batch): replace prose comments with usage examples across all packages
Applies AX principle 2 (Comments as Usage Examples) — removes prose
descriptions that restate the function signature ("returns", "retrieves",
"creates", "wraps", etc.) and keeps or replaces with concrete usage
examples showing real calls with realistic values.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-02 18:28:16 +01:00

236 lines
6.2 KiB
Go

package database
import (
"context"
"time"
"forge.lthn.ai/Snider/Mining/pkg/logging"
)
// t := parseSQLiteTimestamp("2006-01-02 15:04:05") // time.Time{...}
// t := parseSQLiteTimestamp("") // time.Time{} (zero)
func parseSQLiteTimestamp(raw string) time.Time {
if raw == "" {
return time.Time{}
}
// Try common SQLite timestamp formats
formats := []string{
"2006-01-02 15:04:05.999999999-07:00",
time.RFC3339Nano,
time.RFC3339,
"2006-01-02 15:04:05",
"2006-01-02T15:04:05Z",
}
for _, format := range formats {
if parsed, err := time.Parse(format, raw); err == nil {
return parsed
}
}
logging.Warn("failed to parse timestamp from database", logging.Fields{"timestamp": raw})
return time.Time{}
}
// database.ResolutionHigh // "high" — 10-second intervals
// database.ResolutionLow // "low" — 1-minute averages
type Resolution string
const (
ResolutionHigh Resolution = "high" // 10-second intervals
ResolutionLow Resolution = "low" // 1-minute averages
)
// point := database.HashratePoint{Timestamp: time.Now(), Hashrate: 1234}
type HashratePoint struct {
Timestamp time.Time `json:"timestamp"`
Hashrate int `json:"hashrate"`
}
// ctx, cancel := context.WithTimeout(ctx, dbInsertTimeout) // 5s ceiling for INSERT
const dbInsertTimeout = 5 * time.Second
// database.InsertHashratePoint(ctx, "xmrig", "xmrig", HashratePoint{Timestamp: time.Now(), Hashrate: 1234}, ResolutionHigh)
func InsertHashratePoint(ctx context.Context, minerName, minerType string, point HashratePoint, resolution Resolution) error {
databaseMutex.RLock()
defer databaseMutex.RUnlock()
if globalDatabase == nil {
return nil // DB not enabled, silently skip
}
// Use provided context or create one with default timeout
if ctx == nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), dbInsertTimeout)
defer cancel()
}
_, err := globalDatabase.ExecContext(ctx, `
INSERT INTO hashrate_history (miner_name, miner_type, timestamp, hashrate, resolution)
VALUES (?, ?, ?, ?, ?)
`, minerName, minerType, point.Timestamp, point.Hashrate, string(resolution))
return err
}
// points, err := database.GetHashrateHistory("xmrig", database.ResolutionHigh, time.Now().Add(-time.Hour), time.Now())
func GetHashrateHistory(minerName string, resolution Resolution, since, until time.Time) ([]HashratePoint, error) {
databaseMutex.RLock()
defer databaseMutex.RUnlock()
if globalDatabase == nil {
return nil, nil
}
rows, err := globalDatabase.Query(`
SELECT timestamp, hashrate
FROM hashrate_history
WHERE miner_name = ?
AND resolution = ?
AND timestamp >= ?
AND timestamp <= ?
ORDER BY timestamp ASC
`, minerName, string(resolution), since, until)
if err != nil {
return nil, databaseError("query hashrate history", err)
}
defer rows.Close()
var points []HashratePoint
for rows.Next() {
var point HashratePoint
if err := rows.Scan(&point.Timestamp, &point.Hashrate); err != nil {
return nil, databaseError("scan row", err)
}
points = append(points, point)
}
return points, rows.Err()
}
// stats, err := database.GetHashrateStats("xmrig")
// if stats != nil { logging.Info("stats", logging.Fields{"average": stats.AverageRate}) }
type HashrateStats struct {
MinerName string `json:"minerName"`
TotalPoints int `json:"totalPoints"`
AverageRate int `json:"averageRate"`
MaxRate int `json:"maxRate"`
MinRate int `json:"minRate"`
FirstSeen time.Time `json:"firstSeen"`
LastSeen time.Time `json:"lastSeen"`
}
// stats, err := database.GetHashrateStats("xmrig")
// if stats != nil { logging.Info("stats", logging.Fields{"miner": minerName, "average": stats.AverageRate}) }
func GetHashrateStats(minerName string) (*HashrateStats, error) {
databaseMutex.RLock()
defer databaseMutex.RUnlock()
if globalDatabase == nil {
return nil, nil
}
// First check if there are any rows for this miner
var count int
err := globalDatabase.QueryRow(`SELECT COUNT(*) FROM hashrate_history WHERE miner_name = ?`, minerName).Scan(&count)
if err != nil {
return nil, err
}
// No data for this miner
if count == 0 {
return nil, nil
}
var stats HashrateStats
stats.MinerName = minerName
// SQLite returns timestamps as strings and AVG as float64, so scan them appropriately
var firstSeenStr, lastSeenStr string
var avgRate float64
err = globalDatabase.QueryRow(`
SELECT
COUNT(*),
COALESCE(AVG(hashrate), 0),
COALESCE(MAX(hashrate), 0),
COALESCE(MIN(hashrate), 0),
MIN(timestamp),
MAX(timestamp)
FROM hashrate_history
WHERE miner_name = ?
`, minerName).Scan(
&stats.TotalPoints,
&avgRate,
&stats.MaxRate,
&stats.MinRate,
&firstSeenStr,
&lastSeenStr,
)
stats.AverageRate = int(avgRate)
if err != nil {
return nil, err
}
// Parse timestamps using helper that logs errors
stats.FirstSeen = parseSQLiteTimestamp(firstSeenStr)
stats.LastSeen = parseSQLiteTimestamp(lastSeenStr)
return &stats, nil
}
// allStats, err := database.GetAllMinerStats()
// for _, stats := range allStats { logging.Info("stats", logging.Fields{"miner": stats.MinerName, "average": stats.AverageRate}) }
func GetAllMinerStats() ([]HashrateStats, error) {
databaseMutex.RLock()
defer databaseMutex.RUnlock()
if globalDatabase == nil {
return nil, nil
}
rows, err := globalDatabase.Query(`
SELECT
miner_name,
COUNT(*),
COALESCE(AVG(hashrate), 0),
COALESCE(MAX(hashrate), 0),
COALESCE(MIN(hashrate), 0),
MIN(timestamp),
MAX(timestamp)
FROM hashrate_history
GROUP BY miner_name
ORDER BY miner_name
`)
if err != nil {
return nil, err
}
defer rows.Close()
var allStats []HashrateStats
for rows.Next() {
var stats HashrateStats
var firstSeenStr, lastSeenStr string
var avgRate float64
if err := rows.Scan(
&stats.MinerName,
&stats.TotalPoints,
&avgRate,
&stats.MaxRate,
&stats.MinRate,
&firstSeenStr,
&lastSeenStr,
); err != nil {
return nil, err
}
stats.AverageRate = int(avgRate)
// Parse timestamps using helper that logs errors
stats.FirstSeen = parseSQLiteTimestamp(firstSeenStr)
stats.LastSeen = parseSQLiteTimestamp(lastSeenStr)
allStats = append(allStats, stats)
}
return allStats, rows.Err()
}