Skip to content

Commit 8cf0cbf

Browse files
committed
Added http2 support to yab
Remove old comments Fix comment stating default http2 behavior in options Fixed test TimedOutUsingHTTP2Transport Fix comments Check if http2 flag is enabled when sending Thrift encoding Fix cli http2 option comment; Move test cases for http1/2 to method Reverted check for http2 flag in cli; Fix creation of http1 server
1 parent b3cacff commit 8cf0cbf

5 files changed

Lines changed: 226 additions & 61 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/yarpc/yab
22

3-
go 1.23
3+
go 1.23.0
44

55
toolchain go1.24.0
66

options.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ type TransportOptions struct {
9595
HTTPMethod string `long:"http-method" description:"The HTTP method to use"`
9696
GRPCMaxResponseSize int `long:"grpc-max-response-size" description:"Maximum response size for gRPC requests. Default value is 4MB"`
9797
ForceJaegerSample bool `long:"force-jaeger-sample" description:"Force all requests to be sampled for Jaeger tracing (use with --jaeger)"`
98+
99+
// Enables HTTP2 transport
100+
UseHTTP2 bool `long:"http2" description:"Enable HTTP/2 for HTTP transport"`
101+
98102
// This is a hack to work around go-flags not allowing disabling flags:
99103
// https://github.com/jessevdk/go-flags/issues/191
100104
// Do not specify this value in a defaults.ini file as it is not possible

transport.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ func getTransport(opts TransportOptions, resolved resolvedProtocolEncoding, trac
185185
Encoding: resolved.enc.String(),
186186
URLs: opts.Peers,
187187
Tracer: tracer,
188+
UseHTTP2: opts.UseHTTP2,
188189
}
189190
return transport.NewHTTP(hopts)
190191
}

transport/http.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@ package transport
2222

2323
import (
2424
"bytes"
25+
"crypto/tls"
2526
"errors"
2627
"fmt"
2728
"io/ioutil"
2829
"math/rand"
30+
"net"
2931
"net/http"
3032
"strconv"
3133
"time"
3234

35+
"golang.org/x/net/http2"
36+
3337
"github.com/opentracing/opentracing-go"
3438
"golang.org/x/net/context"
3539
)
@@ -51,6 +55,9 @@ type HTTPOptions struct {
5155
ShardKey string
5256
Encoding string
5357
Tracer opentracing.Tracer
58+
59+
// HTTP/2 specific options
60+
UseHTTP2 bool
5461
}
5562

5663
var (
@@ -70,11 +77,25 @@ func NewHTTP(opts HTTPOptions) (Transport, error) {
7077
opts.Method = "POST"
7178
}
7279

80+
var transport http.RoundTripper
81+
82+
if opts.UseHTTP2 {
83+
transport = &http2.Transport{
84+
AllowHTTP: true,
85+
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
86+
var d net.Dialer
87+
return d.DialContext(ctx, network, addr)
88+
},
89+
}
90+
} else {
91+
transport = &http.Transport{}
92+
}
93+
7394
return &httpTransport{
7495
opts: opts,
7596
// Use independent HTTP clients for each transport.
7697
client: &http.Client{
77-
Transport: &http.Transport{},
98+
Transport: transport,
7899
},
79100
tracer: opts.Tracer,
80101
}, nil

transport/http_test.go

Lines changed: 198 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,28 @@ package transport
2222

2323
import (
2424
"io"
25-
"io/ioutil"
2625
"net/http"
2726
"net/http/httptest"
2827
"net/url"
2928
"strconv"
3029
"testing"
3130
"time"
3231

32+
"golang.org/x/net/http2"
33+
"golang.org/x/net/http2/h2c"
34+
3335
"golang.org/x/net/context"
3436

3537
"github.com/stretchr/testify/assert"
3638
"github.com/stretchr/testify/require"
3739
)
3840

