Skip to content

Commit 6d566c3

Browse files
committed
internal/daemon: batch and backpressure PTY output
1 parent f20decb commit 6d566c3

5 files changed

Lines changed: 395 additions & 103 deletions

File tree

internal/daemon/server_attach.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ func (s *Server) watchSession(sess *Session) {
235235
empty := len(s.sessions) == 0
236236
s.mu.Unlock()
237237
if s.autoExit && empty {
238+
sess.waitClients()
238239
slog.Info("auto-exit: last session ended, shutting down")
239240
s.Shutdown()
240241
}

internal/daemon/session.go

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515

1616
var feedPool = sync.Pool{
1717
New: func() any {
18-
b := make([]byte, 32*1024)
18+
b := make([]byte, ptyBatchSize)
1919
return &b
2020
},
2121
}
@@ -45,6 +45,9 @@ type sessionClient struct {
4545
version string
4646
readOnly bool
4747
outCh chan protocol.Message
48+
ready chan<- struct{}
49+
writeDone chan struct{}
50+
final protocol.Message
4851
}
4952

5053
type sessionAction interface {
@@ -107,22 +110,26 @@ type Session struct {
107110
PID uint32
108111
CreatedAt time.Time
109112

110-
ptmx *os.File
111-
cmd *exec.Cmd
112-
term *terminalState
113-
feedCh chan feedItem
114-
tempDir string
113+
ptmx *os.File
114+
cmd *exec.Cmd
115+
term *terminalState
116+
feedCh chan feedItem
117+
feedDone chan struct{}
118+
ptyDone chan struct{}
119+
tempDir string
115120

116-
actions chan sessionAction
117-
ptyOut chan []byte
118-
done chan struct{}
119-
exitCode int32
121+
actions chan sessionAction
122+
ptyOut chan []byte
123+
clientReady chan struct{}
124+
done chan struct{}
125+
exitCode int32
120126

121127
// sizeVal packs cols|rows as (cols<<16)|rows for lock-free reads.
122128
sizeVal atomic.Uint32
123129

124-
resizePolicy config.ResizePolicy
125-
ctx context.Context
130+
resizePolicy config.ResizePolicy
131+
clientWriters sync.WaitGroup
132+
ctx context.Context
126133
}
127134

128135
func (s *Session) size() (uint16, uint16) {

internal/daemon/session_clients.go

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,36 @@ import (
66
"log/slog"
77
"math"
88
"syscall"
9-
"time"
109

1110
"code.selman.me/hauntty/internal/config"
1211
"code.selman.me/hauntty/internal/protocol"
1312
"github.com/creack/pty"
1413
)
1514

16-
const (
17-
sessionClientOutBufferSize = 256
18-
slowClientGracePeriod = 100 * time.Millisecond
19-
)
15+
const sessionClientOutBufferSize = 256
2016

2117
func (c *sessionClient) writeLoop() {
18+
defer func() {
19+
close(c.writeDone)
20+
select {
21+
case c.ready <- struct{}{}:
22+
default:
23+
}
24+
}()
25+
2226
for msg := range c.outCh {
27+
select {
28+
case c.ready <- struct{}{}:
29+
default:
30+
}
2331
if err := c.conn.WriteMessage(msg); err != nil {
24-
break
32+
_ = c.closeConn()
33+
return
34+
}
35+
}
36+
if c.final != nil {
37+
if err := c.conn.WriteMessage(c.final); err != nil {
38+
_ = c.closeConn()
2539
}
2640
}
2741
}
@@ -169,32 +183,37 @@ func removeClient(clients []*sessionClient, target *sessionClient) []*sessionCli
169183
return clients
170184
}
171185

172-
func broadcastOutput(clients []*sessionClient, name string, msg *protocol.Output) []*sessionClient {
173-
i := 0
186+
func queueOutput(clients []*sessionClient, msg *protocol.Output) []*sessionClient {
187+
var pending []*sessionClient
174188
for _, c := range clients {
175189
select {
176-
case c.outCh <- msg:
177-
clients[i] = c
178-
i++
190+
case <-c.writeDone:
179191
continue
180192
default:
181193
}
182-
183-
timer := time.NewTimer(slowClientGracePeriod)
184194
select {
185195
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)
196+
default:
197+
pending = append(pending, c)
198+
}
199+
}
200+
return pending
201+
}
202+
203+
func pruneFinishedClients(clients, pending []*sessionClient) ([]*sessionClient, []*sessionClient, bool) {
204+
kept := clients[:0]
205+
changed := false
206+
for _, c := range clients {
207+
select {
208+
case <-c.writeDone:
209+
pending = removeClient(pending, c)
193210
close(c.outCh)
194-
_ = c.closeConn()
211+
changed = true
212+
default:
213+
kept = append(kept, c)
195214
}
196215
}
197-
return clients[:i]
216+
return kept, pending, changed
198217
}
199218

200219
func notifyClientsChanged(clients []*sessionClient, sizeFn func() (uint16, uint16)) {

0 commit comments

Comments
 (0)