Skip to content
Snippets Groups Projects
Commit 4cabb4df authored by ale's avatar ale
Browse files

Add optional support for response gzip compression

The clients already support it (via the default net/http.Transport),
and it can be enabled with a configuration parameter. The default is
still currently disabled, though there is plan to flip it later to
enabled-by-default if it proves to be working as expected in
production.

We also introduce, via a slight refactoring of the Serve() code, a
test of the top-level http.Handler (with the full middleware stack).
parent f1e753e3
No related branches found
No related tags found
No related merge requests found
......@@ -4,6 +4,7 @@ go 1.11
require (
contrib.go.opencensus.io/exporter/zipkin v0.1.2
github.com/NYTimes/gziphandler v1.1.1
github.com/amoghe/go-crypt v0.0.0-20191109212615-b2ff80594b7f
github.com/bbrks/wrap/v2 v2.5.0
github.com/cenkalti/backoff/v4 v4.1.1
......
......@@ -5,6 +5,8 @@ contrib.go.opencensus.io/exporter/zipkin v0.1.2/go.mod h1:mP5xM3rrgOjpn79MM8fZbj
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
......@@ -30,6 +32,7 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
......@@ -177,6 +180,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/theckman/go-flock v0.8.1 h1:kTixuOsFBOtGYSTLRLWK6GOs1hk/8OD11sR1pDd0dl4=
github.com/theckman/go-flock v0.8.1/go.mod h1:kjuth3y9VJ2aNlkNEO99G/8lp9fMIKaGyBmh84IBheM=
......@@ -295,6 +299,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
package serverutil
import (
"compress/gzip"
"context"
"crypto/tls"
"fmt"
......@@ -16,12 +17,25 @@ import (
"time"
"git.autistici.org/ai3/go-common/tracing"
"github.com/NYTimes/gziphandler"
"github.com/coreos/go-systemd/v22/daemon"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var gracefulShutdownTimeout = 3 * time.Second
var (
gracefulShutdownTimeout = 3 * time.Second
gzipLevel = gzip.BestSpeed
gzipMinSize = 1300
gzipContentTypes = []string{
"application/json",
"application/javascript",
"text/html",
"text/plain",
"text/css",
}
)
// ServerConfig stores common HTTP/HTTPS server configuration parameters.
type ServerConfig struct {
......@@ -29,20 +43,23 @@ type ServerConfig struct {
MaxInflightRequests int `yaml:"max_inflight_requests"`
RequestTimeoutSecs int `yaml:"request_timeout"`
TrustedForwarders []string `yaml:"trusted_forwarders"`
// TODO: switch do disable_compression (flip default) later.
EnableCompression bool `yaml:"enable_compression"`
}
func (config *ServerConfig) buildHTTPServer(h http.Handler) (*http.Server, error) {
func (config *ServerConfig) buildHTTPHandler(h http.Handler) (http.Handler, *tls.Config, error) {
var tlsConfig *tls.Config
var err error
if config != nil {
if config.TLS != nil {
tlsConfig, err = config.TLS.TLSConfig()
if err != nil {
return nil, err
return nil, nil, err
}
h, err = config.TLS.TLSAuthWrapper(h)
if err != nil {
return nil, err
return nil, nil, err
}
}
......@@ -51,7 +68,7 @@ func (config *ServerConfig) buildHTTPServer(h http.Handler) (*http.Server, error
if len(config.TrustedForwarders) > 0 {
h, err = newProxyHeaders(h, config.TrustedForwarders)
if err != nil {
return nil, err
return nil, nil, err
}
}
......@@ -68,15 +85,23 @@ func (config *ServerConfig) buildHTTPServer(h http.Handler) (*http.Server, error
}
}
// These are not meant to be external-facing servers, so we
// can be generous with the timeouts to keep the number of
// reconnections low.
return &http.Server{
Handler: addDefaultHandlers(h),
ReadHeaderTimeout: 30 * time.Second,
IdleTimeout: 600 * time.Second,
TLSConfig: tlsConfig,
}, nil
// Add all the default handlers (health, monitoring, etc).
h = addDefaultHandlers(h)
// Optionally enable compression.
if config.EnableCompression {
gzwrap, err := gziphandler.GzipHandlerWithOpts(
gziphandler.CompressionLevel(gzipLevel),
gziphandler.MinSize(gzipMinSize),
gziphandler.ContentTypes(gzipContentTypes),
)
if err != nil {
return nil, nil, err
}
h = gzwrap(h)
}
return h, tlsConfig, nil
}
// Serve HTTP(S) content on the specified address. If config.TLS is
......@@ -90,12 +115,22 @@ func Serve(h http.Handler, config *ServerConfig, addr string) error {
// debugging endpoints).
h = tracing.WrapHandler(h, guessEndpointName(addr))
// Create the HTTP server.
srv, err := config.buildHTTPServer(h)
// Create the top-level HTTP handler with all our additions.
hh, tlsConfig, err := config.buildHTTPHandler(h)
if err != nil {
return err
}
// These are not meant to be external-facing servers, so we
// can be generous with the timeouts to keep the number of
// reconnections low.
srv := &http.Server{
Handler: hh,
ReadHeaderTimeout: 30 * time.Second,
IdleTimeout: 600 * time.Second,
TLSConfig: tlsConfig,
}
// Create the net.Listener first, so we can detect
// initialization-time errors safely.
l, err := net.Listen("tcp", addr)
......
package serverutil
import (
"context"
"crypto/rand"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.autistici.org/ai3/go-common/clientutil"
)
type TestRequest struct {
Data []string `json:"data"`
}
type TestObject struct {
Name string `json:"name"`
Host string `json:"host"`
Timestamp time.Time `json:"timestamp"`
PubKey []byte `json:"pubkey"`
}
type TestResponse struct {
Objects []*TestObject `json:"objects"`
}
func fastRandomBytes(n int) []byte {
b := make([]byte, n)
rand.Read(b) // nolint: errcheck
return b
}
func makeTestHandler() http.HandlerFunc {
// Generate a large-ish random response.
var resp TestResponse
now := time.Now()
n := 256
resp.Objects = make([]*TestObject, 0, n)
for i := 0; i < n; i++ {
resp.Objects = append(resp.Objects, &TestObject{
Name: fmt.Sprintf("test-object-%06d", i+1),
Host: "host-452-ff-bb",
Timestamp: now,
PubKey: fastRandomBytes(256),
})
}
return func(w http.ResponseWriter, httpReq *http.Request) {
var req TestRequest
if !DecodeJSONRequest(w, httpReq, &req) {
return
}
EncodeJSONResponse(w, &resp)
}
}
const apiPath = "/api/v1/random"
func makeTestRequest() *TestRequest {
var req TestRequest
n := 256
req.Data = make([]string, 0, n)
for i := 0; i < n; i++ {
req.Data = append(req.Data, fmt.Sprintf("data-item-%06d", i))
}
return &req
}
func makeSingleRequest(backend clientutil.Backend, req *TestRequest) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var resp TestResponse
return backend.Call(ctx, "", apiPath, &req, &resp)
}
func runHTTPTest(t *testing.T, config *ServerConfig) {
mux := http.NewServeMux()
mux.HandleFunc(apiPath, makeTestHandler())
h, _, err := config.buildHTTPHandler(mux)
if err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(h)
defer srv.Close()
backend, err := clientutil.NewBackend(&clientutil.BackendConfig{
URL: srv.URL,
})
if err != nil {
t.Fatalf("NewBackend() error: %v", err)
}
defer backend.Close()
if err := makeSingleRequest(backend, makeTestRequest()); err != nil {
t.Fatal(err)
}
}
func TestHTTP(t *testing.T) {
runHTTPTest(t, &ServerConfig{})
}
func TestHTTP_Compression(t *testing.T) {
runHTTPTest(t, &ServerConfig{
EnableCompression: true,
})
}
func BenchmarkLoad(b *testing.B) {
mux := http.NewServeMux()
mux.HandleFunc(apiPath, makeTestHandler())
config := &ServerConfig{
EnableCompression: true,
}
h, _, _ := config.buildHTTPHandler(mux)
srv := httptest.NewServer(h)
defer srv.Close()
backend, err := clientutil.NewBackend(&clientutil.BackendConfig{
URL: srv.URL,
})
if err != nil {
b.Fatalf("NewBackend() error: %v", err)
}
defer backend.Close()
req := makeTestRequest()
// Run clients.
b.SetParallelism(100)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
makeSingleRequest(backend, req) // nolint: errcheck
}
})
}
......@@ -30,19 +30,15 @@ func DecodeJSONRequest(w http.ResponseWriter, r *http.Request, obj interface{})
// EncodeJSONResponse writes an application/json response to w.
func EncodeJSONResponse(w http.ResponseWriter, obj interface{}) {
data, err := json.Marshal(obj)
if err != nil {
log.Printf("JSON serialization error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Expires", "-1")
w.Header().Set("X-Content-Type-Options", "nosniff")
if _, err = w.Write(data); err != nil {
log.Printf("error writing response: %v", err)
err := json.NewEncoder(w).Encode(obj)
if err != nil {
log.Printf("error writing JSON response: %v", err)
// Too late to return an error to the client now.
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment