Mining/pkg/database/hashrate.go

325 lines
7.5 KiB
Go

package database
import (
"fmt"
"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"`
}
// InsertHashratePoint stores a hashrate measurement in the database
func InsertHashratePoint(minerName, minerType string, point HashratePoint, resolution Resolution) error {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil // DB not enabled, silently skip
}
_, err := db.Exec(`
INSERT INTO hashrate_history (miner_name, miner_type, timestamp, hashrate, resolution)
VALUES (?, ?, ?, ?, ?)
`, minerName, minerType, point.Timestamp, point.Hashrate, string(resolution))
return err
}
// InsertHashratePoints stores multiple hashrate measurements in a single transaction
func InsertHashratePoints(minerName, minerType string, points []HashratePoint, resolution Resolution) error {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil
}
if len(points) == 0 {
return nil
}
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
stmt, err := tx.Prepare(`
INSERT INTO hashrate_history (miner_name, miner_type, timestamp, hashrate, resolution)
VALUES (?, ?, ?, ?, ?)
`)
if err != nil {
return fmt.Errorf("failed to prepare statement: %w", err)
}
defer stmt.Close()
for _, point := range points {
_, err := stmt.Exec(minerName, minerType, point.Timestamp, point.Hashrate, string(resolution))
if err != nil {
return fmt.Errorf("failed to insert point: %w", err)
}
}
return tx.Commit()
}
// 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()
}
// GetLatestHashrate retrieves the most recent hashrate for a miner
func GetLatestHashrate(minerName string) (*HashratePoint, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil, nil
}
var point HashratePoint
err := db.QueryRow(`
SELECT timestamp, hashrate
FROM hashrate_history
WHERE miner_name = ?
ORDER BY timestamp DESC
LIMIT 1
`, minerName).Scan(&point.Timestamp, &point.Hashrate)
if err != nil {
return nil, nil // Not found is not an error
}
return &point, nil
}
// GetAverageHashrate calculates the average hashrate for a miner in a time range
func GetAverageHashrate(minerName string, since, until time.Time) (int, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return 0, nil
}
var avg float64
err := db.QueryRow(`
SELECT COALESCE(AVG(hashrate), 0)
FROM hashrate_history
WHERE miner_name = ?
AND timestamp >= ?
AND timestamp <= ?
`, minerName, since, until).Scan(&avg)
return int(avg), err
}
// GetMaxHashrate retrieves the maximum hashrate for a miner in a time range
func GetMaxHashrate(minerName string, since, until time.Time) (int, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return 0, nil
}
var max int
err := db.QueryRow(`
SELECT COALESCE(MAX(hashrate), 0)
FROM hashrate_history
WHERE miner_name = ?
AND timestamp >= ?
AND timestamp <= ?
`, minerName, since, until).Scan(&max)
return max, 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, so scan them as strings first
var firstSeenStr, lastSeenStr string
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,
&stats.AverageRate,
&stats.MaxRate,
&stats.MinRate,
&firstSeenStr,
&lastSeenStr,
)
if err != nil {
return nil, err
}
// Parse timestamps
stats.FirstSeen, _ = time.Parse("2006-01-02 15:04:05.999999999-07:00", firstSeenStr)
if stats.FirstSeen.IsZero() {
stats.FirstSeen, _ = time.Parse(time.RFC3339Nano, firstSeenStr)
}
stats.LastSeen, _ = time.Parse("2006-01-02 15:04:05.999999999-07:00", lastSeenStr)
if stats.LastSeen.IsZero() {
stats.LastSeen, _ = time.Parse(time.RFC3339Nano, 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
if err := rows.Scan(
&stats.MinerName,
&stats.TotalPoints,
&stats.AverageRate,
&stats.MaxRate,
&stats.MinRate,
&firstSeenStr,
&lastSeenStr,
); err != nil {
return nil, err
}
// Parse timestamps
stats.FirstSeen, _ = time.Parse("2006-01-02 15:04:05.999999999-07:00", firstSeenStr)
if stats.FirstSeen.IsZero() {
stats.FirstSeen, _ = time.Parse(time.RFC3339Nano, firstSeenStr)
}
stats.LastSeen, _ = time.Parse("2006-01-02 15:04:05.999999999-07:00", lastSeenStr)
if stats.LastSeen.IsZero() {
stats.LastSeen, _ = time.Parse(time.RFC3339Nano, lastSeenStr)
}
allStats = append(allStats, stats)
}
return allStats, rows.Err()
}
// CleanupOldData removes hashrate data older than the specified duration
func CleanupOldData(resolution Resolution, maxAge time.Duration) error {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil
}
cutoff := time.Now().Add(-maxAge)
_, err := db.Exec(`
DELETE FROM hashrate_history
WHERE resolution = ?
AND timestamp < ?
`, string(resolution), cutoff)
return err
}