detached b902d96

fix(daemon): retire expired remote resume credentials

commit
b902d9649a90148dcd65095bcff24b6c7f4f890b
author
brice · · 2026-08-06 20:03 UTC
parent
28680b3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +60 -1
M internal/usecase/daemon/remote_warm.go +3 -0
@@ -124,6 +124,8 @@ func (d *Daemon) expireRemoteViewWarm(view *remoteView, warm *remoteViewWarm) {
124124 view.warm = nil
125125 view.warmGeneration++
126126 view.closed = true
127+ retirements := d.purgeParkedForRemoteViewLocked(view)
128+ d.purgeParkingForRemoteViewLocked(view)
127129 link := view.link
128130 view.link = nil
129131 view.linkGeneration++
@@ -135,6 +137,7 @@ func (d *Daemon) expireRemoteViewWarm(view *remoteView, warm *remoteViewWarm) {
135137 d.mu.Unlock()
136138
137139 warm.stop()
140+ d.finishParkedAttachmentRetirements(retirements)
138141 if link == nil {
139142 return
140143 }
@@ -124,6 +124,8 @@ func (d *Daemon) expireRemoteViewWarm(view *remoteView, warm *remoteViewWarm) {
124 view.warm = nil124 view.warm = nil
125 view.warmGeneration++125 view.warmGeneration++
126 view.closed = true126 view.closed = true
127+ retirements := d.purgeParkedForRemoteViewLocked(view)
128+ d.purgeParkingForRemoteViewLocked(view)
127 link := view.link129 link := view.link
128 view.link = nil130 view.link = nil
129 view.linkGeneration++131 view.linkGeneration++
@@ -135,6 +137,7 @@ func (d *Daemon) expireRemoteViewWarm(view *remoteView, warm *remoteViewWarm) {
135 d.mu.Unlock()137 d.mu.Unlock()
136 138
137 warm.stop()139 warm.stop()
140+ d.finishParkedAttachmentRetirements(retirements)
138 if link == nil {141 if link == nil {
139 return142 return
140 }143 }
M internal/usecase/daemon/remote_warm_test.go +25 -0
@@ -29,6 +29,31 @@ func TestRemoteViewWarmExpiryRemovesOnlyDetachedExactView(t *testing.T) {
2929 view.mu.Unlock()
3030 }
3131
32+func TestRemoteViewWarmExpiryPurgesRemoteResumeCredentials(t *testing.T) {
33+ clock := &signalClock{timers: make(chan *signalTimer, 1)}
34+ d := newTestDaemon(t, nil, clock)
35+ view := registerRemoteWarmTestView(t, d)
36+ parked := &attachedClient{resumeToken: 41, parked: true}
37+ parking := &attachedClient{resumeToken: 42}
38+ d.mu.Lock()
39+ d.parked[parked.resumeToken] = &parkedAttachment{owner: view, ac: parked, timer: stubTimer{}, done: make(chan struct{})}
40+ d.parking[parking.resumeToken] = &parkingAttachment{owner: view, ac: parking, done: make(chan struct{})}
41+ d.mu.Unlock()
42+
43+ d.parkRemoteViewWarm(view)
44+ warm, timer := remoteWarmTimer(t, clock, view)
45+ timer.ch <- time.Now()
46+ awaitTestCompletion(t, warm.done, "remote warm expiry did not complete")
47+
48+ d.mu.Lock()
49+ require.Empty(t, d.parked)
50+ require.Empty(t, d.parking)
51+ d.mu.Unlock()
52+ require.Zero(t, parked.resumeToken)
53+ require.False(t, parked.parked)
54+ require.Zero(t, parking.resumeToken)
55+}
56+
3257 func TestRemoteViewWarmActivationFencesStaleExpiry(t *testing.T) {
3358 clock := &signalClock{timers: make(chan *signalTimer, 1)}
3459 d := newTestDaemon(t, nil, clock)
@@ -29,6 +29,31 @@ func TestRemoteViewWarmExpiryRemovesOnlyDetachedExactView(t *testing.T) {
29 view.mu.Unlock()29 view.mu.Unlock()
30 }30 }
31 31
32+func TestRemoteViewWarmExpiryPurgesRemoteResumeCredentials(t *testing.T) {
33+ clock := &signalClock{timers: make(chan *signalTimer, 1)}
34+ d := newTestDaemon(t, nil, clock)
35+ view := registerRemoteWarmTestView(t, d)
36+ parked := &attachedClient{resumeToken: 41, parked: true}
37+ parking := &attachedClient{resumeToken: 42}
38+ d.mu.Lock()
39+ d.parked[parked.resumeToken] = &parkedAttachment{owner: view, ac: parked, timer: stubTimer{}, done: make(chan struct{})}
40+ d.parking[parking.resumeToken] = &parkingAttachment{owner: view, ac: parking, done: make(chan struct{})}
41+ d.mu.Unlock()
42+
43+ d.parkRemoteViewWarm(view)
44+ warm, timer := remoteWarmTimer(t, clock, view)
45+ timer.ch <- time.Now()
46+ awaitTestCompletion(t, warm.done, "remote warm expiry did not complete")
47+
48+ d.mu.Lock()
49+ require.Empty(t, d.parked)
50+ require.Empty(t, d.parking)
51+ d.mu.Unlock()
52+ require.Zero(t, parked.resumeToken)
53+ require.False(t, parked.parked)
54+ require.Zero(t, parking.resumeToken)
55+}
56+
32 func TestRemoteViewWarmActivationFencesStaleExpiry(t *testing.T) {57 func TestRemoteViewWarmActivationFencesStaleExpiry(t *testing.T) {
33 clock := &signalClock{timers: make(chan *signalTimer, 1)}58 clock := &signalClock{timers: make(chan *signalTimer, 1)}
34 d := newTestDaemon(t, nil, clock)59 d := newTestDaemon(t, nil, clock)
M internal/usecase/daemon/resume.go +32 -1
@@ -146,6 +146,20 @@ func (d *Daemon) purgeParkingForSessionLocked(sess *session) {
146146 }
147147 }
148148
149+// purgeParkingForRemoteViewLocked invalidates in-flight resume publication for
150+// one terminal remote view. At this point the view has no attachment members,
151+// so clearing the credential cannot race a live owner.
152+func (d *Daemon) purgeParkingForRemoteViewLocked(view *remoteView) {
153+ for token, pending := range d.parking {
154+ if owner, remote := normalizeAttachmentOwner(pending.owner).(*remoteView); remote && owner == view {
155+ delete(d.parking, token)
156+ pending.ac.resumeToken = 0
157+ pending.ac.parked = false
158+ pending.closeDone()
159+ }
160+ }
161+}
162+
149163 // waitParkingInFlight waits for an identified same-client parking publication
150164 // to finish. Returns true when a matching in-flight entry was observed.
151165 // Unknown and mismatched credentials return false without waiting.
@@ -353,11 +367,12 @@ func (d *Daemon) abortResumeClaim(ac *attachedClient) bool {
353367 ac.parked = true
354368 ac.connectionGeneration.Add(1)
355369 sess := localSession(parked.owner)
370+ view, remote := normalizeAttachmentOwner(parked.owner).(*remoteView)
356371 if sess != nil {
357372 sess.mu.Lock()
358373 sess.unregisterAttachmentLocked(ac)
359374 sess.mu.Unlock()
360- } else if view, remote := normalizeAttachmentOwner(parked.owner).(*remoteView); remote {
375+ } else if remote {
361376 view.mu.Lock()
362377 view.unregisterAttachmentLocked(ac)
363378 view.mu.Unlock()
@@ -383,6 +398,9 @@ func (d *Daemon) abortResumeClaim(ac *attachedClient) bool {
383398 if sess != nil {
384399 d.recalculateSessionGeometryAndInvalidate(sess, nil, "resume.go")
385400 }
401+ if remote {
402+ d.parkRemoteViewWarm(view)
403+ }
386404 d.watchParkedTimer(token, rearmed)
387405 if captured != nil {
388406 _ = ac.closeCapturedTransport(ac.revokeTransport(captured))
@@ -423,6 +441,19 @@ func (d *Daemon) purgeParkedForSessionLocked(sess *session) []parkedAttachmentRe
423441 return retirements
424442 }
425443
444+// purgeParkedForRemoteViewLocked retires every resume credential owned by the
445+// exact remote view. The caller completes the returned external cleanup after
446+// releasing d.mu.
447+func (d *Daemon) purgeParkedForRemoteViewLocked(view *remoteView) []parkedAttachmentRetirement {
448+ var retirements []parkedAttachmentRetirement
449+ for token, parked := range d.parked {
450+ if owner, remote := normalizeAttachmentOwner(parked.owner).(*remoteView); remote && owner == view {
451+ retirements = append(retirements, d.retireParkedAttachmentLocked(token, parked))
452+ }
453+ }
454+ return retirements
455+}
456+
426457 func (d *Daemon) purgeAllParkedLocked() []parkedAttachmentRetirement {
427458 retirements := make([]parkedAttachmentRetirement, 0, len(d.parked))
428459 for token, parked := range d.parked {
@@ -146,6 +146,20 @@ func (d *Daemon) purgeParkingForSessionLocked(sess *session) {
146 }146 }
147 }147 }
148 148
149+// purgeParkingForRemoteViewLocked invalidates in-flight resume publication for
150+// one terminal remote view. At this point the view has no attachment members,
151+// so clearing the credential cannot race a live owner.
152+func (d *Daemon) purgeParkingForRemoteViewLocked(view *remoteView) {
153+ for token, pending := range d.parking {
154+ if owner, remote := normalizeAttachmentOwner(pending.owner).(*remoteView); remote && owner == view {
155+ delete(d.parking, token)
156+ pending.ac.resumeToken = 0
157+ pending.ac.parked = false
158+ pending.closeDone()
159+ }
160+ }
161+}
162+
149 // waitParkingInFlight waits for an identified same-client parking publication163 // waitParkingInFlight waits for an identified same-client parking publication
150 // to finish. Returns true when a matching in-flight entry was observed.164 // to finish. Returns true when a matching in-flight entry was observed.
151 // Unknown and mismatched credentials return false without waiting.165 // Unknown and mismatched credentials return false without waiting.
@@ -353,11 +367,12 @@ func (d *Daemon) abortResumeClaim(ac *attachedClient) bool {
353 ac.parked = true367 ac.parked = true
354 ac.connectionGeneration.Add(1)368 ac.connectionGeneration.Add(1)
355 sess := localSession(parked.owner)369 sess := localSession(parked.owner)
370+ view, remote := normalizeAttachmentOwner(parked.owner).(*remoteView)
356 if sess != nil {371 if sess != nil {
357 sess.mu.Lock()372 sess.mu.Lock()
358 sess.unregisterAttachmentLocked(ac)373 sess.unregisterAttachmentLocked(ac)
359 sess.mu.Unlock()374 sess.mu.Unlock()
360- } else if view, remote := normalizeAttachmentOwner(parked.owner).(*remoteView); remote {375+ } else if remote {
361 view.mu.Lock()376 view.mu.Lock()
362 view.unregisterAttachmentLocked(ac)377 view.unregisterAttachmentLocked(ac)
363 view.mu.Unlock()378 view.mu.Unlock()
@@ -383,6 +398,9 @@ func (d *Daemon) abortResumeClaim(ac *attachedClient) bool {
383 if sess != nil {398 if sess != nil {
384 d.recalculateSessionGeometryAndInvalidate(sess, nil, "resume.go")399 d.recalculateSessionGeometryAndInvalidate(sess, nil, "resume.go")
385 }400 }
401+ if remote {
402+ d.parkRemoteViewWarm(view)
403+ }
386 d.watchParkedTimer(token, rearmed)404 d.watchParkedTimer(token, rearmed)
387 if captured != nil {405 if captured != nil {
388 _ = ac.closeCapturedTransport(ac.revokeTransport(captured))406 _ = ac.closeCapturedTransport(ac.revokeTransport(captured))
@@ -423,6 +441,19 @@ func (d *Daemon) purgeParkedForSessionLocked(sess *session) []parkedAttachmentRe
423 return retirements441 return retirements
424 }442 }
425 443
444+// purgeParkedForRemoteViewLocked retires every resume credential owned by the
445+// exact remote view. The caller completes the returned external cleanup after
446+// releasing d.mu.
447+func (d *Daemon) purgeParkedForRemoteViewLocked(view *remoteView) []parkedAttachmentRetirement {
448+ var retirements []parkedAttachmentRetirement
449+ for token, parked := range d.parked {
450+ if owner, remote := normalizeAttachmentOwner(parked.owner).(*remoteView); remote && owner == view {
451+ retirements = append(retirements, d.retireParkedAttachmentLocked(token, parked))
452+ }
453+ }
454+ return retirements
455+}
456+
426 func (d *Daemon) purgeAllParkedLocked() []parkedAttachmentRetirement {457 func (d *Daemon) purgeAllParkedLocked() []parkedAttachmentRetirement {
427 retirements := make([]parkedAttachmentRetirement, 0, len(d.parked))458 retirements := make([]parkedAttachmentRetirement, 0, len(d.parked))
428 for token, parked := range d.parked {459 for token, parked := range d.parked {

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 13

↑↓ moveenter openesc close