Reduce metric writes and preserve scheduler history

This commit is contained in:
Mikei386
2026-07-13 20:53:16 +02:00
parent ea97d7dbc9
commit 819055cbf0
12 changed files with 228 additions and 27 deletions
+78 -8
View File
@@ -1,6 +1,7 @@
package store
import (
"bytes"
"encoding/json"
"errors"
"fmt"
@@ -70,16 +71,48 @@ func (s *Store) SaveRuns(runs []model.Run) error {
return writeJSONAtomic(s.runsPath(), runs, 0600)
}
func (s *Store) LoadBackupMetrics() ([]model.BackupMetric, error) {
func (s *Store) LoadBackupMetrics() ([]model.BackupMetric, bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var metrics []model.BackupMetric
if err := readJSON(s.backupMetricsPath(), &metrics); errors.Is(err, os.ErrNotExist) {
return []model.BackupMetric{}, nil
} else if err != nil {
return nil, err
path := s.backupMetricsPath()
b, err := os.ReadFile(path)
legacySource := false
if errors.Is(err, os.ErrNotExist) {
path = s.legacyBackupMetricsPath()
b, err = os.ReadFile(path)
legacySource = true
if errors.Is(err, os.ErrNotExist) {
return []model.BackupMetric{}, false, nil
}
}
return metrics, nil
if err != nil {
return nil, false, err
}
b = bytes.TrimSpace(b)
if len(b) == 0 {
return []model.BackupMetric{}, false, nil
}
if b[0] == '[' {
var metrics []model.BackupMetric
if err := json.Unmarshal(b, &metrics); err != nil {
return nil, true, fmt.Errorf("decode %s: %w", path, err)
}
return metrics, true, nil
}
lines := bytes.Split(b, []byte{'\n'})
metrics := make([]model.BackupMetric, 0, len(lines))
for index, line := range lines {
line = bytes.TrimSpace(line)
if len(line) == 0 {
continue
}
var metric model.BackupMetric
if err := json.Unmarshal(line, &metric); err != nil {
return nil, legacySource, fmt.Errorf("decode %s line %d: %w", path, index+1, err)
}
metrics = append(metrics, metric)
}
return metrics, legacySource, nil
}
func (s *Store) SaveBackupMetrics(metrics []model.BackupMetric) error {
@@ -88,12 +121,45 @@ func (s *Store) SaveBackupMetrics(metrics []model.BackupMetric) error {
if len(metrics) > 5000 {
metrics = metrics[len(metrics)-5000:]
}
return writeJSONAtomic(s.backupMetricsPath(), metrics, 0600)
var data bytes.Buffer
encoder := json.NewEncoder(&data)
for _, metric := range metrics {
if err := encoder.Encode(metric); err != nil {
return err
}
}
return writeBytesAtomic(s.backupMetricsPath(), data.Bytes(), 0600)
}
func (s *Store) AppendBackupMetric(metric model.BackupMetric) error {
s.mu.Lock()
defer s.mu.Unlock()
b, err := json.Marshal(metric)
if err != nil {
return err
}
b = append(b, '\n')
file, err := os.OpenFile(s.backupMetricsPath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return err
}
if _, err := file.Write(b); err != nil {
file.Close()
return err
}
if err := file.Sync(); err != nil {
file.Close()
return err
}
return file.Close()
}
func (s *Store) configPath() string { return filepath.Join(s.dir, "config.json") }
func (s *Store) runsPath() string { return filepath.Join(s.dir, "runs.json") }
func (s *Store) backupMetricsPath() string {
return filepath.Join(s.dir, "backup-metrics.jsonl")
}
func (s *Store) legacyBackupMetricsPath() string {
return filepath.Join(s.dir, "backup-metrics.json")
}
@@ -114,6 +180,10 @@ func writeJSONAtomic(path string, value any, mode os.FileMode) error {
return err
}
b = append(b, '\n')
return writeBytesAtomic(path, b, mode)
}
func writeBytesAtomic(path string, b []byte, mode os.FileMode) error {
tmp, err := os.CreateTemp(filepath.Dir(path), ".urbm-*")
if err != nil {
return err
+33 -1
View File
@@ -43,10 +43,13 @@ func TestBackupMetricsPersistSeparatelyFromRuns(t *testing.T) {
if err := s.SaveRuns(nil); err != nil {
t.Fatal(err)
}
loaded, err := s.LoadBackupMetrics()
loaded, legacy, err := s.LoadBackupMetrics()
if err != nil {
t.Fatal(err)
}
if legacy {
t.Fatal("compacted metric file was detected as legacy JSON array")
}
if len(loaded) != 1 || loaded[0].RunID != "run-1" || loaded[0].BytesProcessed != 42 {
t.Fatalf("backup metrics = %#v", loaded)
}
@@ -58,3 +61,32 @@ func TestBackupMetricsPersistSeparatelyFromRuns(t *testing.T) {
t.Fatalf("backup metrics mode = %o", info.Mode().Perm())
}
}
func TestBackupMetricsMigrateFromJSONArrayAndAppend(t *testing.T) {
s := New(t.TempDir())
finished := time.Date(2026, 7, 13, 1, 30, 0, 0, time.UTC)
legacyJSON := `[{"schemaVersion":1,"runId":"old","jobId":"job-1","finishedAt":"2026-07-13T01:30:00Z","status":"success","bytesProcessed":42,"filesProcessed":7}]`
if err := os.WriteFile(s.legacyBackupMetricsPath(), []byte(legacyJSON), 0600); err != nil {
t.Fatal(err)
}
metrics, legacy, err := s.LoadBackupMetrics()
if err != nil {
t.Fatal(err)
}
if !legacy || len(metrics) != 1 || metrics[0].RunID != "old" {
t.Fatalf("legacy metrics = %#v, legacy = %v", metrics, legacy)
}
if err := s.SaveBackupMetrics(metrics); err != nil {
t.Fatal(err)
}
if err := s.AppendBackupMetric(model.BackupMetric{SchemaVersion: model.SchemaVersion, RunID: "new", JobID: "job-1", FinishedAt: finished.Add(time.Hour), Status: "success"}); err != nil {
t.Fatal(err)
}
metrics, legacy, err = s.LoadBackupMetrics()
if err != nil {
t.Fatal(err)
}
if legacy || len(metrics) != 2 || metrics[1].RunID != "new" {
t.Fatalf("appended metrics = %#v, legacy = %v", metrics, legacy)
}
}