detached 99b00c3

fix(mr): count retry attempts only for the keys that wait in the scheduler

commit
99b00c335f6212a20fd9fc84655748ae0f9cf3f6
author
brice · · 2026-10-19 15:18 UTC
committer
brice · · 2026-10-19 18:30 UTC
parent
33a90cd
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +18 -7
M internal/usecase/export_test.go +7 -0
@@ -182,3 +182,10 @@ func (s *Mergeability) ScheduledRetries() int {
182182 defer s.retryMu.Unlock()
183183 return len(s.heap)
184184 }
185+
186+// RetryAttempts gives the number of keys whose failed attempts are counted.
187+func (s *Mergeability) RetryAttempts() int {
188+ s.retryMu.Lock()
189+ defer s.retryMu.Unlock()
190+ return len(s.retries)
191+}
@@ -182,3 +182,10 @@ func (s *Mergeability) ScheduledRetries() int {
182 defer s.retryMu.Unlock()182 defer s.retryMu.Unlock()
183 return len(s.heap)183 return len(s.heap)
184 }184 }
185+
186+// RetryAttempts gives the number of keys whose failed attempts are counted.
187+func (s *Mergeability) RetryAttempts() int {
188+ s.retryMu.Lock()
189+ defer s.retryMu.Unlock()
190+ return len(s.retries)
191+}
M internal/usecase/mergeability.go +10 -7
@@ -161,19 +161,22 @@ func (s *Mergeability) retry(key retryKey) {
161161 if s.closed || s.scheduled[key] {
162162 return
163163 }
164- s.retries[key]++
165- n := s.retries[key]
166- if n > mergeabilityRetries {
167- delete(s.retries, key)
168- return
169- }
164+ // The key that waits is chosen before counting attempts, so that only
165+ // the waiting keys hold attempt counts: at most cfg.Queue requests plus
166+ // one per repository.
170167 sk := key
171- if key.kind == retryCheck && s.checkRetries >= s.cfg.Queue {
168+ if key.kind == retryCheck && s.retries[key] == 0 && s.checkRetries >= s.cfg.Queue {
172169 sk = retryKey{kind: retryRescan, mk: mergeKey{repo: key.mk.repo}}
173170 if s.scheduled[sk] {
174171 return
175172 }
176173 }
174+ s.retries[sk]++
175+ n := s.retries[sk]
176+ if n > mergeabilityRetries {
177+ delete(s.retries, sk)
178+ return
179+ }
177180 if sk.kind == retryCheck {
178181 s.checkRetries++
179182 }
@@ -161,19 +161,22 @@ func (s *Mergeability) retry(key retryKey) {
161 if s.closed || s.scheduled[key] {161 if s.closed || s.scheduled[key] {
162 return162 return
163 }163 }
164- s.retries[key]++164+ // The key that waits is chosen before counting attempts, so that only
165- n := s.retries[key]165+ // the waiting keys hold attempt counts: at most cfg.Queue requests plus
166- if n > mergeabilityRetries {166+ // one per repository.
167- delete(s.retries, key)
168- return
169- }
170 sk := key167 sk := key
171- if key.kind == retryCheck && s.checkRetries >= s.cfg.Queue {168+ if key.kind == retryCheck && s.retries[key] == 0 && s.checkRetries >= s.cfg.Queue {
172 sk = retryKey{kind: retryRescan, mk: mergeKey{repo: key.mk.repo}}169 sk = retryKey{kind: retryRescan, mk: mergeKey{repo: key.mk.repo}}
173 if s.scheduled[sk] {170 if s.scheduled[sk] {
174 return171 return
175 }172 }
176 }173 }
174+ s.retries[sk]++
175+ n := s.retries[sk]
176+ if n > mergeabilityRetries {
177+ delete(s.retries, sk)
178+ return
179+ }
177 if sk.kind == retryCheck {180 if sk.kind == retryCheck {
178 s.checkRetries++181 s.checkRetries++
179 }182 }
M internal/usecase/mergeability_test.go +1 -0
@@ -576,6 +576,7 @@ func TestMergeabilityRetriesAreBounded(t *testing.T) {
576576 }
577577 assert.LessOrEqual(t, svc.ScheduledRetries(), queue+repos)
578578 assert.Positive(t, svc.ScheduledRetries())
579+ assert.LessOrEqual(t, svc.RetryAttempts(), queue+repos, "only the waiting keys count their attempts")
579580 assert.LessOrEqual(t, runtime.NumGoroutine(), before, "retries must not start goroutines")
580581 svc.Close(context.Background()) // returns although retries are pending
581582 }
@@ -576,6 +576,7 @@ func TestMergeabilityRetriesAreBounded(t *testing.T) {
576 }576 }
577 assert.LessOrEqual(t, svc.ScheduledRetries(), queue+repos)577 assert.LessOrEqual(t, svc.ScheduledRetries(), queue+repos)
578 assert.Positive(t, svc.ScheduledRetries())578 assert.Positive(t, svc.ScheduledRetries())
579+ assert.LessOrEqual(t, svc.RetryAttempts(), queue+repos, "only the waiting keys count their attempts")
579 assert.LessOrEqual(t, runtime.NumGoroutine(), before, "retries must not start goroutines")580 assert.LessOrEqual(t, runtime.NumGoroutine(), before, "retries must not start goroutines")
580 svc.Close(context.Background()) // returns although retries are pending581 svc.Close(context.Background()) // returns although retries are pending
581 }582 }

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 2

Tags 45

↑↓ moveenter openesc close