refactor(store): propagate context.Context through all Store methods

Every Store interface method (except Close) now takes context.Context
as first parameter. All 54 db.Query/Exec/QueryRow calls in SQLStore
replaced with their *Context variants. DB operations now respect
cancellation and deadlines.

Context sources by caller:
- Engine dbWriter/poll/pruner: engine ctx from Start()
- HTTP handlers: r.Context()
- config.Apply/Export: caller-provided ctx
- TUI/main.go init: context.Background()

RunCheck and all sub-checks (HTTP/ping/port/DNS) accept parent ctx.
HTTP checks now inherit shutdown cancellation instead of rooting in
context.Background(). dbWrite.exec takes ctx so the writer goroutine
can cancel stuck DB operations.

DeleteSite/ImportData use BeginTx(ctx) instead of Begin().
This commit is contained in:
2026-06-11 14:40:30 -04:00
parent 5d5153351e
commit 70a83a1da9
28 changed files with 813 additions and 677 deletions
+75 -65
View File
@@ -38,37 +38,43 @@ func newMockStore() *mockStore {
}
}
func (m *mockStore) Init() error { return nil }
func (m *mockStore) GetSites() ([]models.Site, error) { return m.sites, nil }
func (m *mockStore) AddSite(models.Site) error { return nil }
func (m *mockStore) UpdateSite(models.Site) error { return nil }
func (m *mockStore) UpdateSitePaused(int, bool) error { return nil }
func (m *mockStore) DeleteSite(int) error { return nil }
func (m *mockStore) AddAlert(string, string, map[string]string) error { return nil }
func (m *mockStore) UpdateAlert(int, string, string, map[string]string) error { return nil }
func (m *mockStore) DeleteAlert(int) error { return nil }
func (m *mockStore) GetAllUsers() ([]models.User, error) { return nil, nil }
func (m *mockStore) AddUser(string, string, string) error { return nil }
func (m *mockStore) UpdateUser(int, string, string, string) error { return nil }
func (m *mockStore) DeleteUser(int) error { return nil }
func (m *mockStore) ExportData() (models.Backup, error) { return models.Backup{}, nil }
func (m *mockStore) ImportData(models.Backup) error { return nil }
func (m *mockStore) GetSiteByName(string) (models.Site, error) { return models.Site{}, nil }
func (m *mockStore) AddSiteReturningID(models.Site) (int, error) { return 0, nil }
func (m *mockStore) AddAlertReturningID(string, string, map[string]string) (int, error) {
func (m *mockStore) Init(context.Context) error { return nil }
func (m *mockStore) GetSites(context.Context) ([]models.Site, error) { return m.sites, nil }
func (m *mockStore) AddSite(context.Context, models.Site) error { return nil }
func (m *mockStore) UpdateSite(context.Context, models.Site) error { return nil }
func (m *mockStore) UpdateSitePaused(context.Context, int, bool) error { return nil }
func (m *mockStore) DeleteSite(context.Context, int) error { return nil }
func (m *mockStore) AddAlert(context.Context, string, string, map[string]string) error { return nil }
func (m *mockStore) UpdateAlert(context.Context, int, string, string, map[string]string) error {
return nil
}
func (m *mockStore) DeleteAlert(context.Context, int) error { return nil }
func (m *mockStore) GetAllUsers(context.Context) ([]models.User, error) { return nil, nil }
func (m *mockStore) AddUser(context.Context, string, string, string) error { return nil }
func (m *mockStore) UpdateUser(context.Context, int, string, string, string) error { return nil }
func (m *mockStore) DeleteUser(context.Context, int) error { return nil }
func (m *mockStore) ExportData(context.Context) (models.Backup, error) { return models.Backup{}, nil }
func (m *mockStore) ImportData(context.Context, models.Backup) error { return nil }
func (m *mockStore) GetSiteByName(context.Context, string) (models.Site, error) {
return models.Site{}, nil
}
func (m *mockStore) AddSiteReturningID(context.Context, models.Site) (int, error) { return 0, nil }
func (m *mockStore) AddAlertReturningID(context.Context, string, string, map[string]string) (int, error) {
return 0, nil
}
func (m *mockStore) SaveCheckFromNode(int, string, int64, bool) error { return nil }
func (m *mockStore) RegisterNode(models.ProbeNode) error { return nil }
func (m *mockStore) GetNode(string) (models.ProbeNode, error) { return models.ProbeNode{}, nil }
func (m *mockStore) GetAllNodes() ([]models.ProbeNode, error) { return nil, nil }
func (m *mockStore) UpdateNodeLastSeen(string) error { return nil }
func (m *mockStore) DeleteNode(string) error { return nil }
func (m *mockStore) LoadAlertHealth() (map[int]models.AlertHealthRecord, error) {
func (m *mockStore) SaveCheckFromNode(context.Context, int, string, int64, bool) error { return nil }
func (m *mockStore) RegisterNode(context.Context, models.ProbeNode) error { return nil }
func (m *mockStore) GetNode(context.Context, string) (models.ProbeNode, error) {
return models.ProbeNode{}, nil
}
func (m *mockStore) GetAllNodes(context.Context) ([]models.ProbeNode, error) { return nil, nil }
func (m *mockStore) UpdateNodeLastSeen(context.Context, string) error { return nil }
func (m *mockStore) DeleteNode(context.Context, string) error { return nil }
func (m *mockStore) LoadAlertHealth(context.Context) (map[int]models.AlertHealthRecord, error) {
return nil, nil
}
func (m *mockStore) SaveAlertHealth(models.AlertHealthRecord) error { return nil }
func (m *mockStore) GetActiveMaintenanceWindows() ([]models.MaintenanceWindow, error) {
func (m *mockStore) SaveAlertHealth(context.Context, models.AlertHealthRecord) error { return nil }
func (m *mockStore) GetActiveMaintenanceWindows(context.Context) ([]models.MaintenanceWindow, error) {
m.mu.Lock()
defer m.mu.Unlock()
var windows []models.MaintenanceWindow
@@ -77,23 +83,27 @@ func (m *mockStore) GetActiveMaintenanceWindows() ([]models.MaintenanceWindow, e
}
return windows, nil
}
func (m *mockStore) GetAllMaintenanceWindows(int) ([]models.MaintenanceWindow, error) {
func (m *mockStore) GetAllMaintenanceWindows(context.Context, int) ([]models.MaintenanceWindow, error) {
return nil, nil
}
func (m *mockStore) AddMaintenanceWindow(models.MaintenanceWindow) error { return nil }
func (m *mockStore) EndMaintenanceWindow(int) error { return nil }
func (m *mockStore) DeleteMaintenanceWindow(int) error { return nil }
func (m *mockStore) PruneExpiredMaintenanceWindows(time.Duration) (int64, error) { return 0, nil }
func (m *mockStore) GetPreference(string) (string, error) { return "", nil }
func (m *mockStore) SetPreference(string, string) error { return nil }
func (m *mockStore) SaveStateChange(int, string, string, string) error { return nil }
func (m *mockStore) GetStateChanges(int, int) ([]models.StateChange, error) { return nil, nil }
func (m *mockStore) GetStateChangesSince(int, time.Time) ([]models.StateChange, error) {
func (m *mockStore) AddMaintenanceWindow(context.Context, models.MaintenanceWindow) error { return nil }
func (m *mockStore) EndMaintenanceWindow(context.Context, int) error { return nil }
func (m *mockStore) DeleteMaintenanceWindow(context.Context, int) error { return nil }
func (m *mockStore) PruneExpiredMaintenanceWindows(context.Context, time.Duration) (int64, error) {
return 0, nil
}
func (m *mockStore) GetPreference(context.Context, string) (string, error) { return "", nil }
func (m *mockStore) SetPreference(context.Context, string, string) error { return nil }
func (m *mockStore) SaveStateChange(context.Context, int, string, string, string) error { return nil }
func (m *mockStore) GetStateChanges(context.Context, int, int) ([]models.StateChange, error) {
return nil, nil
}
func (m *mockStore) GetStateChangesSince(context.Context, int, time.Time) ([]models.StateChange, error) {
return nil, nil
}
func (m *mockStore) Close() error { return nil }
func (m *mockStore) GetAllAlerts() ([]models.AlertConfig, error) {
func (m *mockStore) GetAllAlerts(context.Context) ([]models.AlertConfig, error) {
m.mu.Lock()
defer m.mu.Unlock()
var result []models.AlertConfig
@@ -103,7 +113,7 @@ func (m *mockStore) GetAllAlerts() ([]models.AlertConfig, error) {
return result, nil
}
func (m *mockStore) GetAlert(id int) (models.AlertConfig, error) {
func (m *mockStore) GetAlert(_ context.Context, id int) (models.AlertConfig, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.getAlertCalls = append(m.getAlertCalls, id)
@@ -113,7 +123,7 @@ func (m *mockStore) GetAlert(id int) (models.AlertConfig, error) {
return models.AlertConfig{}, fmt.Errorf("alert %d not found", id)
}
func (m *mockStore) GetAlertByName(name string) (models.AlertConfig, error) {
func (m *mockStore) GetAlertByName(_ context.Context, name string) (models.AlertConfig, error) {
m.mu.Lock()
defer m.mu.Unlock()
for _, a := range m.alerts {
@@ -124,37 +134,37 @@ func (m *mockStore) GetAlertByName(name string) (models.AlertConfig, error) {
return models.AlertConfig{}, fmt.Errorf("alert %q not found", name)
}
func (m *mockStore) IsMonitorInMaintenance(id int) (bool, error) {
func (m *mockStore) IsMonitorInMaintenance(_ context.Context, id int) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
return m.maintenance[id], nil
}
func (m *mockStore) SaveCheck(siteID int, latencyNs int64, isUp bool) error {
func (m *mockStore) SaveCheck(_ context.Context, siteID int, latencyNs int64, isUp bool) error {
m.mu.Lock()
defer m.mu.Unlock()
m.savedChecks = append(m.savedChecks, savedCheck{siteID, latencyNs, isUp})
return nil
}
func (m *mockStore) SaveLog(msg string) error {
func (m *mockStore) SaveLog(_ context.Context, msg string) error {
m.mu.Lock()
defer m.mu.Unlock()
m.savedLogs = append(m.savedLogs, msg)
return nil
}
func (m *mockStore) LoadLogs(limit int) ([]string, error) {
func (m *mockStore) LoadLogs(_ context.Context, limit int) ([]string, error) {
return m.logs, nil
}
func (m *mockStore) LoadAllHistory(limit int) (map[int][]models.CheckRecord, error) {
func (m *mockStore) LoadAllHistory(_ context.Context, limit int) (map[int][]models.CheckRecord, error) {
return m.history, nil
}
func (m *mockStore) PruneLogs() error { return nil }
func (m *mockStore) PruneCheckHistory() error { return nil }
func (m *mockStore) PruneStateChanges() error { return nil }
func (m *mockStore) PruneLogs(context.Context) error { return nil }
func (m *mockStore) PruneCheckHistory(context.Context) error { return nil }
func (m *mockStore) PruneStateChanges(context.Context) error { return nil }
// --- Helpers ---
@@ -336,7 +346,7 @@ func TestHandleStatusChange_AlertSuppressedMaintenance(t *testing.T) {
e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "UP", MaxRetries: 0, AlertID: 1}
injectSite(e, site)
e.refreshMaintenanceCache()
e.refreshMaintenanceCache(context.Background())
e.handleStatusChange(site, "DOWN", 0, 0, "test error")
@@ -368,7 +378,7 @@ func TestHandleStatusChange_RecoverySuppressedMaintenance(t *testing.T) {
e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "DOWN", AlertID: 1}
injectSite(e, site)
e.refreshMaintenanceCache()
e.refreshMaintenanceCache(context.Background())
e.handleStatusChange(site, "UP", 200, 0, "")
@@ -456,7 +466,7 @@ func TestHandleStatusChange_SSLWarningSuppressedMaint(t *testing.T) {
CertExpiry: time.Now().Add(15 * 24 * time.Hour),
}
injectSite(e, site)
e.refreshMaintenanceCache()
e.refreshMaintenanceCache(context.Background())
e.handleStatusChange(site, "UP", 200, 0, "")
@@ -563,7 +573,7 @@ func TestCheckPush_DeadlineMissed(t *testing.T) {
}
injectSite(e, site)
e.checkPush(site)
e.checkPush(context.Background(), site)
s, _ := getSite(e, 1)
if s.Status != "DOWN" {
@@ -581,7 +591,7 @@ func TestCheckPush_OverdueBecomesLate(t *testing.T) {
}
injectSite(e, site)
e.checkPush(site)
e.checkPush(context.Background(), site)
s, _ := getSite(e, 1)
if s.Status != "LATE" {
@@ -601,7 +611,7 @@ func TestCheckPush_OverdueBecomesStale(t *testing.T) {
}
injectSite(e, site)
e.checkPush(site)
e.checkPush(context.Background(), site)
s, _ := getSite(e, 1)
if s.Status != "STALE" {
@@ -618,7 +628,7 @@ func TestCheckPush_WithinDeadline(t *testing.T) {
}
injectSite(e, site)
e.checkPush(site)
e.checkPush(context.Background(), site)
s, _ := getSite(e, 1)
if s.Status != "UP" {
@@ -635,7 +645,7 @@ func TestCheckPush_PendingStaysPending(t *testing.T) {
}
injectSite(e, site)
e.checkPush(site)
e.checkPush(context.Background(), site)
s, _ := getSite(e, 1)
if s.Status != "PENDING" {
@@ -655,7 +665,7 @@ func TestCheckGroup_AllChildrenUp(t *testing.T) {
injectSite(e, child1)
injectSite(e, child2)
e.checkGroup(group)
e.checkGroup(context.Background(), group)
s, _ := getSite(e, 1)
if s.Status != "UP" {
@@ -673,7 +683,7 @@ func TestCheckGroup_OneChildDown(t *testing.T) {
injectSite(e, child1)
injectSite(e, child2)
e.checkGroup(group)
e.checkGroup(context.Background(), group)
s, _ := getSite(e, 1)
if s.Status != "DOWN" {
@@ -691,7 +701,7 @@ func TestCheckGroup_PausedChildIgnored(t *testing.T) {
injectSite(e, child1)
injectSite(e, child2)
e.checkGroup(group)
e.checkGroup(context.Background(), group)
s, _ := getSite(e, 1)
if s.Status != "UP" {
@@ -709,9 +719,9 @@ func TestCheckGroup_MaintenanceChildIgnored(t *testing.T) {
injectSite(e, group)
injectSite(e, child1)
injectSite(e, child2)
e.refreshMaintenanceCache()
e.refreshMaintenanceCache(context.Background())
e.checkGroup(group)
e.checkGroup(context.Background(), group)
s, _ := getSite(e, 1)
if s.Status != "UP" {
@@ -725,7 +735,7 @@ func TestCheckGroup_NoChildren(t *testing.T) {
group := models.Site{ID: 1, Name: "group", Type: "group", Status: "UP"}
injectSite(e, group)
e.checkGroup(group)
e.checkGroup(context.Background(), group)
s, _ := getSite(e, 1)
if s.Status != "PENDING" {
@@ -1241,7 +1251,7 @@ func TestCheckGroup_AllPausedNoAutoFreeze(t *testing.T) {
injectSite(e, child1)
injectSite(e, child2)
e.checkGroup(group)
e.checkGroup(context.Background(), group)
s, _ := getSite(e, 1)
if s.Paused {
@@ -1361,7 +1371,7 @@ func TestIsInMaintenance_UsesCache(t *testing.T) {
child := models.Site{ID: 20, Name: "child", Type: "http", ParentID: 10, Status: "UP"}
injectSite(e, group)
injectSite(e, child)
e.refreshMaintenanceCache()
e.refreshMaintenanceCache(context.Background())
if !e.isInMaintenance(10) {
t.Error("group should be in maintenance (direct)")
@@ -1381,7 +1391,7 @@ func TestIsInMaintenance_GlobalMaintenance(t *testing.T) {
e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Type: "http", Status: "UP"}
injectSite(e, site)
e.refreshMaintenanceCache()
e.refreshMaintenanceCache(context.Background())
if !e.isInMaintenance(1) {
t.Error("all monitors should be in maintenance during global window")