detached 1d86a91

fix(daemon): refuse a same-peer switch during a purge as unavailable and keep its offer

commit
1d86a915a91a3574d16123d5fe481e4a73786532
author
brice · · 2026-10-08 08:55 UTC
committer
brice · · 2026-10-08 09:31 UTC
parent
c002b88
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +18 -10
M internal/usecase/daemon/same_peer_switch.go +8 -7
@@ -69,17 +69,13 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p
6969 if err := request.Validate(); err != nil || !effect.current() || effect.sess == nil || effect.ac == nil {
7070 return
7171 }
72- if !effect.ac.consumeSamePeerOffer(request.Target) {
73- d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
74- return
75- }
76-
7772 // The switch moves an attachment between sessions, so it must not run
78- // inside the exact set a KillAll purge has captured.
73+ // inside the exact set a KillAll purge has captured. A purge is retry
74+ // state, so it is checked first and keeps any pending daemon offer.
7975 d.mu.Lock()
8076 if d.purgeAdmissionClosedLocked() {
8177 d.mu.Unlock()
82- d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
78+ d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchUnavailable)
8379 return
8480 }
8581 d.purgeAdmissionActive++
@@ -90,6 +86,11 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p
9086 release := func() { releaseOnce.Do(d.releasePurgeAdmission) }
9187 defer release()
9288
89+ if !effect.ac.consumeSamePeerOffer(request.Target) {
90+ d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
91+ return
92+ }
93+
9394 target, targetTabIndex, ok := d.samePeerTarget(request)
9495 if !ok || target == effect.sess {
9596 d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
@@ -69,17 +69,13 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p
69 if err := request.Validate(); err != nil || !effect.current() || effect.sess == nil || effect.ac == nil {69 if err := request.Validate(); err != nil || !effect.current() || effect.sess == nil || effect.ac == nil {
70 return70 return
71 }71 }
72- if !effect.ac.consumeSamePeerOffer(request.Target) {
73- d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
74- return
75- }
76-
77 // The switch moves an attachment between sessions, so it must not run72 // The switch moves an attachment between sessions, so it must not run
78- // inside the exact set a KillAll purge has captured.73+ // inside the exact set a KillAll purge has captured. A purge is retry
74+ // state, so it is checked first and keeps any pending daemon offer.
79 d.mu.Lock()75 d.mu.Lock()
80 if d.purgeAdmissionClosedLocked() {76 if d.purgeAdmissionClosedLocked() {
81 d.mu.Unlock()77 d.mu.Unlock()
82- d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)78+ d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchUnavailable)
83 return79 return
84 }80 }
85 d.purgeAdmissionActive++81 d.purgeAdmissionActive++
@@ -90,6 +86,11 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p
90 release := func() { releaseOnce.Do(d.releasePurgeAdmission) }86 release := func() { releaseOnce.Do(d.releasePurgeAdmission) }
91 defer release()87 defer release()
92 88
89+ if !effect.ac.consumeSamePeerOffer(request.Target) {
90+ d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
91+ return
92+ }
93+
93 target, targetTabIndex, ok := d.samePeerTarget(request)94 target, targetTabIndex, ok := d.samePeerTarget(request)
94 if !ok || target == effect.sess {95 if !ok || target == effect.sess {
95 d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)96 d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
M internal/usecase/daemon/same_peer_switch_test.go +10 -3
@@ -133,11 +133,13 @@ func TestSamePeerSwitchClientInitiated(t *testing.T) {
133133 offer *protocol.ExactSessionTarget
134134 purging bool
135135 switched bool
136+ wantCode protocol.SamePeerSwitchFailureCode
136137 }{
137138 {name: "no pending offer switches", switched: true},
138139 {name: "matching offer switches", offer: &requestTarget, switched: true},
139- {name: "another pending offer refuses", offer: &otherOffer},
140- {name: "a running purge refuses", purging: true},
140+ {name: "another pending offer refuses", offer: &otherOffer, wantCode: protocol.SamePeerSwitchStaleTarget},
141+ {name: "a running purge refuses", purging: true, wantCode: protocol.SamePeerSwitchUnavailable},
142+ {name: "a running purge keeps the pending offer", offer: &requestTarget, purging: true, wantCode: protocol.SamePeerSwitchUnavailable},
141143 }
142144 for _, tt := range tests {
143145 t.Run(tt.name, func(t *testing.T) {
@@ -179,7 +181,12 @@ func TestSamePeerSwitchClientInitiated(t *testing.T) {
179181 }
180182 require.Same(t, source, ac.currentAttachmentSession())
181183 failure := decodeServerMessage(t, awaitFrame(t, sends, "SamePeerSwitchFailure")).(protocol.SamePeerSwitchFailure)
182- require.Equal(t, protocol.SamePeerSwitchStaleTarget, failure.Code)
184+ require.Equal(t, tt.wantCode, failure.Code)
185+ if tt.offer != nil {
186+ ac.samePeerOfferMu.Lock()
187+ defer ac.samePeerOfferMu.Unlock()
188+ require.Equal(t, tt.offer, ac.samePeerOffer, "a refused switch keeps the pending offer")
189+ }
183190 })
184191 }
185192 }
@@ -133,11 +133,13 @@ func TestSamePeerSwitchClientInitiated(t *testing.T) {
133 offer *protocol.ExactSessionTarget133 offer *protocol.ExactSessionTarget
134 purging bool134 purging bool
135 switched bool135 switched bool
136+ wantCode protocol.SamePeerSwitchFailureCode
136 }{137 }{
137 {name: "no pending offer switches", switched: true},138 {name: "no pending offer switches", switched: true},
138 {name: "matching offer switches", offer: &requestTarget, switched: true},139 {name: "matching offer switches", offer: &requestTarget, switched: true},
139- {name: "another pending offer refuses", offer: &otherOffer},140+ {name: "another pending offer refuses", offer: &otherOffer, wantCode: protocol.SamePeerSwitchStaleTarget},
140- {name: "a running purge refuses", purging: true},141+ {name: "a running purge refuses", purging: true, wantCode: protocol.SamePeerSwitchUnavailable},
142+ {name: "a running purge keeps the pending offer", offer: &requestTarget, purging: true, wantCode: protocol.SamePeerSwitchUnavailable},
141 }143 }
142 for _, tt := range tests {144 for _, tt := range tests {
143 t.Run(tt.name, func(t *testing.T) {145 t.Run(tt.name, func(t *testing.T) {
@@ -179,7 +181,12 @@ func TestSamePeerSwitchClientInitiated(t *testing.T) {
179 }181 }
180 require.Same(t, source, ac.currentAttachmentSession())182 require.Same(t, source, ac.currentAttachmentSession())
181 failure := decodeServerMessage(t, awaitFrame(t, sends, "SamePeerSwitchFailure")).(protocol.SamePeerSwitchFailure)183 failure := decodeServerMessage(t, awaitFrame(t, sends, "SamePeerSwitchFailure")).(protocol.SamePeerSwitchFailure)
182- require.Equal(t, protocol.SamePeerSwitchStaleTarget, failure.Code)184+ require.Equal(t, tt.wantCode, failure.Code)
185+ if tt.offer != nil {
186+ ac.samePeerOfferMu.Lock()
187+ defer ac.samePeerOfferMu.Unlock()
188+ require.Equal(t, tt.offer, ac.samePeerOffer, "a refused switch keeps the pending offer")
189+ }
183 })190 })
184 }191 }
185 }192 }

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