detached dce3fad

fix(daemon): read attachment lease readiness under the coordinator lock

fireWithTimerTokenAndLease read lease.ready after releasing c.mu while markAttachmentReady writes it under c.mu, a data race seen in CI. Leases are now filtered for readiness while the lock is held.
commit
dce3fad94ec4d4a2f19f4f9cd557794aa6694ca9
author
brice · · 2026-10-08 08:07 UTC
parent
f67cb47
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +9 -9
M internal/usecase/daemon/render_coordinator.go +9 -9
@@ -1005,21 +1005,21 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui
10051005 if c.pendingGeneration == 0 {
10061006 c.pendingGeneration = 1
10071007 }
1008- leases := make(map[*attachedClient]*attachmentLease, len(c.leases))
1008+ // lease.ready is guarded by c.mu (markAttachmentReady writes it), so only
1009+ // leases already ready under the lock are probed after unlocking.
1010+ probe := make(map[*attachedClient]*attachmentLease, len(c.leases))
10091011 for ac, lease := range c.leases {
1010- if lease != nil && lease.active {
1011- leases[ac] = lease
1012+ if lease != nil && lease.active && lease.ready {
1013+ probe[ac] = lease
10121014 }
10131015 }
10141016 c.mu.Unlock()
10151017
1016- ready := make(map[*attachmentLease]bool, len(leases))
1017- for ac, lease := range leases {
1018+ ready := make(map[*attachmentLease]bool, len(probe))
1019+ for ac, lease := range probe {
10181020 // Welcome holds attachment.sendMu while its transport Send is in
10191021 // flight. Do not probe an incarnation before it is ready.
1020- if lease.ready {
1021- ready[lease] = c.attachmentAckReady(ac)
1022- }
1022+ ready[lease] = c.attachmentAckReady(ac)
10231023 }
10241024
10251025 // The readiness callback can detach, replace, or publish a sync batch.
@@ -1041,7 +1041,7 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui
10411041
10421042 // Re-snapshot after every external readiness probe. A replacement may have
10431043 // retired one of the leases we probed while the callback was unlocked.
1044- leases = make(map[*attachedClient]*attachmentLease, len(c.leases))
1044+ leases := make(map[*attachedClient]*attachmentLease, len(c.leases))
10451045 for ac, lease := range c.leases {
10461046 if lease != nil && lease.active {
10471047 leases[ac] = lease
@@ -1005,21 +1005,21 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui
1005 if c.pendingGeneration == 0 {1005 if c.pendingGeneration == 0 {
1006 c.pendingGeneration = 11006 c.pendingGeneration = 1
1007 }1007 }
1008- leases := make(map[*attachedClient]*attachmentLease, len(c.leases))1008+ // lease.ready is guarded by c.mu (markAttachmentReady writes it), so only
1009+ // leases already ready under the lock are probed after unlocking.
1010+ probe := make(map[*attachedClient]*attachmentLease, len(c.leases))
1009 for ac, lease := range c.leases {1011 for ac, lease := range c.leases {
1010- if lease != nil && lease.active {1012+ if lease != nil && lease.active && lease.ready {
1011- leases[ac] = lease1013+ probe[ac] = lease
1012 }1014 }
1013 }1015 }
1014 c.mu.Unlock()1016 c.mu.Unlock()
1015 1017
1016- ready := make(map[*attachmentLease]bool, len(leases))1018+ ready := make(map[*attachmentLease]bool, len(probe))
1017- for ac, lease := range leases {1019+ for ac, lease := range probe {
1018 // Welcome holds attachment.sendMu while its transport Send is in1020 // Welcome holds attachment.sendMu while its transport Send is in
1019 // flight. Do not probe an incarnation before it is ready.1021 // flight. Do not probe an incarnation before it is ready.
1020- if lease.ready {1022+ ready[lease] = c.attachmentAckReady(ac)
1021- ready[lease] = c.attachmentAckReady(ac)
1022- }
1023 }1023 }
1024 1024
1025 // The readiness callback can detach, replace, or publish a sync batch.1025 // The readiness callback can detach, replace, or publish a sync batch.
@@ -1041,7 +1041,7 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui
1041 1041
1042 // Re-snapshot after every external readiness probe. A replacement may have1042 // Re-snapshot after every external readiness probe. A replacement may have
1043 // retired one of the leases we probed while the callback was unlocked.1043 // retired one of the leases we probed while the callback was unlocked.
1044- leases = make(map[*attachedClient]*attachmentLease, len(c.leases))1044+ leases := make(map[*attachedClient]*attachmentLease, len(c.leases))
1045 for ac, lease := range c.leases {1045 for ac, lease := range c.leases {
1046 if lease != nil && lease.active {1046 if lease != nil && lease.active {
1047 leases[ac] = lease1047 leases[ac] = lease

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 12

Tags 11

↑↓ moveenter openesc close