Skip to content

Commit f2316e3

Browse files
committed
net/http: refactor ResponseWriter.ReadFrom to permit splice on Linux
Rather than probe and guess if sendfile will work inside ResponseWriter.ReadFrom(src), this change fixes the underlying issue of starting to respond before src is readable We'll no longer send a status OK if a header has not yet been written and reading from src is destined to fail. This small change implicitly takes care of the need for the server to sniff the response body to determine the Content-Type. This allows splice to work on Linux when src is a socket or any non-regular file that's spliceable. The extra read of 512 bytes may raise an objection, and that's fair, but we're already swapping some syscall prep work for another and a read of 512 probably will not impact the overall performance. For shorter bodies, there's likely less setup time. A little initial slop is not too unusual in zero copy network code, and sometimes actually helps. Fixes golang#40888 Change-Id: I4a8e2ad0ace1318bae66dae5671d06ea6d4838ed GitHub-Last-Rev: 097364e GitHub-Pull-Request: golang#40903 Reviewed-on: https://go-review.googlesource.com/c/go/+/249238 Run-TryBot: Emmanuel Odeke <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Emmanuel Odeke <[email protected]>
1 parent 1fa328f commit f2316e3

File tree

2 files changed

+46
-36
lines changed

2 files changed

+46
-36
lines changed

src/net/http/fs_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,14 @@ func TestLinuxSendfile(t *testing.T) {
11361136
t.Skipf("skipping; failed to run strace: %v", err)
11371137
}
11381138

1139+
filename := fmt.Sprintf("1kb-%d", os.Getpid())
1140+
filepath := path.Join(os.TempDir(), filename)
1141+
1142+
if err := ioutil.WriteFile(filepath, bytes.Repeat([]byte{'a'}, 1<<10), 0755); err != nil {
1143+
t.Fatal(err)
1144+
}
1145+
defer os.Remove(filepath)
1146+
11391147
var buf bytes.Buffer
11401148
child := exec.Command("strace", "-f", "-q", os.Args[0], "-test.run=TestLinuxSendfileChild")
11411149
child.ExtraFiles = append(child.ExtraFiles, lnf)
@@ -1146,7 +1154,7 @@ func TestLinuxSendfile(t *testing.T) {
11461154
t.Skipf("skipping; failed to start straced child: %v", err)
11471155
}
11481156

1149-
res, err := Get(fmt.Sprintf("http://%s/", ln.Addr()))
1157+
res, err := Get(fmt.Sprintf("http://%s/%s", ln.Addr(), filename))
11501158
if err != nil {
11511159
t.Fatalf("http client error: %v", err)
11521160
}
@@ -1192,7 +1200,7 @@ func TestLinuxSendfileChild(*testing.T) {
11921200
panic(err)
11931201
}
11941202
mux := NewServeMux()
1195-
mux.Handle("/", FileServer(Dir("testdata")))
1203+
mux.Handle("/", FileServer(Dir(os.TempDir())))
11961204
mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
11971205
os.Exit(0)
11981206
})

src/net/http/server.go

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -561,51 +561,53 @@ type writerOnly struct {
561561
io.Writer
562562
}
563563

564-
func srcIsRegularFile(src io.Reader) (isRegular bool, err error) {
565-
switch v := src.(type) {
566-
case *os.File:
567-
fi, err := v.Stat()
568-
if err != nil {
569-
return false, err
570-
}
571-
return fi.Mode().IsRegular(), nil
572-
case *io.LimitedReader:
573-
return srcIsRegularFile(v.R)
574-
default:
575-
return
576-
}
577-
}
578-
579564
// ReadFrom is here to optimize copying from an *os.File regular file
580-
// to a *net.TCPConn with sendfile.
565+
// to a *net.TCPConn with sendfile, or from a supported src type such
566+
// as a *net.TCPConn on Linux with splice.
581567
func (w *response) ReadFrom(src io.Reader) (n int64, err error) {
568+
bufp := copyBufPool.Get().(*[]byte)
569+
buf := *bufp
570+
defer copyBufPool.Put(bufp)
571+
582572
// Our underlying w.conn.rwc is usually a *TCPConn (with its
583-
// own ReadFrom method). If not, or if our src isn't a regular
584-
// file, just fall back to the normal copy method.
573+
// own ReadFrom method). If not, just fall back to the normal
574+
// copy method.
585575
rf, ok := w.conn.rwc.(io.ReaderFrom)
586-
regFile, err := srcIsRegularFile(src)
587-
if err != nil {
588-
return 0, err
589-
}
590-
if !ok || !regFile {
591-
bufp := copyBufPool.Get().(*[]byte)
592-
defer copyBufPool.Put(bufp)
593-
return io.CopyBuffer(writerOnly{w}, src, *bufp)
576+
if !ok {
577+
return io.CopyBuffer(writerOnly{w}, src, buf)
594578
}
595579

596580
// sendfile path:
597581

598-
if !w.wroteHeader {
599-
w.WriteHeader(StatusOK)
600-
}
582+
// Do not start actually writing response until src is readable.
583+
// If body length is <= sniffLen, sendfile/splice path will do
584+
// little anyway. This small read also satisfies sniffing the
585+
// body in case Content-Type is missing.
586+
nr, er := src.Read(buf[:sniffLen])
587+
atEOF := errors.Is(er, io.EOF)
588+
n += int64(nr)
601589

602-
if w.needsSniff() {
603-
n0, err := io.Copy(writerOnly{w}, io.LimitReader(src, sniffLen))
604-
n += n0
605-
if err != nil {
606-
return n, err
590+
if nr > 0 {
591+
// Write the small amount read normally.
592+
nw, ew := w.Write(buf[:nr])
593+
if ew != nil {
594+
err = ew
595+
} else if nr != nw {
596+
err = io.ErrShortWrite
607597
}
608598
}
599+
if err == nil && er != nil && !atEOF {
600+
err = er
601+
}
602+
603+
// Do not send StatusOK in the error case where nothing has been written.
604+
if err == nil && !w.wroteHeader {
605+
w.WriteHeader(StatusOK) // nr == 0, no error (or EOF)
606+
}
607+
608+
if err != nil || atEOF {
609+
return n, err
610+
}
609611

610612
w.w.Flush() // get rid of any previous writes
611613
w.cw.flush() // make sure Header is written; flush data to rwc

0 commit comments

Comments
 (0)