Skip to content

Commit 9c8faad

Browse files
authored
conn: skip timeout callbacks for background contexts (#566)
Every frame read and write registered a context.AfterFunc callback so a context cancellation could close the connection. context.Background cannot be canceled because its Done channel is nil, but the code still constructed, stored, stopped, and cleared a callback registration for each operation. Skip that work when ctx.Done() is nil, and only clear a timeout when one was actually installed. BenchmarkConn also joins its writer goroutine after timing so repeated benchmark runs finish cleanly. The results below compare parent 7039364 with this commit on an Apple M4 Max using GOMAXPROCS=1 over 10 samples. A temporary in-package harness, not included in this commit, repeatedly called one internal frame read using either context.Background or an uncanceled context.WithCancel. Header reads parse a minimal frame header; payload reads copy 512 bytes from a buffered repeating reader. Before this change, setupReadTimeout called context.AfterFunc even for context.Background. Avoiding that dead registration reduces background header reads from 121.95 to 29.94 ns/op and payload reads from 124.17 to 29.15 ns/op. Both fall from 152 B/op and 4 allocs/op to zero. Cancelable contexts still need an AfterFunc registration, so they remain at 152 B/op and 4 allocs/op. BenchmarkConn/disabledCompress uses a cancelable context as well, so it remains at 1219 B/op and 32 allocs/op.
1 parent 7039364 commit 9c8faad

4 files changed

Lines changed: 40 additions & 17 deletions

File tree

conn.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,24 +168,34 @@ func (c *Conn) close() error {
168168
return err
169169
}
170170

171-
func (c *Conn) setupWriteTimeout(ctx context.Context) {
171+
func (c *Conn) setupWriteTimeout(ctx context.Context) bool {
172+
if ctx.Done() == nil {
173+
return false
174+
}
175+
172176
stop := context.AfterFunc(ctx, func() {
173177
c.clearWriteTimeout()
174178
c.close()
175179
})
176180
swapTimeoutStop(&c.writeTimeoutStop, &stop)
181+
return true
177182
}
178183

179184
func (c *Conn) clearWriteTimeout() {
180185
swapTimeoutStop(&c.writeTimeoutStop, nil)
181186
}
182187

183-
func (c *Conn) setupReadTimeout(ctx context.Context) {
188+
func (c *Conn) setupReadTimeout(ctx context.Context) bool {
189+
if ctx.Done() == nil {
190+
return false
191+
}
192+
184193
stop := context.AfterFunc(ctx, func() {
185194
c.clearReadTimeout()
186195
c.close()
187196
})
188197
swapTimeoutStop(&c.readTimeoutStop, &stop)
198+
return true
189199
}
190200

191201
func (c *Conn) clearReadTimeout() {

conn_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,10 +592,11 @@ func BenchmarkConn(b *testing.B) {
592592
msg := []byte(strings.Repeat("1234", 128))
593593
readBuf := make([]byte, len(msg))
594594
writes := make(chan struct{})
595-
defer close(writes)
596595
werrs := make(chan error)
596+
writerDone := make(chan struct{})
597597

598598
go func() {
599+
defer close(writerDone)
599600
for range writes {
600601
select {
601602
case werrs <- c1.Write(bb.ctx, websocket.MessageText, msg):
@@ -650,6 +651,13 @@ func BenchmarkConn(b *testing.B) {
650651
}
651652
b.StopTimer()
652653

654+
close(writes)
655+
select {
656+
case <-writerDone:
657+
case <-bb.ctx.Done():
658+
b.Fatal(bb.ctx.Err())
659+
}
660+
653661
b.ReportMetric(float64(*bytesWritten/b.N), "written/op")
654662
b.ReportMetric(float64(*bytesRead/b.N), "read/op")
655663

read.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -218,29 +218,33 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) {
218218
}
219219

220220
// prepareRead sets the read timeout and checks whether the connection is closed.
221-
func (c *Conn) prepareRead(ctx context.Context) error {
221+
func (c *Conn) prepareRead(ctx context.Context) (bool, error) {
222222
select {
223223
case <-c.closed:
224-
return net.ErrClosed
224+
return false, net.ErrClosed
225225
default:
226226
}
227-
c.setupReadTimeout(ctx)
227+
timeoutSet := c.setupReadTimeout(ctx)
228228

229229
c.closeStateMu.Lock()
230230
closeReceivedErr := c.closeReceivedErr
231231
c.closeStateMu.Unlock()
232232
if closeReceivedErr != nil {
233-
c.clearReadTimeout()
234-
return closeReceivedErr
233+
if timeoutSet {
234+
c.clearReadTimeout()
235+
}
236+
return false, closeReceivedErr
235237
}
236238

237-
return nil
239+
return timeoutSet, nil
238240
}
239241

240242
// finishRead clears the read timeout and reports whether the connection or
241243
// operation context ended while the read was in progress.
242-
func (c *Conn) finishRead(ctx context.Context, err *error) {
243-
c.clearReadTimeout()
244+
func (c *Conn) finishRead(ctx context.Context, err *error, timeoutSet bool) {
245+
if timeoutSet {
246+
c.clearReadTimeout()
247+
}
244248
select {
245249
case <-c.closed:
246250
if *err != nil {
@@ -254,11 +258,11 @@ func (c *Conn) finishRead(ctx context.Context, err *error) {
254258
}
255259

256260
func (c *Conn) readFrameHeader(ctx context.Context) (_ header, err error) {
257-
err = c.prepareRead(ctx)
261+
timeoutSet, err := c.prepareRead(ctx)
258262
if err != nil {
259263
return header{}, err
260264
}
261-
defer c.finishRead(ctx, &err)
265+
defer c.finishRead(ctx, &err, timeoutSet)
262266

263267
h, err := readFrameHeader(c.br, c.readHeaderBuf[:])
264268
if err != nil {
@@ -269,11 +273,11 @@ func (c *Conn) readFrameHeader(ctx context.Context) (_ header, err error) {
269273
}
270274

271275
func (c *Conn) readFramePayload(ctx context.Context, p []byte) (_ int, err error) {
272-
err = c.prepareRead(ctx)
276+
timeoutSet, err := c.prepareRead(ctx)
273277
if err != nil {
274278
return 0, err
275279
}
276-
defer c.finishRead(ctx, &err)
280+
defer c.finishRead(ctx, &err, timeoutSet)
277281

278282
n, err := io.ReadFull(c.br, p)
279283
if err != nil {

write.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,9 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco
318318
return 0, net.ErrClosed
319319
default:
320320
}
321-
c.setupWriteTimeout(ctx)
322-
defer c.clearWriteTimeout()
321+
if c.setupWriteTimeout(ctx) {
322+
defer c.clearWriteTimeout()
323+
}
323324

324325
c.writeHeader.fin = fin
325326
c.writeHeader.opcode = opcode

0 commit comments

Comments
 (0)