fix(remote): arm proxy expiry after detach
- commit
- 59f9ff589a4fa290a7cf09b554e6d8cd8ebffeb7
- author
- brice · · 2026-07-31 15:56 UTC
- parent
- eba0907
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +25 -1
M internal/usecase/daemon/client_transport_errors.go +0 -1
| @@ -131,7 +131,6 @@ func (d *Daemon) detachProxyOnSendError(p *proxySession, ac *attachedClient, fai | |||
| 131 | 131 | d.unregisterPreview(ac) | |
| 132 | 132 | ac.clearPreviousSession() | |
| 133 | 133 | _ = ac.closeCapturedTransport(failed) | |
| 134 | - | d.armProxyWarm(p) | |
| 135 | 134 | d.log.Warn("detached proxy client after send error", "host", p.key.Host, "session", p.key.Name) | |
| 136 | 135 | } | |
| 137 | 136 | ||
| @@ -131,7 +131,6 @@ func (d *Daemon) detachProxyOnSendError(p *proxySession, ac *attachedClient, fai | |||||
| 131 | d.unregisterPreview(ac) | 131 | d.unregisterPreview(ac) | ||
| 132 | ac.clearPreviousSession() | 132 | ac.clearPreviousSession() | ||
| 133 | _ = ac.closeCapturedTransport(failed) | 133 | _ = ac.closeCapturedTransport(failed) | ||
| 134 | - | d.armProxyWarm(p) | |||
| 135 | d.log.Warn("detached proxy client after send error", "host", p.key.Host, "session", p.key.Name) | 134 | d.log.Warn("detached proxy client after send error", "host", p.key.Host, "session", p.key.Name) | ||
| 136 | } | 135 | } | ||
| 137 | 136 | ||||
M internal/usecase/daemon/proxy_lifecycle_test.go +22 -0
| @@ -162,6 +162,28 @@ func TestArmProxyWarmKeepsExpiryPathWhenPublicationFails(t *testing.T) { | |||
| 162 | 162 | } | |
| 163 | 163 | } | |
| 164 | 164 | ||
| 165 | + | func TestDetachProxyIfCurrentTransportArmsExpiredProxy(t *testing.T) { | |
| 166 | + | d, p, clock := newProxyLifecycleFixture(t) | |
| 167 | + | transport := newProxyTestTransport() | |
| 168 | + | t.Cleanup(func() { _ = transport.Close() }) | |
| 169 | + | ac := &attachedClient{tr: transport, output: newOutputStateStream()} | |
| 170 | + | ac.setSession(p) | |
| 171 | + | setProxyLifecycleClient(p, ac) | |
| 172 | + | p.mu.Lock() | |
| 173 | + | p.expired = true | |
| 174 | + | p.mu.Unlock() | |
| 175 | + | ||
| 176 | + | require.True(t, d.detachProxyIfCurrentTransport(p, ac, ac.transportSnapshot())) | |
| 177 | + | awaitTestValue(t, clock.timers, "expired proxy detach did not arm a warm timer") | |
| 178 | + | token, generation := proxyWarmToken(p) | |
| 179 | + | require.NotNil(t, token, "expired headless proxy has no expiry path") | |
| 180 | + | require.Equal(t, generation, token.generation) | |
| 181 | + | require.Nil(t, ac.currentAttachmentSession()) | |
| 182 | + | ||
| 183 | + | token.stop() | |
| 184 | + | awaitTestCompletion(t, token.done, "warm timer did not stop during cleanup") | |
| 185 | + | } | |
| 186 | + | ||
| 165 | 187 | func TestMarkProxyReplacedWarmTimerOwnership(t *testing.T) { | |
| 166 | 188 | t.Run("rekeys an already armed timer", func(t *testing.T) { | |
| 167 | 189 | d, p, clock := newProxyLifecycleFixture(t) | |
| @@ -162,6 +162,28 @@ func TestArmProxyWarmKeepsExpiryPathWhenPublicationFails(t *testing.T) { | |||||
| 162 | } | 162 | } | ||
| 163 | } | 163 | } | ||
| 164 | 164 | ||||
| 165 | + | func TestDetachProxyIfCurrentTransportArmsExpiredProxy(t *testing.T) { | |||
| 166 | + | d, p, clock := newProxyLifecycleFixture(t) | |||
| 167 | + | transport := newProxyTestTransport() | |||
| 168 | + | t.Cleanup(func() { _ = transport.Close() }) | |||
| 169 | + | ac := &attachedClient{tr: transport, output: newOutputStateStream()} | |||
| 170 | + | ac.setSession(p) | |||
| 171 | + | setProxyLifecycleClient(p, ac) | |||
| 172 | + | p.mu.Lock() | |||
| 173 | + | p.expired = true | |||
| 174 | + | p.mu.Unlock() | |||
| 175 | + | ||||
| 176 | + | require.True(t, d.detachProxyIfCurrentTransport(p, ac, ac.transportSnapshot())) | |||
| 177 | + | awaitTestValue(t, clock.timers, "expired proxy detach did not arm a warm timer") | |||
| 178 | + | token, generation := proxyWarmToken(p) | |||
| 179 | + | require.NotNil(t, token, "expired headless proxy has no expiry path") | |||
| 180 | + | require.Equal(t, generation, token.generation) | |||
| 181 | + | require.Nil(t, ac.currentAttachmentSession()) | |||
| 182 | + | ||||
| 183 | + | token.stop() | |||
| 184 | + | awaitTestCompletion(t, token.done, "warm timer did not stop during cleanup") | |||
| 185 | + | } | |||
| 186 | + | ||||
| 165 | func TestMarkProxyReplacedWarmTimerOwnership(t *testing.T) { | 187 | func TestMarkProxyReplacedWarmTimerOwnership(t *testing.T) { | ||
| 166 | t.Run("rekeys an already armed timer", func(t *testing.T) { | 188 | t.Run("rekeys an already armed timer", func(t *testing.T) { | ||
| 167 | d, p, clock := newProxyLifecycleFixture(t) | 189 | d, p, clock := newProxyLifecycleFixture(t) | ||
M internal/usecase/daemon/session.go +3 -0
| @@ -778,6 +778,9 @@ func (d *Daemon) detachProxyIfCurrentTransport(p *proxySession, ac *attachedClie | |||
| 778 | 778 | } | |
| 779 | 779 | p.sessionCore.mu.Unlock() | |
| 780 | 780 | d.notices.routingMu.Unlock() | |
| 781 | + | if current { | |
| 782 | + | d.armProxyWarm(p) | |
| 783 | + | } | |
| 781 | 784 | return current | |
| 782 | 785 | } | |
| 783 | 786 | ||
| @@ -778,6 +778,9 @@ func (d *Daemon) detachProxyIfCurrentTransport(p *proxySession, ac *attachedClie | |||||
| 778 | } | 778 | } | ||
| 779 | p.sessionCore.mu.Unlock() | 779 | p.sessionCore.mu.Unlock() | ||
| 780 | d.notices.routingMu.Unlock() | 780 | d.notices.routingMu.Unlock() | ||
| 781 | + | if current { | |||
| 782 | + | d.armProxyWarm(p) | |||
| 783 | + | } | |||
| 781 | return current | 784 | return current | ||
| 782 | } | 785 | } | ||
| 783 | 786 | ||||