Mining/pkg/database/hashrate.go

234 lines
5.5 KiB
Go
Raw Normal View History

package database
import (
"context"
"fmt"
"time"
"github.com/Snider/Mining/pkg/logging"
)
// parseSQLiteTimestamp parses timestamp strings from SQLite which may use various formats.
// Logs a warning if parsing fails and returns zero time.
func parseSQLiteTimestamp(s string) time.Time {
if s == "" {
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 t, err := time.Parse(format, s); err == nil {
return t
}
}
logging.Warn("failed to parse timestamp from database", logging.Fields{"timestamp": s})
return time.Time{}
}
// Resolution indicates the data resolution type
type Resolution string
const (
ResolutionHigh Resolution = "high" // 10-second intervals
ResolutionLow Resolution = "low" // 1-minute averages
)
// HashratePoint represents a single hashrate measurement
type HashratePoint struct {
Timestamp time.Time `json:"timestamp"`
Hashrate int `json:"hashrate"`
}
// dbInsertTimeout is the maximum time to wait for a database insert operation
const dbInsertTimeout = 5 * time.Second
// InsertHashratePoint stores a hashrate measurement in the database.
// If ctx is nil, a default timeout context will be used.
func InsertHashratePoint(ctx context.Context, minerName, minerType string, point HashratePoint, resolution Resolution) error {
dbMu.RLock()
defer dbMu.RUnlock()
if db == 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 := db.ExecContext(ctx, `
INSERT INTO hashrate_history (miner_name, miner_type, timestamp, hashrate, resolution)
VALUES (?, ?, ?, ?, ?)
`, minerName, minerType, point.Timestamp, point.Hashrate, string(resolution))
return err
}
// GetHashrateHistory retrieves hashrate history for a miner within a time range
func GetHashrateHistory(minerName string, resolution Resolution, since, until time.Time) ([]HashratePoint, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil, nil
}
rows, err := db.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, fmt.Errorf("failed to query hashrate history: %w", 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, fmt.Errorf("failed to scan row: %w", err)
}
points = append(points, point)
}
return points, rows.Err()
}
// GetHashrateStats retrieves aggregated stats for a miner
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"`
}
func GetHashrateStats(minerName string) (*HashrateStats, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil, nil
}
// First check if there are any rows for this miner
var count int
err := db.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 = db.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
}
// GetAllMinerStats retrieves stats for all miners
func GetAllMinerStats() ([]HashrateStats, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil, nil
}
rows, err := db.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()
}