Skip to content

Commit

Permalink
net/http/httputil: add hook for managing io.Copy buffers per request
Browse files Browse the repository at this point in the history
Adds ReverseProxy.BufferPool for users with sensitive allocation
requirements. Permits avoiding 32 KB of io.Copy garbage per request.

Fixes golang#10277

Change-Id: I5dfd58fa70a363ead4be56405e507df90d871719
Reviewed-on: https://go-review.googlesource.com/9399
Reviewed-by: Keith Randall <[email protected]>
Run-TryBot: Brad Fitzpatrick <[email protected]>
TryBot-Result: Gobot Gobot <[email protected]>
  • Loading branch information
bradfitz committed Oct 28, 2015
1 parent 31430bd commit 492a62e
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 1 deletion.
21 changes: 20 additions & 1 deletion src/net/http/httputil/reverseproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ type ReverseProxy struct {
// If nil, logging goes to os.Stderr via the log package's
// standard logger.
ErrorLog *log.Logger

// BufferPool optionally specifies a buffer pool to
// get byte slices for use by io.CopyBuffer when
// copying HTTP response bodies.
BufferPool BufferPool
}

// A BufferPool is an interface for getting and returning temporary
// byte slices for use by io.CopyBuffer.
type BufferPool interface {
Get() []byte
Put([]byte)
}

func singleJoiningSlash(a, b string) string {
Expand Down Expand Up @@ -245,7 +257,14 @@ func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
}
}

io.Copy(dst, src)
var buf []byte
if p.BufferPool != nil {
buf = p.BufferPool.Get()
}
io.CopyBuffer(dst, src, buf)
if p.BufferPool != nil {
p.BufferPool.Put(buf)
}
}

func (p *ReverseProxy) logf(format string, args ...interface{}) {
Expand Down
68 changes: 68 additions & 0 deletions src/net/http/httputil/reverseproxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ package httputil

import (
"bufio"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strconv"
"strings"
"sync"
"testing"
"time"
)
Expand Down Expand Up @@ -316,3 +319,68 @@ func TestNilBody(t *testing.T) {
t.Errorf("Got %q; want %q", slurp, "hi")
}
}

type bufferPool struct {
get func() []byte
put func([]byte)
}

func (bp bufferPool) Get() []byte { return bp.get() }
func (bp bufferPool) Put(v []byte) { bp.put(v) }

func TestReverseProxyGetPutBuffer(t *testing.T) {
const msg = "hi"
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, msg)
}))
defer backend.Close()

backendURL, err := url.Parse(backend.URL)
if err != nil {
t.Fatal(err)
}

var (
mu sync.Mutex
log []string
)
addLog := func(event string) {
mu.Lock()
defer mu.Unlock()
log = append(log, event)
}
rp := NewSingleHostReverseProxy(backendURL)
const size = 1234
rp.BufferPool = bufferPool{
get: func() []byte {
addLog("getBuf")
return make([]byte, size)
},
put: func(p []byte) {
addLog("putBuf-" + strconv.Itoa(len(p)))
},
}
frontend := httptest.NewServer(rp)
defer frontend.Close()

req, _ := http.NewRequest("GET", frontend.URL, nil)
req.Close = true
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("Get: %v", err)
}
slurp, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Fatalf("reading body: %v", err)
}
if string(slurp) != msg {
t.Errorf("msg = %q; want %q", slurp, msg)
}
wantLog := []string{"getBuf", "putBuf-" + strconv.Itoa(size)}
mu.Lock()
defer mu.Unlock()
if !reflect.DeepEqual(log, wantLog) {
t.Errorf("Log events = %q; want %q", log, wantLog)
}
}

0 comments on commit 492a62e

Please sign in to comment.