fix(domain): emit one mirror.failed event per incident, including the pause
- commit
- 3aadd9b8e88cb83fb7c3fe9058ec232cc3444002
- author
- brice · · 2026-10-20 05:30 UTC
- committer
- brice · · 2026-10-20 10:02 UTC
- parent
- 1069a88
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +5 -4
M internal/domain/mirror_state.go +4 -3
| @@ -57,7 +57,7 @@ type MirrorOutcome struct { | |||
| 57 | 57 | } // Err nil == success | |
| 58 | 58 | type MirrorTransition struct { | |
| 59 | 59 | Status MirrorStatus | |
| 60 | - | FailedEvent bool // State moved into Failing or Paused from OK/Disabled, or Failing -> Paused | |
| 60 | + | FailedEvent bool // State moved into Failing or Paused from OK/Disabled: once per incident | |
| 61 | 61 | Recovered bool // State moved from Failing/Paused to OK | |
| 62 | 62 | } | |
| 63 | 63 | ||
| @@ -113,7 +113,8 @@ func MirrorBackoff(n int) time.Duration { | |||
| 113 | 113 | } | |
| 114 | 114 | ||
| 115 | 115 | // MirrorDisable/MirrorReset are the config-change transitions: reset lifts Paused/Failing to OK | |
| 116 | - | // with counters cleared (config changed, or sync --force before the attempt); disable -> Disabled. | |
| 116 | + | // with counters cleared (config changed); disable -> Disabled. sync --force does not reset: the | |
| 117 | + | // attempt is judged against the previous status so a recovery still reports Recovered. | |
| 117 | 118 | func MirrorReset(p MirrorStatus, now time.Time) MirrorStatus { | |
| 118 | 119 | p.State = MirrorOK | |
| 119 | 120 | p.Class = MirrorClassNone | |
| @@ -158,7 +159,7 @@ func ApplyMirrorOutcome(prev MirrorStatus, out MirrorOutcome, now time.Time) Mir | |||
| 158 | 159 | p.NextRetryAt = now.Add(MirrorBackoff(p.ConsecutiveFailures)) | |
| 159 | 160 | } | |
| 160 | 161 | p.Reason = MirrorReason(p) | |
| 161 | - | return MirrorTransition{Status: p, FailedEvent: prev.State != MirrorFailing && prev.State != MirrorPaused || prev.State == MirrorFailing && p.State == MirrorPaused} | |
| 162 | + | return MirrorTransition{Status: p, FailedEvent: prev.State != MirrorFailing && prev.State != MirrorPaused} | |
| 162 | 163 | } | |
| 163 | 164 | ||
| 164 | 165 | // MirrorReason is the human sentence of a status, e.g. | |
| @@ -57,7 +57,7 @@ type MirrorOutcome struct { | |||||
| 57 | } // Err nil == success | 57 | } // Err nil == success | ||
| 58 | type MirrorTransition struct { | 58 | type MirrorTransition struct { | ||
| 59 | Status MirrorStatus | 59 | Status MirrorStatus | ||
| 60 | - | FailedEvent bool // State moved into Failing or Paused from OK/Disabled, or Failing -> Paused | 60 | + | FailedEvent bool // State moved into Failing or Paused from OK/Disabled: once per incident |
| 61 | Recovered bool // State moved from Failing/Paused to OK | 61 | Recovered bool // State moved from Failing/Paused to OK | ||
| 62 | } | 62 | } | ||
| 63 | 63 | ||||
| @@ -113,7 +113,8 @@ func MirrorBackoff(n int) time.Duration { | |||||
| 113 | } | 113 | } | ||
| 114 | 114 | ||||
| 115 | // MirrorDisable/MirrorReset are the config-change transitions: reset lifts Paused/Failing to OK | 115 | // MirrorDisable/MirrorReset are the config-change transitions: reset lifts Paused/Failing to OK | ||
| 116 | - | // with counters cleared (config changed, or sync --force before the attempt); disable -> Disabled. | 116 | + | // with counters cleared (config changed); disable -> Disabled. sync --force does not reset: the |
| 117 | + | // attempt is judged against the previous status so a recovery still reports Recovered. | |||
| 117 | func MirrorReset(p MirrorStatus, now time.Time) MirrorStatus { | 118 | func MirrorReset(p MirrorStatus, now time.Time) MirrorStatus { | ||
| 118 | p.State = MirrorOK | 119 | p.State = MirrorOK | ||
| 119 | p.Class = MirrorClassNone | 120 | p.Class = MirrorClassNone | ||
| @@ -158,7 +159,7 @@ func ApplyMirrorOutcome(prev MirrorStatus, out MirrorOutcome, now time.Time) Mir | |||||
| 158 | p.NextRetryAt = now.Add(MirrorBackoff(p.ConsecutiveFailures)) | 159 | p.NextRetryAt = now.Add(MirrorBackoff(p.ConsecutiveFailures)) | ||
| 159 | } | 160 | } | ||
| 160 | p.Reason = MirrorReason(p) | 161 | p.Reason = MirrorReason(p) | ||
| 161 | - | return MirrorTransition{Status: p, FailedEvent: prev.State != MirrorFailing && prev.State != MirrorPaused || prev.State == MirrorFailing && p.State == MirrorPaused} | 162 | + | return MirrorTransition{Status: p, FailedEvent: prev.State != MirrorFailing && prev.State != MirrorPaused} |
| 162 | } | 163 | } | ||
| 163 | 164 | ||||
| 164 | // MirrorReason is the human sentence of a status, e.g. | 165 | // MirrorReason is the human sentence of a status, e.g. | ||
M internal/domain/mirror_test.go +1 -1
| @@ -138,7 +138,7 @@ func TestMirrorTransitions(t *testing.T) { | |||
| 138 | 138 | if fatal || n >= 5 { | |
| 139 | 139 | want = MirrorPaused | |
| 140 | 140 | } | |
| 141 | - | if tr.Status.State != want || tr.Status.ConsecutiveFailures != n || tr.FailedEvent != (n == 1 || !fatal && n == 5) { | |
| 141 | + | if tr.Status.State != want || tr.Status.ConsecutiveFailures != n || tr.FailedEvent != (n == 1) { | |
| 142 | 142 | t.Fatalf("n=%d %+v", n, tr) | |
| 143 | 143 | } | |
| 144 | 144 | if want == MirrorFailing && !tr.Status.NextRetryAt.Equal(now.Add(MirrorBackoff(n))) { | |
| @@ -138,7 +138,7 @@ func TestMirrorTransitions(t *testing.T) { | |||||
| 138 | if fatal || n >= 5 { | 138 | if fatal || n >= 5 { | ||
| 139 | want = MirrorPaused | 139 | want = MirrorPaused | ||
| 140 | } | 140 | } | ||
| 141 | - | if tr.Status.State != want || tr.Status.ConsecutiveFailures != n || tr.FailedEvent != (n == 1 || !fatal && n == 5) { | 141 | + | if tr.Status.State != want || tr.Status.ConsecutiveFailures != n || tr.FailedEvent != (n == 1) { |
| 142 | t.Fatalf("n=%d %+v", n, tr) | 142 | t.Fatalf("n=%d %+v", n, tr) | ||
| 143 | } | 143 | } | ||
| 144 | if want == MirrorFailing && !tr.Status.NextRetryAt.Equal(now.Add(MirrorBackoff(n))) { | 144 | if want == MirrorFailing && !tr.Status.NextRetryAt.Equal(now.Add(MirrorBackoff(n))) { | ||