Files
AetherForge/server/internal/db/subnet_spread_pause.go
AetherForge 7b2d41cda8
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Expand P2 test coverage: mining chain, spread lanes, path forge, WS/beacon, E2E onion, file handling
2026-06-07 04:58:55 -07:00

130 lines
3.7 KiB
Go

package db
import (
"fmt"
"strings"
"time"
)
const subnetSpreadFailureThreshold = 5
const subnetSpreadPauseDuration = 24 * time.Hour
// SubnetSpreadPause tracks /24 spread failure counts and temporary pauses.
type SubnetSpreadPause struct {
Prefix string `json:"prefix"`
FailCount int `json:"fail_count"`
PausedUntil *time.Time `json:"paused_until,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
func (d *Database) ensureSubnetSpreadPauseTable() error {
if d == nil {
return nil
}
_, err := d.Exec(`CREATE TABLE IF NOT EXISTS subnet_spread_pause (
prefix TEXT PRIMARY KEY,
fail_count INTEGER NOT NULL DEFAULT 0,
paused_until DATETIME,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)`)
if err != nil {
return fmt.Errorf("subnet_spread_pause migration: %w", err)
}
_, _ = d.Exec(`CREATE INDEX IF NOT EXISTS idx_subnet_spread_pause_until ON subnet_spread_pause(paused_until)`)
return nil
}
// RecordSubnetSpreadFailure increments failures for a /24 prefix; pauses at threshold.
func (d *Database) RecordSubnetSpreadFailure(prefix string) (paused bool, err error) {
if err := d.ensureSubnetSpreadPauseTable(); err != nil {
return false, err
}
prefix = normalizeSubnetPrefix(prefix)
if prefix == "" {
return false, fmt.Errorf("subnet prefix required")
}
now := time.Now().UTC()
var failCount int
err = d.QueryRow(`SELECT fail_count FROM subnet_spread_pause WHERE prefix = ?`, prefix).Scan(&failCount)
if err != nil {
_, err = d.Exec(
`INSERT INTO subnet_spread_pause (prefix, fail_count, paused_until, updated_at) VALUES (?, 1, NULL, ?)`,
prefix, now,
)
return false, err
}
failCount++
var pausedUntil *time.Time
if failCount >= subnetSpreadFailureThreshold {
until := now.Add(subnetSpreadPauseDuration)
pausedUntil = &until
paused = true
}
if pausedUntil != nil {
_, err = d.Exec(
`UPDATE subnet_spread_pause SET fail_count = ?, paused_until = ?, updated_at = ? WHERE prefix = ?`,
failCount, pausedUntil, now, prefix,
)
} else {
_, err = d.Exec(
`UPDATE subnet_spread_pause SET fail_count = ?, updated_at = ? WHERE prefix = ?`,
failCount, now, prefix,
)
}
return paused, err
}
// IsSubnetSpreadPaused reports whether spread commands to prefix are blocked.
func (d *Database) IsSubnetSpreadPaused(prefix string) (bool, error) {
if err := d.ensureSubnetSpreadPauseTable(); err != nil {
return false, err
}
prefix = normalizeSubnetPrefix(prefix)
if prefix == "" {
return false, nil
}
var pausedUntil *time.Time
err := d.QueryRow(`SELECT paused_until FROM subnet_spread_pause WHERE prefix = ?`, prefix).Scan(&pausedUntil)
if err != nil {
return false, nil
}
if pausedUntil == nil {
return false, nil
}
until := pausedUntil.UTC()
if time.Now().UTC().Before(until) {
return true, nil
}
_, _ = d.Exec(`UPDATE subnet_spread_pause SET paused_until = NULL, updated_at = ? WHERE prefix = ?`, time.Now().UTC(), prefix)
return false, nil
}
// GetSubnetSpreadPause returns the pause row for UI / tests.
func (d *Database) GetSubnetSpreadPause(prefix string) (*SubnetSpreadPause, error) {
if err := d.ensureSubnetSpreadPauseTable(); err != nil {
return nil, err
}
prefix = normalizeSubnetPrefix(prefix)
if prefix == "" {
return nil, nil
}
row := &SubnetSpreadPause{Prefix: prefix}
var pausedRaw *time.Time
err := d.QueryRow(
`SELECT fail_count, paused_until, updated_at FROM subnet_spread_pause WHERE prefix = ?`,
prefix,
).Scan(&row.FailCount, &pausedRaw, &row.UpdatedAt)
if err != nil {
return nil, nil
}
row.PausedUntil = pausedRaw
return row, nil
}
func normalizeSubnetPrefix(prefix string) string {
prefix = strings.TrimSpace(prefix)
prefix = strings.TrimSuffix(prefix, ".0/24")
prefix = strings.TrimSuffix(prefix, "/24")
return prefix
}