Reduce metric writes and preserve scheduler history
This commit is contained in:
+78
-8
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user