detached f8072b0

fix(backup): keep backup successes recorded while seeding reconciles targets

RetainBackupTargets now takes the time the declaration was read and keeps entries updated after it, so a target declared and backed up during startup seeding is not dropped.
commit
f8072b0ceb272936a65300f0e29e96dcc0442925
author
brice · · 2026-10-17 06:59 UTC
parent
229d05e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +56 -23
M internal/adapters/out/telemetry/backup_metrics.go +15 -10
@@ -24,13 +24,15 @@ func targetAttrs(ref domain.BackupTargetRef) []attribute.KeyValue {
2424 }
2525
2626 type backupLastSuccess struct {
27- at time.Time
28- size int64
27+ at time.Time
28+ size int64
29+ updated time.Time // when this entry was written, for retain
2930 }
3031
3132 // backupState holds the last success per target, exported by observable
3233 // gauges at each collection.
3334 type backupState struct {
35+ now func() time.Time
3436 mu sync.Mutex
3537 last map[domain.BackupTargetRef]backupLastSuccess
3638 nextRun map[domain.BackupKind]func() time.Time
@@ -38,6 +40,7 @@ type backupState struct {
3840
3941 func newBackupState() *backupState {
4042 return &backupState{
43+ now: time.Now,
4144 last: map[domain.BackupTargetRef]backupLastSuccess{},
4245 nextRun: map[domain.BackupKind]func() time.Time{},
4346 }
@@ -54,21 +57,23 @@ func (s *backupState) remember(run domain.BackupRunRecord) {
5457 if prev, ok := s.last[key]; ok && !run.StartedAt.After(prev.at) {
5558 return
5659 }
57- s.last[key] = backupLastSuccess{at: run.StartedAt, size: run.SizeBytes}
60+ s.last[key] = backupLastSuccess{at: run.StartedAt, size: run.SizeBytes, updated: s.now()}
5861 }
5962
60-// retain drops the last success of every target of kind not in targets.
61-func (s *backupState) retain(kind domain.BackupKind, targets []domain.BackupTargetRef) {
63+// retain drops the last success of every target of kind not in targets,
64+// unless it was updated after asOf.
65+func (s *backupState) retain(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time) {
6266 keep := make(map[domain.BackupTargetRef]struct{}, len(targets))
6367 for _, ref := range targets {
6468 keep[ref] = struct{}{}
6569 }
6670 s.mu.Lock()
6771 defer s.mu.Unlock()
68- for key := range s.last {
69- if _, ok := keep[key]; key.Kind == kind && !ok {
70- delete(s.last, key)
72+ for key, last := range s.last {
73+ if _, ok := keep[key]; key.Kind != kind || ok || last.updated.After(asOf) {
74+ continue
7175 }
76+ delete(s.last, key)
7277 }
7378 }
7479
@@ -140,8 +145,8 @@ func (m *Metrics) ObserveStoredBackup(run domain.BackupRunRecord) {
140145 }
141146
142147 // RetainBackupTargets implements out.BackupMetrics.
143-func (m *Metrics) RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef) {
144- m.backups.retain(kind, targets)
148+func (m *Metrics) RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time) {
149+ m.backups.retain(kind, targets, asOf)
145150 }
146151
147152 // ObserveBackupNextRun exports the next planned run of one backup kind.
@@ -24,13 +24,15 @@ func targetAttrs(ref domain.BackupTargetRef) []attribute.KeyValue {
24 }24 }
25 25
26 type backupLastSuccess struct {26 type backupLastSuccess struct {
27- at time.Time27+ at time.Time
28- size int6428+ size int64
29+ updated time.Time // when this entry was written, for retain
29 }30 }
30 31
31 // backupState holds the last success per target, exported by observable32 // backupState holds the last success per target, exported by observable
32 // gauges at each collection.33 // gauges at each collection.
33 type backupState struct {34 type backupState struct {
35+ now func() time.Time
34 mu sync.Mutex36 mu sync.Mutex
35 last map[domain.BackupTargetRef]backupLastSuccess37 last map[domain.BackupTargetRef]backupLastSuccess
36 nextRun map[domain.BackupKind]func() time.Time38 nextRun map[domain.BackupKind]func() time.Time
@@ -38,6 +40,7 @@ type backupState struct {
38 40
39 func newBackupState() *backupState {41 func newBackupState() *backupState {
40 return &backupState{42 return &backupState{
43+ now: time.Now,
41 last: map[domain.BackupTargetRef]backupLastSuccess{},44 last: map[domain.BackupTargetRef]backupLastSuccess{},
42 nextRun: map[domain.BackupKind]func() time.Time{},45 nextRun: map[domain.BackupKind]func() time.Time{},
43 }46 }
@@ -54,21 +57,23 @@ func (s *backupState) remember(run domain.BackupRunRecord) {
54 if prev, ok := s.last[key]; ok && !run.StartedAt.After(prev.at) {57 if prev, ok := s.last[key]; ok && !run.StartedAt.After(prev.at) {
55 return58 return
56 }59 }
57- s.last[key] = backupLastSuccess{at: run.StartedAt, size: run.SizeBytes}60+ s.last[key] = backupLastSuccess{at: run.StartedAt, size: run.SizeBytes, updated: s.now()}
58 }61 }
59 62
60-// retain drops the last success of every target of kind not in targets.63+// retain drops the last success of every target of kind not in targets,
61-func (s *backupState) retain(kind domain.BackupKind, targets []domain.BackupTargetRef) {64+// unless it was updated after asOf.
65+func (s *backupState) retain(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time) {
62 keep := make(map[domain.BackupTargetRef]struct{}, len(targets))66 keep := make(map[domain.BackupTargetRef]struct{}, len(targets))
63 for _, ref := range targets {67 for _, ref := range targets {
64 keep[ref] = struct{}{}68 keep[ref] = struct{}{}
65 }69 }
66 s.mu.Lock()70 s.mu.Lock()
67 defer s.mu.Unlock()71 defer s.mu.Unlock()
68- for key := range s.last {72+ for key, last := range s.last {
69- if _, ok := keep[key]; key.Kind == kind && !ok {73+ if _, ok := keep[key]; key.Kind != kind || ok || last.updated.After(asOf) {
70- delete(s.last, key)74+ continue
71 }75 }
76+ delete(s.last, key)
72 }77 }
73 }78 }
74 79
@@ -140,8 +145,8 @@ func (m *Metrics) ObserveStoredBackup(run domain.BackupRunRecord) {
140 }145 }
141 146
142 // RetainBackupTargets implements out.BackupMetrics.147 // RetainBackupTargets implements out.BackupMetrics.
143-func (m *Metrics) RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef) {148+func (m *Metrics) RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time) {
144- m.backups.retain(kind, targets)149+ m.backups.retain(kind, targets, asOf)
145 }150 }
146 151
147 // ObserveBackupNextRun exports the next planned run of one backup kind.152 // ObserveBackupNextRun exports the next planned run of one backup kind.
M internal/adapters/out/telemetry/backup_metrics_test.go +18 -1
@@ -131,12 +131,29 @@ func TestRetainBackupTargets_ForgetsUndeclaredTargetsOfThatKindOnly(t *testing.T
131131 m.ObserveStoredBackup(domain.BackupRunRecord{BackupTargetRef: ref, Succeeded: true, StartedAt: at})
132132 }
133133
134- m.RetainBackupTargets(domain.BackupKindVolume, []domain.BackupTargetRef{keep})
134+ m.RetainBackupTargets(domain.BackupKindVolume, []domain.BackupTargetRef{keep}, time.Now())
135135
136136 assert.Equal(t, map[string]float64{"data": float64(at.Unix()), "orders": float64(at.Unix())},
137137 gaugeByTarget(t, collect(t, reader)["gordon.backup.last_success_timestamp_seconds"]))
138138 }
139139
140+// TestRetainBackupTargets_KeepsSuccessRecordedAfterTheSnapshot covers a
141+// target declared and backed up while seeding read an older declaration.
142+func TestRetainBackupTargets_KeepsSuccessRecordedAfterTheSnapshot(t *testing.T) {
143+ m, reader := newTestMetrics(t)
144+ clock := time.Date(2026, 10, 17, 8, 0, 0, 0, time.UTC)
145+ m.backups.now = func() time.Time { return clock }
146+ snapshot := clock
147+ clock = clock.Add(time.Second)
148+ fresh := domain.BackupTargetRef{Kind: domain.BackupKindVolume, App: "new", Service: "api", Target: "data"}
149+ m.RecordBackupRun(context.Background(), domain.BackupRunRecord{BackupTargetRef: fresh, Succeeded: true, StartedAt: clock})
150+
151+ m.RetainBackupTargets(domain.BackupKindVolume, nil, snapshot)
152+
153+ assert.Equal(t, map[string]float64{"data": float64(clock.Unix())},
154+ gaugeByTarget(t, collect(t, reader)["gordon.backup.last_success_timestamp_seconds"]))
155+}
156+
140157 func TestObserveBackupNextRun(t *testing.T) {
141158 m, reader := newTestMetrics(t)
142159 next := time.Date(2026, 10, 18, 1, 30, 0, 0, time.UTC)
@@ -131,12 +131,29 @@ func TestRetainBackupTargets_ForgetsUndeclaredTargetsOfThatKindOnly(t *testing.T
131 m.ObserveStoredBackup(domain.BackupRunRecord{BackupTargetRef: ref, Succeeded: true, StartedAt: at})131 m.ObserveStoredBackup(domain.BackupRunRecord{BackupTargetRef: ref, Succeeded: true, StartedAt: at})
132 }132 }
133 133
134- m.RetainBackupTargets(domain.BackupKindVolume, []domain.BackupTargetRef{keep})134+ m.RetainBackupTargets(domain.BackupKindVolume, []domain.BackupTargetRef{keep}, time.Now())
135 135
136 assert.Equal(t, map[string]float64{"data": float64(at.Unix()), "orders": float64(at.Unix())},136 assert.Equal(t, map[string]float64{"data": float64(at.Unix()), "orders": float64(at.Unix())},
137 gaugeByTarget(t, collect(t, reader)["gordon.backup.last_success_timestamp_seconds"]))137 gaugeByTarget(t, collect(t, reader)["gordon.backup.last_success_timestamp_seconds"]))
138 }138 }
139 139
140+// TestRetainBackupTargets_KeepsSuccessRecordedAfterTheSnapshot covers a
141+// target declared and backed up while seeding read an older declaration.
142+func TestRetainBackupTargets_KeepsSuccessRecordedAfterTheSnapshot(t *testing.T) {
143+ m, reader := newTestMetrics(t)
144+ clock := time.Date(2026, 10, 17, 8, 0, 0, 0, time.UTC)
145+ m.backups.now = func() time.Time { return clock }
146+ snapshot := clock
147+ clock = clock.Add(time.Second)
148+ fresh := domain.BackupTargetRef{Kind: domain.BackupKindVolume, App: "new", Service: "api", Target: "data"}
149+ m.RecordBackupRun(context.Background(), domain.BackupRunRecord{BackupTargetRef: fresh, Succeeded: true, StartedAt: clock})
150+
151+ m.RetainBackupTargets(domain.BackupKindVolume, nil, snapshot)
152+
153+ assert.Equal(t, map[string]float64{"data": float64(clock.Unix())},
154+ gaugeByTarget(t, collect(t, reader)["gordon.backup.last_success_timestamp_seconds"]))
155+}
156+
140 func TestObserveBackupNextRun(t *testing.T) {157 func TestObserveBackupNextRun(t *testing.T) {
141 m, reader := newTestMetrics(t)158 m, reader := newTestMetrics(t)
142 next := time.Date(2026, 10, 18, 1, 30, 0, 0, time.UTC)159 next := time.Date(2026, 10, 18, 1, 30, 0, 0, time.UTC)
M internal/boundaries/out/backup_metrics.go +4 -2
@@ -2,6 +2,7 @@ package out
22
33 import (
44 "context"
5+ "time"
56
67 "github.com/bnema/gordon/internal/domain"
78 )
@@ -18,6 +19,7 @@ type BackupMetrics interface {
1819 ObserveStoredBackup(run domain.BackupRunRecord)
1920 // RetainBackupTargets forgets the last success of every target of kind
2021 // that is not in targets, so a removed app or backup declaration stops
21- // being reported.
22- RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef)
22+ // being reported. targets is the declaration read at asOf: a target
23+ // updated after asOf is kept, since it may have been declared since.
24+ RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time)
2325 }
@@ -2,6 +2,7 @@ package out
2 2
3 import (3 import (
4 "context"4 "context"
5+ "time"
5 6
6 "github.com/bnema/gordon/internal/domain"7 "github.com/bnema/gordon/internal/domain"
7 )8 )
@@ -18,6 +19,7 @@ type BackupMetrics interface {
18 ObserveStoredBackup(run domain.BackupRunRecord)19 ObserveStoredBackup(run domain.BackupRunRecord)
19 // RetainBackupTargets forgets the last success of every target of kind20 // RetainBackupTargets forgets the last success of every target of kind
20 // that is not in targets, so a removed app or backup declaration stops21 // that is not in targets, so a removed app or backup declaration stops
21- // being reported.22+ // being reported. targets is the declaration read at asOf: a target
22- RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef)23+ // updated after asOf is kept, since it may have been declared since.
24+ RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time)
23 }25 }
M internal/boundaries/out/mocks/mock_backup_metrics.go +13 -6
@@ -6,6 +6,7 @@ package mocks
66
77 import (
88 "context"
9+ "time"
910
1011 "github.com/bnema/gordon/internal/domain"
1112 mock "github.com/stretchr/testify/mock"
@@ -134,8 +135,8 @@ func (_c *MockBackupMetrics_RecordBackupRun_Call) RunAndReturn(run func(ctx cont
134135 }
135136
136137 // RetainBackupTargets provides a mock function for the type MockBackupMetrics
137-func (_mock *MockBackupMetrics) RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef) {
138- _mock.Called(kind, targets)
138+func (_mock *MockBackupMetrics) RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time) {
139+ _mock.Called(kind, targets, asOf)
139140 return
140141 }
141142
@@ -147,11 +148,12 @@ type MockBackupMetrics_RetainBackupTargets_Call struct {
147148 // RetainBackupTargets is a helper method to define mock.On call
148149 // - kind domain.BackupKind
149150 // - targets []domain.BackupTargetRef
150-func (_e *MockBackupMetrics_Expecter) RetainBackupTargets(kind any, targets any) *MockBackupMetrics_RetainBackupTargets_Call {
151- return &MockBackupMetrics_RetainBackupTargets_Call{Call: _e.mock.On("RetainBackupTargets", kind, targets)}
151+// - asOf time.Time
152+func (_e *MockBackupMetrics_Expecter) RetainBackupTargets(kind any, targets any, asOf any) *MockBackupMetrics_RetainBackupTargets_Call {
153+ return &MockBackupMetrics_RetainBackupTargets_Call{Call: _e.mock.On("RetainBackupTargets", kind, targets, asOf)}
152154 }
153155
154-func (_c *MockBackupMetrics_RetainBackupTargets_Call) Run(run func(kind domain.BackupKind, targets []domain.BackupTargetRef)) *MockBackupMetrics_RetainBackupTargets_Call {
156+func (_c *MockBackupMetrics_RetainBackupTargets_Call) Run(run func(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time)) *MockBackupMetrics_RetainBackupTargets_Call {
155157 _c.Call.Run(func(args mock.Arguments) {
156158 var arg0 domain.BackupKind
157159 if args[0] != nil {
@@ -161,9 +163,14 @@ func (_c *MockBackupMetrics_RetainBackupTargets_Call) Run(run func(kind domain.B
161163 if args[1] != nil {
162164 arg1 = args[1].([]domain.BackupTargetRef)
163165 }
166+ var arg2 time.Time
167+ if args[2] != nil {
168+ arg2 = args[2].(time.Time)
169+ }
164170 run(
165171 arg0,
166172 arg1,
173+ arg2,
167174 )
168175 })
169176 return _c
@@ -174,7 +181,7 @@ func (_c *MockBackupMetrics_RetainBackupTargets_Call) Return() *MockBackupMetric
174181 return _c
175182 }
176183
177-func (_c *MockBackupMetrics_RetainBackupTargets_Call) RunAndReturn(run func(kind domain.BackupKind, targets []domain.BackupTargetRef)) *MockBackupMetrics_RetainBackupTargets_Call {
184+func (_c *MockBackupMetrics_RetainBackupTargets_Call) RunAndReturn(run func(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time)) *MockBackupMetrics_RetainBackupTargets_Call {
178185 _c.Run(run)
179186 return _c
180187 }
@@ -6,6 +6,7 @@ package mocks
6 6
7 import (7 import (
8 "context"8 "context"
9+ "time"
9 10
10 "github.com/bnema/gordon/internal/domain"11 "github.com/bnema/gordon/internal/domain"
11 mock "github.com/stretchr/testify/mock"12 mock "github.com/stretchr/testify/mock"
@@ -134,8 +135,8 @@ func (_c *MockBackupMetrics_RecordBackupRun_Call) RunAndReturn(run func(ctx cont
134 }135 }
135 136
136 // RetainBackupTargets provides a mock function for the type MockBackupMetrics137 // RetainBackupTargets provides a mock function for the type MockBackupMetrics
137-func (_mock *MockBackupMetrics) RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef) {138+func (_mock *MockBackupMetrics) RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time) {
138- _mock.Called(kind, targets)139+ _mock.Called(kind, targets, asOf)
139 return140 return
140 }141 }
141 142
@@ -147,11 +148,12 @@ type MockBackupMetrics_RetainBackupTargets_Call struct {
147 // RetainBackupTargets is a helper method to define mock.On call148 // RetainBackupTargets is a helper method to define mock.On call
148 // - kind domain.BackupKind149 // - kind domain.BackupKind
149 // - targets []domain.BackupTargetRef150 // - targets []domain.BackupTargetRef
150-func (_e *MockBackupMetrics_Expecter) RetainBackupTargets(kind any, targets any) *MockBackupMetrics_RetainBackupTargets_Call {151+// - asOf time.Time
151- return &MockBackupMetrics_RetainBackupTargets_Call{Call: _e.mock.On("RetainBackupTargets", kind, targets)}152+func (_e *MockBackupMetrics_Expecter) RetainBackupTargets(kind any, targets any, asOf any) *MockBackupMetrics_RetainBackupTargets_Call {
153+ return &MockBackupMetrics_RetainBackupTargets_Call{Call: _e.mock.On("RetainBackupTargets", kind, targets, asOf)}
152 }154 }
153 155
154-func (_c *MockBackupMetrics_RetainBackupTargets_Call) Run(run func(kind domain.BackupKind, targets []domain.BackupTargetRef)) *MockBackupMetrics_RetainBackupTargets_Call {156+func (_c *MockBackupMetrics_RetainBackupTargets_Call) Run(run func(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time)) *MockBackupMetrics_RetainBackupTargets_Call {
155 _c.Call.Run(func(args mock.Arguments) {157 _c.Call.Run(func(args mock.Arguments) {
156 var arg0 domain.BackupKind158 var arg0 domain.BackupKind
157 if args[0] != nil {159 if args[0] != nil {
@@ -161,9 +163,14 @@ func (_c *MockBackupMetrics_RetainBackupTargets_Call) Run(run func(kind domain.B
161 if args[1] != nil {163 if args[1] != nil {
162 arg1 = args[1].([]domain.BackupTargetRef)164 arg1 = args[1].([]domain.BackupTargetRef)
163 }165 }
166+ var arg2 time.Time
167+ if args[2] != nil {
168+ arg2 = args[2].(time.Time)
169+ }
164 run(170 run(
165 arg0,171 arg0,
166 arg1,172 arg1,
173+ arg2,
167 )174 )
168 })175 })
169 return _c176 return _c
@@ -174,7 +181,7 @@ func (_c *MockBackupMetrics_RetainBackupTargets_Call) Return() *MockBackupMetric
174 return _c181 return _c
175 }182 }
176 183
177-func (_c *MockBackupMetrics_RetainBackupTargets_Call) RunAndReturn(run func(kind domain.BackupKind, targets []domain.BackupTargetRef)) *MockBackupMetrics_RetainBackupTargets_Call {184+func (_c *MockBackupMetrics_RetainBackupTargets_Call) RunAndReturn(run func(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time)) *MockBackupMetrics_RetainBackupTargets_Call {
178 _c.Run(run)185 _c.Run(run)
179 return _c186 return _c
180 }187 }
M internal/usecase/backup/metrics_test.go +2 -2
@@ -87,7 +87,7 @@ func TestVolumeService_SeedMetrics(t *testing.T) {
8787 {App: "gone", VolumeName: "old", StartedAt: started, SizeBytes: 1},
8888 }}
8989 metrics := outmocks.NewMockBackupMetrics(t)
90- metrics.EXPECT().RetainBackupTargets(domain.BackupKindVolume, []domain.BackupTargetRef{shopData}).Once()
90+ metrics.EXPECT().RetainBackupTargets(domain.BackupKindVolume, []domain.BackupTargetRef{shopData}, mock.Anything).Once()
9191 metrics.EXPECT().ObserveStoredBackup(domain.BackupRunRecord{
9292 BackupTargetRef: shopData, Succeeded: true, SizeBytes: 42, StartedAt: started,
9393 }).Once()
@@ -166,7 +166,7 @@ func TestService_SeedMetrics(t *testing.T) {
166166 {App: "shop", Service: "api", DBName: "orders", StartedAt: started, SizeBytes: 7},
167167 {App: "shop", Service: "api", DBName: "dropped", StartedAt: started, SizeBytes: 1},
168168 }, nil).Once()
169- metrics.EXPECT().RetainBackupTargets(domain.BackupKindDatabase, []domain.BackupTargetRef{shopOrders}).Once()
169+ metrics.EXPECT().RetainBackupTargets(domain.BackupKindDatabase, []domain.BackupTargetRef{shopOrders}, mock.Anything).Once()
170170 metrics.EXPECT().ObserveStoredBackup(domain.BackupRunRecord{
171171 BackupTargetRef: shopOrders, Succeeded: true, SizeBytes: 7, StartedAt: started,
172172 }).Once()
@@ -87,7 +87,7 @@ func TestVolumeService_SeedMetrics(t *testing.T) {
87 {App: "gone", VolumeName: "old", StartedAt: started, SizeBytes: 1},87 {App: "gone", VolumeName: "old", StartedAt: started, SizeBytes: 1},
88 }}88 }}
89 metrics := outmocks.NewMockBackupMetrics(t)89 metrics := outmocks.NewMockBackupMetrics(t)
90- metrics.EXPECT().RetainBackupTargets(domain.BackupKindVolume, []domain.BackupTargetRef{shopData}).Once()90+ metrics.EXPECT().RetainBackupTargets(domain.BackupKindVolume, []domain.BackupTargetRef{shopData}, mock.Anything).Once()
91 metrics.EXPECT().ObserveStoredBackup(domain.BackupRunRecord{91 metrics.EXPECT().ObserveStoredBackup(domain.BackupRunRecord{
92 BackupTargetRef: shopData, Succeeded: true, SizeBytes: 42, StartedAt: started,92 BackupTargetRef: shopData, Succeeded: true, SizeBytes: 42, StartedAt: started,
93 }).Once()93 }).Once()
@@ -166,7 +166,7 @@ func TestService_SeedMetrics(t *testing.T) {
166 {App: "shop", Service: "api", DBName: "orders", StartedAt: started, SizeBytes: 7},166 {App: "shop", Service: "api", DBName: "orders", StartedAt: started, SizeBytes: 7},
167 {App: "shop", Service: "api", DBName: "dropped", StartedAt: started, SizeBytes: 1},167 {App: "shop", Service: "api", DBName: "dropped", StartedAt: started, SizeBytes: 1},
168 }, nil).Once()168 }, nil).Once()
169- metrics.EXPECT().RetainBackupTargets(domain.BackupKindDatabase, []domain.BackupTargetRef{shopOrders}).Once()169+ metrics.EXPECT().RetainBackupTargets(domain.BackupKindDatabase, []domain.BackupTargetRef{shopOrders}, mock.Anything).Once()
170 metrics.EXPECT().ObserveStoredBackup(domain.BackupRunRecord{170 metrics.EXPECT().ObserveStoredBackup(domain.BackupRunRecord{
171 BackupTargetRef: shopOrders, Succeeded: true, SizeBytes: 7, StartedAt: started,171 BackupTargetRef: shopOrders, Succeeded: true, SizeBytes: 7, StartedAt: started,
172 }).Once()172 }).Once()
M internal/usecase/backup/service.go +2 -1
@@ -68,6 +68,7 @@ func (s *Service) SeedMetrics(ctx context.Context) error {
6868 if s.metrics == nil {
6969 return nil
7070 }
71+ asOf := time.Now()
7172 targets, err := s.allTargets(ctx)
7273 if err != nil {
7374 return fmt.Errorf("seed backup metrics: %w", err)
@@ -79,7 +80,7 @@ func (s *Service) SeedMetrics(ctx context.Context) error {
7980 refs = append(refs, ref)
8081 declared[ref] = struct{}{}
8182 }
82- s.metrics.RetainBackupTargets(domain.BackupKindDatabase, refs)
83+ s.metrics.RetainBackupTargets(domain.BackupKindDatabase, refs, asOf)
8384
8485 jobs, err := s.ListBackups(ctx, "")
8586 if err != nil {
@@ -68,6 +68,7 @@ func (s *Service) SeedMetrics(ctx context.Context) error {
68 if s.metrics == nil {68 if s.metrics == nil {
69 return nil69 return nil
70 }70 }
71+ asOf := time.Now()
71 targets, err := s.allTargets(ctx)72 targets, err := s.allTargets(ctx)
72 if err != nil {73 if err != nil {
73 return fmt.Errorf("seed backup metrics: %w", err)74 return fmt.Errorf("seed backup metrics: %w", err)
@@ -79,7 +80,7 @@ func (s *Service) SeedMetrics(ctx context.Context) error {
79 refs = append(refs, ref)80 refs = append(refs, ref)
80 declared[ref] = struct{}{}81 declared[ref] = struct{}{}
81 }82 }
82- s.metrics.RetainBackupTargets(domain.BackupKindDatabase, refs)83+ s.metrics.RetainBackupTargets(domain.BackupKindDatabase, refs, asOf)
83 84
84 jobs, err := s.ListBackups(ctx, "")85 jobs, err := s.ListBackups(ctx, "")
85 if err != nil {86 if err != nil {
M internal/usecase/backup/volume_service.go +2 -1
@@ -64,6 +64,7 @@ func (s *VolumeService) SeedMetrics(ctx context.Context) error {
6464 if s.metrics == nil {
6565 return nil
6666 }
67+ asOf := time.Now()
6768 targets, err := s.allVolumeTargets(ctx)
6869 if err != nil {
6970 return fmt.Errorf("seed volume backup metrics: %w", err)
@@ -75,7 +76,7 @@ func (s *VolumeService) SeedMetrics(ctx context.Context) error {
7576 refs = append(refs, ref)
7677 byVolume[target.App+"/"+target.VolumeName] = ref
7778 }
78- s.metrics.RetainBackupTargets(domain.BackupKindVolume, refs)
79+ s.metrics.RetainBackupTargets(domain.BackupKindVolume, refs, asOf)
7980
8081 archives, err := s.storage.LatestVolumeArchives(ctx)
8182 if err != nil {
@@ -64,6 +64,7 @@ func (s *VolumeService) SeedMetrics(ctx context.Context) error {
64 if s.metrics == nil {64 if s.metrics == nil {
65 return nil65 return nil
66 }66 }
67+ asOf := time.Now()
67 targets, err := s.allVolumeTargets(ctx)68 targets, err := s.allVolumeTargets(ctx)
68 if err != nil {69 if err != nil {
69 return fmt.Errorf("seed volume backup metrics: %w", err)70 return fmt.Errorf("seed volume backup metrics: %w", err)
@@ -75,7 +76,7 @@ func (s *VolumeService) SeedMetrics(ctx context.Context) error {
75 refs = append(refs, ref)76 refs = append(refs, ref)
76 byVolume[target.App+"/"+target.VolumeName] = ref77 byVolume[target.App+"/"+target.VolumeName] = ref
77 }78 }
78- s.metrics.RetainBackupTargets(domain.BackupKindVolume, refs)79+ s.metrics.RetainBackupTargets(domain.BackupKindVolume, refs, asOf)
79 80
80 archives, err := s.storage.LatestVolumeArchives(ctx)81 archives, err := s.storage.LatestVolumeArchives(ctx)
81 if err != nil {82 if err != nil {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close