fix: harden DynDNS recovery and publish v1.5.1
This commit is contained in:
1 parent
3383b91e49
commit
81def77e3c
37 files changed
+2415
-250
No files matched your search
@@ -1435,6 +1435,50 @@ func (s *Store) FinishUpdateRun(ctx context.Context, id int64, status, ipv4, ipv
|
||||
return err
|
||||
}
|
||||
|
||||
// CompleteUpdateRun commits history, dashboard state and audit together.
|
||||
func (s *Store) CompleteUpdateRun(ctx context.Context, id int64, status, ipv4, ipv6, summary string) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE update_runs SET finished_at = ?, status = ?, ipv4 = ?, ipv6 = ?, summary = ? WHERE id = ?`, now(), status, ipv4, ipv6, summary, id); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE settings SET last_ipv4 = COALESCE(NULLIF(?, ''), last_ipv4), last_ipv6 = COALESCE(NULLIF(?, ''), last_ipv6), last_run_at = ?, last_run_status = ? WHERE id = 1`, ipv4, ipv6, now(), status); err != nil {
|
||||
return err
|
||||
}
|
||||
if status != "ok" {
|
||||
if _, err := tx.ExecContext(ctx, `INSERT INTO audit_events(created_at, kind, message) VALUES(?, ?, ?)`, now(), "update."+status, summary); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// RecoverInterruptedRuns is called once at process startup, before workers start.
|
||||
func (s *Store) RecoverInterruptedRuns(ctx context.Context) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
result, err := tx.ExecContext(ctx, `UPDATE update_runs SET finished_at = ?, status = 'error', summary = 'Durch Neustart unterbrochen; erneute Prüfung folgt automatisch' WHERE status = 'running'`, now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE settings SET last_run_status = 'error' WHERE id = 1`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (s *Store) RecentRuns(ctx context.Context, limit int) ([]model.UpdateRun, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id, started_at, finished_at, status, ipv4, ipv6, summary FROM update_runs ORDER BY id DESC LIMIT ?`, limit)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,6 +14,49 @@ import (
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/secure"
|
||||
)
|
||||
|
||||
func TestRunCompletionIsAtomicAndInterruptedRunsRecover(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
database, err := Open(ctx, filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer database.Close()
|
||||
id, err := database.CreateUpdateRun(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := database.db.Exec(`CREATE TRIGGER reject_state BEFORE UPDATE ON settings BEGIN SELECT RAISE(FAIL, 'storage failure'); END`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.CompleteUpdateRun(ctx, id, "ok", "198.51.100.42", "", "completed"); err == nil {
|
||||
t.Fatal("storage failure silently ignored")
|
||||
}
|
||||
runs, err := database.RecentRuns(ctx, 1)
|
||||
if err != nil || runs[0].Status != "running" {
|
||||
t.Fatalf("partially committed run: %#v %v", runs, err)
|
||||
}
|
||||
if _, err := database.db.Exec(`DROP TRIGGER reject_state`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.CompleteUpdateRun(ctx, id, "ok", "198.51.100.42", "", "completed"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := database.CreateUpdateRun(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.RecoverInterruptedRuns(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runs, err = database.RecentRuns(ctx, 10)
|
||||
if err != nil || len(runs) != 2 || runs[0].Status != "error" || runs[0].FinishedAt.IsZero() || runs[1].Status != "ok" {
|
||||
t.Fatalf("incorrect restart recovery: %#v %v", runs, err)
|
||||
}
|
||||
settings, err := database.GetSettings(ctx)
|
||||
if err != nil || settings.LastIPv4 != "198.51.100.42" || settings.LastRunStatus != "error" {
|
||||
t.Fatalf("restart corrupted state: %#v %v", settings, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateAppliesEveryIntermediateSchemaFromV1(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in new issue
Block a user