Skip to content

Commit b384ee7

Browse files
committed
net, os, internal/poll: test for use of sendfile
The net package's sendfile tests exercise various paths where we expect sendfile to be used, but don't verify that sendfile was in fact used. Add a hook to internal/poll.SendFile to let us verify that sendfile was called when expected. Update os package tests (which use their own hook mechanism) to use this hook as well. For #66988 Change-Id: I7afb130dcfe0063d60c6ea0f8560cf8665ad5a81 Reviewed-on: https://go-review.googlesource.com/c/go/+/581778 Reviewed-by: Ian Lance Taylor <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 1969162 commit b384ee7

13 files changed

+111
-28
lines changed

src/internal/poll/sendfile.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package poll
6+
7+
var TestHookDidSendFile = func(dstFD *FD, src int, written int64, err error, handled bool) {}

src/internal/poll/sendfile_bsd.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ const maxSendfileSize int = 4 << 20
1414

1515
// SendFile wraps the sendfile system call.
1616
func SendFile(dstFD *FD, src int, pos, remain int64) (written int64, err error, handled bool) {
17+
defer func() {
18+
TestHookDidSendFile(dstFD, src, written, err, handled)
19+
}()
1720
if err := dstFD.writeLock(); err != nil {
1821
return 0, err, false
1922
}

src/internal/poll/sendfile_linux.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ const maxSendfileSize int = 4 << 20
1212

1313
// SendFile wraps the sendfile system call.
1414
func SendFile(dstFD *FD, src int, remain int64) (written int64, err error, handled bool) {
15+
defer func() {
16+
TestHookDidSendFile(dstFD, src, written, err, handled)
17+
}()
1518
if err := dstFD.writeLock(); err != nil {
1619
return 0, err, false
1720
}

src/internal/poll/sendfile_solaris.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ const maxSendfileSize int = 4 << 20
1717

1818
// SendFile wraps the sendfile system call.
1919
func SendFile(dstFD *FD, src int, pos, remain int64) (written int64, err error, handled bool) {
20+
defer func() {
21+
TestHookDidSendFile(dstFD, src, written, err, handled)
22+
}()
2023
if err := dstFD.writeLock(); err != nil {
2124
return 0, err, false
2225
}

src/internal/poll/sendfile_windows.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111

1212
// SendFile wraps the TransmitFile call.
1313
func SendFile(fd *FD, src syscall.Handle, n int64) (written int64, err error) {
14+
defer func() {
15+
TestHookDidSendFile(fd, 0, written, err, written > 0)
16+
}()
1417
if fd.kind == kindPipe {
1518
// TransmitFile does not work with pipes
1619
return 0, syscall.ESPIPE

src/net/sendfile_linux.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"os"
1111
)
1212

13+
const supportsSendfile = true
14+
1315
// sendFile copies the contents of r to c using the sendfile
1416
// system call to minimize copies.
1517
//

src/net/sendfile_stub.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build aix || js || netbsd || openbsd || ios || wasip1
5+
//go:build !(linux || (darwin && !ios) || dragonfly || freebsd || solaris || windows)
66

77
package net
88

99
import "io"
1010

11+
const supportsSendfile = false
12+
1113
func sendFile(c *netFD, r io.Reader) (n int64, err error, handled bool) {
1214
return 0, nil, false
1315
}

src/net/sendfile_test.go

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"encoding/hex"
1212
"errors"
1313
"fmt"
14+
"internal/poll"
1415
"io"
1516
"os"
1617
"runtime"
@@ -26,6 +27,48 @@ const (
2627
newtonSHA256 = "d4a9ac22462b35e7821a4f2706c211093da678620a8f9997989ee7cf8d507bbd"
2728
)
2829

30+
// expectSendfile runs f, and verifies that internal/poll.SendFile successfully handles
31+
// a write to wantConn during f's execution.
32+
//
33+
// On platforms where supportsSendfile is false, expectSendfile runs f but does not
34+
// expect a call to SendFile.
35+
func expectSendfile(t *testing.T, wantConn Conn, f func()) {
36+
t.Helper()
37+
if !supportsSendfile {
38+
f()
39+
return
40+
}
41+
orig := poll.TestHookDidSendFile
42+
defer func() {
43+
poll.TestHookDidSendFile = orig
44+
}()
45+
var (
46+
called bool
47+
gotHandled bool
48+
gotFD *poll.FD
49+
)
50+
poll.TestHookDidSendFile = func(dstFD *poll.FD, src int, written int64, err error, handled bool) {
51+
if called {
52+
t.Error("internal/poll.SendFile called multiple times, want one call")
53+
}
54+
called = true
55+
gotHandled = handled
56+
gotFD = dstFD
57+
}
58+
f()
59+
if !called {
60+
t.Error("internal/poll.SendFile was not called, want it to be")
61+
return
62+
}
63+
if !gotHandled {
64+
t.Error("internal/poll.SendFile did not handle the write, want it to")
65+
return
66+
}
67+
if &wantConn.(*TCPConn).fd.pfd != gotFD {
68+
t.Error("internal.poll.SendFile called with unexpected FD")
69+
}
70+
}
71+
2972
func TestSendfile(t *testing.T) {
3073
ln := newLocalListener(t, "tcp")
3174
defer ln.Close()
@@ -53,7 +96,17 @@ func TestSendfile(t *testing.T) {
5396

5497
// Return file data using io.Copy, which should use
5598
// sendFile if available.
56-
sbytes, err := io.Copy(conn, f)
99+
var sbytes int64
100+
switch runtime.GOOS {
101+
case "windows":
102+
// Windows is not using sendfile for some reason:
103+
// https://go.dev/issue/67042
104+
sbytes, err = io.Copy(conn, f)
105+
default:
106+
expectSendfile(t, conn, func() {
107+
sbytes, err = io.Copy(conn, f)
108+
})
109+
}
57110
if err != nil {
58111
errc <- err
59112
return
@@ -121,7 +174,9 @@ func TestSendfileParts(t *testing.T) {
121174
for i := 0; i < 3; i++ {
122175
// Return file data using io.CopyN, which should use
123176
// sendFile if available.
124-
_, err = io.CopyN(conn, f, 3)
177+
expectSendfile(t, conn, func() {
178+
_, err = io.CopyN(conn, f, 3)
179+
})
125180
if err != nil {
126181
errc <- err
127182
return
@@ -180,7 +235,9 @@ func TestSendfileSeeked(t *testing.T) {
180235
return
181236
}
182237

183-
_, err = io.CopyN(conn, f, sendSize)
238+
expectSendfile(t, conn, func() {
239+
_, err = io.CopyN(conn, f, sendSize)
240+
})
184241
if err != nil {
185242
errc <- err
186243
return
@@ -240,6 +297,10 @@ func TestSendfilePipe(t *testing.T) {
240297
return
241298
}
242299
defer conn.Close()
300+
// The comment above states that this should call into sendfile,
301+
// but empirically it doesn't seem to do so at this time.
302+
// If it does, or does on some platforms, this CopyN should be wrapped
303+
// in expectSendfile.
243304
_, err = io.CopyN(conn, r, 1)
244305
if err != nil {
245306
t.Error(err)
@@ -333,6 +394,10 @@ func TestSendfileOnWriteTimeoutExceeded(t *testing.T) {
333394
}
334395
defer f.Close()
335396

397+
// We expect this to use sendfile, but as of the time this comment was written
398+
// poll.SendFile on an FD past its timeout can return an error indicating that
399+
// it didn't handle the operation, resulting in a non-sendfile retry.
400+
// So don't use expectSendfile here.
336401
_, err = io.Copy(conn, f)
337402
if errors.Is(err, os.ErrDeadlineExceeded) {
338403
return nil

src/net/sendfile_unix_alt.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"syscall"
1414
)
1515

16+
const supportsSendfile = true
17+
1618
// sendFile copies the contents of r to c using the sendfile
1719
// system call to minimize copies.
1820
//
@@ -35,6 +37,8 @@ func sendFile(c *netFD, r io.Reader) (written int64, err error, handled bool) {
3537
return 0, nil, true
3638
}
3739
}
40+
// r might be an *os.File or an os.fileWithoutWriteTo.
41+
// Type assert to an interface rather than *os.File directly to handle the latter case.
3842
f, ok := r.(interface {
3943
fs.File
4044
io.Seeker

src/net/sendfile_windows.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"syscall"
1212
)
1313

14+
const supportsSendfile = true
15+
1416
// sendFile copies the contents of r to c using the TransmitFile
1517
// system call to minimize copies.
1618
//

src/os/export_linux_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,5 @@ package os
77
var (
88
PollCopyFileRangeP = &pollCopyFileRange
99
PollSpliceFile = &pollSplice
10-
PollSendFile = &pollSendFile
1110
GetPollFDAndNetwork = getPollFDAndNetwork
1211
)

src/os/writeto_linux_test.go

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -109,38 +109,29 @@ func newSendFileTest(t *testing.T, proto string, size int64) (net.Conn, *File, n
109109

110110
func hookSendFile(t *testing.T) *sendFileHook {
111111
h := new(sendFileHook)
112-
h.install()
113-
t.Cleanup(h.uninstall)
112+
orig := poll.TestHookDidSendFile
113+
t.Cleanup(func() {
114+
poll.TestHookDidSendFile = orig
115+
})
116+
poll.TestHookDidSendFile = func(dstFD *poll.FD, src int, written int64, err error, handled bool) {
117+
h.called = true
118+
h.dstfd = dstFD.Sysfd
119+
h.srcfd = src
120+
h.written = written
121+
h.err = err
122+
h.handled = handled
123+
}
114124
return h
115125
}
116126

117127
type sendFileHook struct {
118128
called bool
119129
dstfd int
120130
srcfd int
121-
remain int64
122131

123132
written int64
124133
handled bool
125134
err error
126-
127-
original func(dst *poll.FD, src int, remain int64) (int64, error, bool)
128-
}
129-
130-
func (h *sendFileHook) install() {
131-
h.original = *PollSendFile
132-
*PollSendFile = func(dst *poll.FD, src int, remain int64) (int64, error, bool) {
133-
h.called = true
134-
h.dstfd = dst.Sysfd
135-
h.srcfd = src
136-
h.remain = remain
137-
h.written, h.err, h.handled = h.original(dst, src, remain)
138-
return h.written, h.err, h.handled
139-
}
140-
}
141-
142-
func (h *sendFileHook) uninstall() {
143-
*PollSendFile = h.original
144135
}
145136

146137
func createTempFile(t *testing.T, size int64) (*File, []byte) {

src/os/zero_copy_linux.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
var (
1414
pollCopyFileRange = poll.CopyFileRange
1515
pollSplice = poll.Splice
16-
pollSendFile = poll.SendFile
1716
)
1817

1918
// wrapSyscallError takes an error and a syscall name. If the error is
@@ -38,7 +37,7 @@ func (f *File) writeTo(w io.Writer) (written int64, handled bool, err error) {
3837
}
3938

4039
rerr := sc.Read(func(fd uintptr) (done bool) {
41-
written, err, handled = pollSendFile(pfd, int(fd), 1<<63-1)
40+
written, err, handled = poll.SendFile(pfd, int(fd), 1<<63-1)
4241
return true
4342
})
4443

0 commit comments

Comments
 (0)