fix(daemon): release switch purge admission once membership commits
Holding it through transition cleanups and first paint let a slow client stall a concurrent KillAll drain until it timed out.
- commit
- fab9d890a7d3eda20cd7e31600b8a238801f3ae7
- author
- brice · · 2026-10-08 06:22 UTC
- committer
- brice · · 2026-10-08 06:24 UTC
- parent
- 261a65d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +8 -1
M internal/usecase/daemon/same_peer_switch.go +8 -1
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | package daemon | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "sync" | |
| 5 | + | ||
| 4 | 6 | "github.com/bnema/vev/internal/domain" | |
| 5 | 7 | "github.com/bnema/vev/internal/protocol" | |
| 6 | 8 | ) | |
| @@ -82,7 +84,11 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p | |||
| 82 | 84 | } | |
| 83 | 85 | d.purgeAdmissionActive++ | |
| 84 | 86 | d.mu.Unlock() | |
| 85 | - | defer d.releasePurgeAdmission() | |
| 87 | + | // The reservation covers only the membership change; first paint can | |
| 88 | + | // block on a slow client and must not stall a purge's drain. | |
| 89 | + | var releaseOnce sync.Once | |
| 90 | + | release := func() { releaseOnce.Do(d.releasePurgeAdmission) } | |
| 91 | + | defer release() | |
| 86 | 92 | ||
| 87 | 93 | target, targetTabIndex, ok := d.samePeerTarget(request) | |
| 88 | 94 | if !ok || target == effect.sess { | |
| @@ -112,6 +118,7 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p | |||
| 112 | 118 | d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget) | |
| 113 | 119 | return | |
| 114 | 120 | } | |
| 121 | + | release() | |
| 115 | 122 | ||
| 116 | 123 | if fresh, admitted := effect.ac.beginAttachmentEffect(transition.published); admitted { | |
| 117 | 124 | fresh.End() | |
| @@ -1,6 +1,8 @@ | |||||
| 1 | package daemon | 1 | package daemon | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "sync" | |||
| 5 | + | ||||
| 4 | "github.com/bnema/vev/internal/domain" | 6 | "github.com/bnema/vev/internal/domain" | ||
| 5 | "github.com/bnema/vev/internal/protocol" | 7 | "github.com/bnema/vev/internal/protocol" | ||
| 6 | ) | 8 | ) | ||
| @@ -82,7 +84,11 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p | |||||
| 82 | } | 84 | } | ||
| 83 | d.purgeAdmissionActive++ | 85 | d.purgeAdmissionActive++ | ||
| 84 | d.mu.Unlock() | 86 | d.mu.Unlock() | ||
| 85 | - | defer d.releasePurgeAdmission() | 87 | + | // The reservation covers only the membership change; first paint can |
| 88 | + | // block on a slow client and must not stall a purge's drain. | |||
| 89 | + | var releaseOnce sync.Once | |||
| 90 | + | release := func() { releaseOnce.Do(d.releasePurgeAdmission) } | |||
| 91 | + | defer release() | |||
| 86 | 92 | ||||
| 87 | target, targetTabIndex, ok := d.samePeerTarget(request) | 93 | target, targetTabIndex, ok := d.samePeerTarget(request) | ||
| 88 | if !ok || target == effect.sess { | 94 | if !ok || target == effect.sess { | ||
| @@ -112,6 +118,7 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p | |||||
| 112 | d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget) | 118 | d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget) | ||
| 113 | return | 119 | return | ||
| 114 | } | 120 | } | ||
| 121 | + | release() | |||
| 115 | 122 | ||||
| 116 | if fresh, admitted := effect.ac.beginAttachmentEffect(transition.published); admitted { | 123 | if fresh, admitted := effect.ac.beginAttachmentEffect(transition.published); admitted { | ||
| 117 | fresh.End() | 124 | fresh.End() | ||