handler: fix sendfile to be non-blocking

Having done a bit more research on exactly how to use sendfile()
effectively with Go's own event loop, we can now leave the HTTP socket
in non-blocking mode and correctly integrate with the event loop to wait
for the socket to be writeable once more.
This commit is contained in:
Laurence Withers 2019-05-28 12:57:59 +01:00
parent 97a823d748
commit c2d7bdaa71
1 changed files with 29 additions and 22 deletions

View File

@ -1,6 +1,7 @@
package htpack package htpack
import ( import (
"fmt"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -195,28 +196,18 @@ func (h *Handler) sendfile(w http.ResponseWriter, data *packed.FileData) {
if err == nil { if err == nil {
err = buf.Flush() err = buf.Flush()
} }
if err == nil {
// Since we're bypassing Read / Write, there is no integration
// with Go's epoll-driven event handling for this file
// descriptor. We'll therefore get EAGAIN behaviour rather
// than blocking for Sendfile(). Work around this by setting
// the file descriptor to blocking mode; since this function
// now guarantees (via defer tcp.Close()) that the connection
// will be closed and not be passed back to Go's own event
// loop, this is safe to do.
rawsock.Control(func(outfd uintptr) {
err = syscall.SetNonblock(int(outfd), false)
})
}
if err != nil { if err != nil {
// error only returned if the underlying connection is broken, // error only returned if the underlying connection is broken,
// so there's no point calling sendfile // so there's no point calling sendfile
return return
} }
var breakErr error
off := int64(data.Offset) off := int64(data.Offset)
remain := data.Length remain := data.Length
for remain > 0 {
for breakErr == nil && remain > 0 {
// sendfile(2) can send a maximum of 1GiB
var amt int var amt int
if remain > (1 << 30) { if remain > (1 << 30) {
amt = (1 << 30) amt = (1 << 30)
@ -224,15 +215,31 @@ func (h *Handler) sendfile(w http.ResponseWriter, data *packed.FileData) {
amt = int(remain) amt = int(remain)
} }
// TODO: outer error handling // behaviour of control function:
// · some bytes written: sets written > 0, returns true (breaks
rawsock.Control(func(outfd uintptr) { // out of loop on first write)
amt, err = unix.Sendfile(int(outfd), int(h.f.Fd()), &off, amt) // · EAGAIN: returns false (causes Write() to loop until
// success or permanent failure)
// · other error: sets breakErr
var written int
rawsock.Write(func(outfd uintptr) bool {
fmt.Fprintf(os.Stderr, "[DEBUG] sendfile(%d, %d, %d, %d) = ",
outfd, h.f.Fd(), off, amt)
written, err = unix.Sendfile(int(outfd), int(h.f.Fd()), &off, amt)
fmt.Fprintf(os.Stderr, "(%d, %v); off now %d\n", written, err, off)
switch err {
case nil:
return true
case syscall.EAGAIN:
return false
default:
breakErr = err
return true
}
}) })
remain -= uint64(amt)
if err != nil { // we may have had a partial write, or file may have been > 1GiB
return remain -= uint64(written)
}
} }
} }