41+
var (
42+
defaultIdleConnTimeout = 15 * time.Minute
43+
serverTimeout = 20 * time.Millisecond
44+
clientTimeout = 10 * time.Millisecond
45+
)
46+
3947
func TestHTTPConstructor(t *testing.T) {
4048
tests := []struct {
4149
opts HTTPOptions
@@ -71,73 +79,19 @@ func TestHTTPConstructor(t *testing.T) {
7179
}
7280

7381
func TestHTTPCall(t *testing.T) {
74-
timeoutCtx, _ := context.WithTimeout(context.Background(), 3*time.Second)
75-
immediateTimeout, _ := context.WithTimeout(context.Background(), time.Nanosecond)
76-
77-
tests := []struct {
78-
msg string
79-
ctxOverride context.Context
80-
hook string
81-
method string
82-
errMsg string
83-
ttlMin time.Duration
84-
ttlMax time.Duration
85-
wantCode int
86-
wantBody []byte // If nil, uses the request body
87-
}{
88-
{
89-
msg: "ok",
90-
ttlMin: time.Second,
91-
ttlMax: time.Second,
92-
wantCode: http.StatusOK,
93-
},
94-
{
95-
msg: "3 second timeout",
96-
ctxOverride: timeoutCtx,
97-
ttlMin: 3*time.Second - 100*time.Millisecond,
98-
ttlMax: 3 * time.Second,
99-
wantCode: http.StatusOK,
100-
},
101-
{
102-
msg: "timed out",
103-
ctxOverride: immediateTimeout,
104-
errMsg: context.DeadlineExceeded.Error(),
105-
},
82+
tests := getCommonHttpTestCases()
83+
tests = append(tests, []TestBody{
10684
{
10785
msg: "connection closed before data",
10886
hook: "kill_conn",
10987
errMsg: "EOF",
11088
},
111-
{
112-
msg: "bad request response",
113-
hook: "bad_req",
114-
errMsg: "non-success response code: 400, body: bad request",
115-
},
11689
{
11790
msg: "connection closed after data",
11891
hook: "flush_and_kill",
11992
errMsg: "unexpected EOF",
12093
},
121-
{
122-
msg: "no content",
123-
hook: "no_content",
124-
wantCode: http.StatusNoContent,
125-
wantBody: []byte{},
126-
},
127-
{
128-
msg: "default method to POST",
129-
hook: "method",
130-
wantCode: http.StatusOK,
131-
wantBody: []byte("POST"),
132-
},
133-
{
134-
msg: "override method to GET",
135-
method: "GET",
136-
hook: "method",
137-
wantCode: http.StatusOK,
138-
wantBody: []byte("GET"),
139-
},
140-
}
94+
}...)
14195

14296
lastReq := struct {
14397
url *url.URL
@@ -149,7 +103,7 @@ func TestHTTPCall(t *testing.T) {
149103
var err error
150104
lastReq.url = r.URL
151105
lastReq.headers = r.Header
152-
lastReq.body, err = ioutil.ReadAll(r.Body)
106+
lastReq.body, err = io.ReadAll(r.Body)
153107
require.NoError(t, err, "Failed to read body from request")
154108

155109
// Test hooks to change the request behaviour.
@@ -169,6 +123,9 @@ func TestHTTPCall(t *testing.T) {
169123
case "server_err":
170124
w.WriteHeader(http.StatusInternalServerError)
171125
return
126+
case "timeout_hook":
127+
time.Sleep(serverTimeout)
128+
return
172129
case "flush_and_kill":
173130
io.WriteString(w, "some data")
174131
flusher := w.(http.Flusher)
@@ -254,3 +211,185 @@ func TestHTTPCall(t *testing.T) {
254211
})
255212
}
256213
}
214+
215+
func TestHTTP2Call(t *testing.T) {
216+
tests := getCommonHttpTestCases()
217+
218+
lastReq := struct {
219+
url *url.URL
220+
headers http.Header
221+
body []byte
222+
}{}
223+
224+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
225+
var err error
226+
lastReq.url = r.URL
227+
lastReq.headers = r.Header
228+
lastReq.body, err = io.ReadAll(r.Body)
229+
require.NoError(t, err, "Failed to read body from request")
230+
231+
// Test hooks to change the request behaviour.
232+
switch f := r.Header.Get("hook"); f {
233+
case "no_content":
234+
w.Header().Set("Custom-Header", "ok")
235+
w.WriteHeader(http.StatusNoContent)
236+
return
237+
case "method":
238+
w.Header().Set("Custom-Header", "ok")
239+
io.WriteString(w, r.Method)
240+
return
241+
case "bad_req":
242+
w.WriteHeader(http.StatusBadRequest)
243+
io.WriteString(w, "bad request")
244+
return
245+
case "server_err":
246+
w.WriteHeader(http.StatusInternalServerError)
247+
return
248+
case "timeout_hook":
249+
time.Sleep(serverTimeout)
250+
return
251+
}
252+
253+
w.Header().Set("Custom-Header", "ok")
254+
io.WriteString(w, "ok")
255+
})
256+
257+
h2s := &http2.Server{
258+
IdleTimeout: defaultIdleConnTimeout,
259+
}
260+
261+
svr := httptest.NewServer(h2c.NewHandler(handler, h2s))
262+
defer svr.Close()
263+
264+
for _, tt := range tests {
265+
t.Run(tt.msg, func(t *testing.T) {
266+
transport, err := NewHTTP(HTTPOptions{
267+
Method: tt.method,
268+
URLs: []string{svr.URL + "/rpc"},
269+
SourceService: "source",
270+
TargetService: "target",
271+
ShardKey: "sk",
272+
RoutingKey: "rk",
273+
RoutingDelegate: "rd",
274+
Encoding: "raw",
275+
UseHTTP2: true,
276+
})
277+
require.NoError(t, err, "Failed to create HTTP transport")
278+
279+
ctx := context.Background()
280+
if tt.ctxOverride != nil {
281+
ctx = tt.ctxOverride
282+
}
283+
284+
r := &Request{Method: "method", Body: []byte{1, 2, 3}}
285+
286+
r.TransportHeaders = map[string]string{"hook": tt.hook}
287+
r.Headers = map[string]string{"headerkey": "headervalue"}
288+
got, err := transport.Call(ctx, r)
289+
if tt.errMsg != "" {
290+
if assert.Error(t, err, "Call should fail") {
291+
assert.Contains(t, err.Error(), tt.errMsg, "Unexpected error")
292+
}
293+
return
294+
}
295+
296+
if !assert.NoError(t, err, "Call shouldn't fail") {
297+
return
298+
}
299+
300+
wantBody := tt.wantBody
301+
if wantBody == nil {
302+
wantBody = []byte("ok")
303+
}
304+
if !assert.Equal(t, wantBody, got.Body, "Response body mismatch") {
305+
return
306+
}
307+
308+
assert.Equal(t, "/rpc", lastReq.url.Path, "Path mismatch")
309+
assert.Equal(t, "target", lastReq.headers.Get("Rpc-Service"), "Service header mismatch")
310+
assert.Equal(t, "source", lastReq.headers.Get("Rpc-Caller"), "Caller header mismatch")
311+
assert.Equal(t, "sk", lastReq.headers.Get("Rpc-Shard-Key"), "Shard key header mismatch")
312+
assert.Equal(t, "rk", lastReq.headers.Get("Rpc-Routing-Key"), "Routing key header mismatch")
313+
assert.Equal(t, "rd", lastReq.headers.Get("Rpc-Routing-Delegate"), "Routing delegate header mismatch")
314+
assert.Equal(t, r.Method, lastReq.headers.Get("Rpc-Procedure"), "Method header mismatch")
315+
assert.Equal(t, "raw", lastReq.headers.Get("Rpc-Encoding"), "Encoding header mismatch")
316+
assert.Equal(t, "headervalue", lastReq.headers.Get("Rpc-Header-Headerkey"), "Application header is sent with prefix")
317+
318+
if tt.ttlMin != 0 && tt.ttlMax != 0 {
319+
ttlMS, err := strconv.Atoi(lastReq.headers.Get("Context-TTL-MS"))
320+
if assert.NoError(t, err, "Failed to parse TTLms header: %v", lastReq.headers.Get("YARPC-TTLms")) {
321+
gotTTL := time.Duration(ttlMS) * time.Millisecond
322+
assert.True(t, gotTTL >= tt.ttlMin && gotTTL <= tt.ttlMax,
323+
"Got TTL %v out of range [%v,%v]", gotTTL, tt.ttlMin, tt.ttlMax)
324+
}
325+
}
326+
327+
assert.Equal(t, "ok", got.Headers["Custom-Header"], "Header mismatch")
328+
assert.Equal(t, tt.wantCode, got.TransportFields["statusCode"], "Status code mismatch")
329+
assert.Equal(t, lastReq.body, r.Body, "Body mismatch")
330+
})
331+
}
332+
}
333+
334+
type TestBody struct {
335+
msg string
336+
ctxOverride context.Context
337+
hook string
338+
method string
339+
errMsg string
340+
ttlMin time.Duration
341+
ttlMax time.Duration
342+
wantCode int
343+
wantBody []byte // If nil, uses the request body
344+
}
345+
346+
func getCommonHttpTestCases() []TestBody {
347+
timeoutCtx, _ := context.WithTimeout(context.Background(), 3*time.Second)
348+
immediateTimeout, _ := context.WithTimeout(context.Background(), clientTimeout)
349+
350+
return []TestBody{
351+
{
352+
msg: "ok",
353+
ttlMin: time.Second,
354+
ttlMax: time.Second,
355+
wantCode: http.StatusOK,
356+
},
357+
{
358+
msg: "3 second timeout",
359+
ctxOverride: timeoutCtx,
360+
ttlMin: 3*time.Second - 100*time.Millisecond,
361+
ttlMax: 3 * time.Second,
362+
wantCode: http.StatusOK,
363+
},
364+
{
365+
msg: "timed out",
366+
ctxOverride: immediateTimeout,
367+
hook: "timeout_hook",
368+
errMsg: context.DeadlineExceeded.Error(),
369+
},
370+
{
371+
msg: "bad request response",
372+
hook: "bad_req",
373+
errMsg: "non-success response code: 400, body: bad request",
374+
},
375+
{
376+
msg: "no content",
377+
hook: "no_content",
378+
wantCode: http.StatusNoContent,
379+
wantBody: []byte{},
380+
},
381+
{
382+
msg: "default method to POST",
383+
hook: "method",
384+
wantCode: http.StatusOK,
385+
wantBody: []byte("POST"),
386+
},
387+
{
388+
msg: "override method to GET",
389+
method: "GET",
390+
hook: "method",
391+
wantCode: http.StatusOK,
392+
wantBody: []byte("GET"),
393+
},
394+
}
395+
}

0 commit comments

Comments
 (0)