Skip to content

Commit 4d6200d

Browse files
committed
grace
1 parent 7fbd0a3 commit 4d6200d

3 files changed

Lines changed: 53 additions & 2 deletions

File tree

internal/daemon/session_clients.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@ import (
66
"log/slog"
77
"math"
88
"syscall"
9+
"time"
910

1011
"code.selman.me/hauntty/internal/config"
1112
"code.selman.me/hauntty/internal/protocol"
1213
"github.com/creack/pty"
1314
)
1415

16+
const (
17+
sessionClientOutBufferSize = 256
18+
slowClientGracePeriod = 100 * time.Millisecond
19+
)
20+
1521
func (c *sessionClient) writeLoop() {
1622
for msg := range c.outCh {
1723
if err := c.conn.WriteMessage(msg); err != nil {
@@ -170,8 +176,20 @@ func broadcastOutput(clients []*sessionClient, name string, msg *protocol.Output
170176
case c.outCh <- msg:
171177
clients[i] = c
172178
i++
179+
continue
173180
default:
174-
slog.Debug("evicting slow client", "session", name)
181+
}
182+
183+
timer := time.NewTimer(slowClientGracePeriod)
184+
select {
185+
case c.outCh <- msg:
186+
if !timer.Stop() {
187+
<-timer.C
188+
}
189+
clients[i] = c
190+
i++
191+
case <-timer.C:
192+
slog.Debug("evicting slow client", "session", name, "grace", slowClientGracePeriod)
175193
close(c.outCh)
176194
_ = c.closeConn()
177195
}

internal/daemon/session_lifecycle.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ func (s *Session) run() {
257257
size: a.spec.size,
258258
version: a.spec.version,
259259
readOnly: a.spec.readOnly,
260-
outCh: make(chan protocol.Message, 64),
260+
outCh: make(chan protocol.Message, sessionClientOutBufferSize),
261261
}
262262
go sc.writeLoop()
263263

internal/daemon/session_loop_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,39 @@ func TestBroadcastOutputEvictsSlowClients(t *testing.T) {
256256
assert.DeepEqual(t, got, msg)
257257
}
258258

259+
func TestBroadcastOutputAllowsTransientBackpressure(t *testing.T) {
260+
msg := &protocol.Output{Data: []byte("hello")}
261+
262+
var closeCount atomic.Int32
263+
slow := &sessionClient{
264+
id: "slow",
265+
closeConn: func() error {
266+
closeCount.Add(1)
267+
return nil
268+
},
269+
outCh: make(chan protocol.Message, 1),
270+
}
271+
busy := &protocol.Output{Data: []byte("busy")}
272+
slow.outCh <- busy
273+
274+
drainDone := make(chan struct{})
275+
go func() {
276+
time.Sleep(slowClientGracePeriod / 4)
277+
<-slow.outCh
278+
close(drainDone)
279+
}()
280+
281+
clients := broadcastOutput([]*sessionClient{slow}, "demo", msg)
282+
<-drainDone
283+
284+
assert.Equal(t, len(clients), 1)
285+
assert.Assert(t, clients[0] == slow)
286+
assert.Equal(t, closeCount.Load(), int32(0))
287+
288+
got := <-slow.outCh
289+
assert.DeepEqual(t, got, msg)
290+
}
291+
259292
func TestNotifyClientsChangedSkipsBlockedClients(t *testing.T) {
260293
ready := &sessionClient{outCh: make(chan protocol.Message, 1)}
261294
blocked := &sessionClient{outCh: make(chan protocol.Message, 1)}

0 commit comments

Comments
 (0)