Skip to content

Commit d27440d

Browse files
mikeraimondidfawley
authored andcommitted
client: set TCP_USER_TIMEOUT socket option for linux (#2307)
Implements proposal A18 (https://github.com/grpc/proposal/blob/master/A18-tcp-user-timeout.md). gRPC Core issue for reference: grpc/grpc#15889
1 parent 1b89e78 commit d27440d

File tree

4 files changed

+141
-11
lines changed

4 files changed

+141
-11
lines changed

internal/syscall/syscall_linux.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
package syscall
2424

2525
import (
26+
"fmt"
27+
"net"
2628
"syscall"
29+
"time"
2730

2831
"golang.org/x/sys/unix"
2932
"google.golang.org/grpc/grpclog"
@@ -65,3 +68,47 @@ func CPUTimeDiff(first *Rusage, latest *Rusage) (float64, float64) {
6568

6669
return uTimeElapsed, sTimeElapsed
6770
}
71+
72+
// SetTCPUserTimeout sets the TCP user timeout on a connection's socket
73+
func SetTCPUserTimeout(conn net.Conn, timeout time.Duration) error {
74+
tcpconn, ok := conn.(*net.TCPConn)
75+
if !ok {
76+
// not a TCP connection. exit early
77+
return nil
78+
}
79+
rawConn, err := tcpconn.SyscallConn()
80+
if err != nil {
81+
return fmt.Errorf("error getting raw connection: %v", err)
82+
}
83+
err = rawConn.Control(func(fd uintptr) {
84+
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_USER_TIMEOUT, int(timeout/time.Millisecond))
85+
})
86+
if err != nil {
87+
return fmt.Errorf("error setting option on socket: %v", err)
88+
}
89+
90+
return nil
91+
}
92+
93+
// GetTCPUserTimeout gets the TCP user timeout on a connection's socket
94+
func GetTCPUserTimeout(conn net.Conn) (opt int, err error) {
95+
tcpconn, ok := conn.(*net.TCPConn)
96+
if !ok {
97+
err = fmt.Errorf("conn is not *net.TCPConn. got %T", conn)
98+
return
99+
}
100+
rawConn, err := tcpconn.SyscallConn()
101+
if err != nil {
102+
err = fmt.Errorf("error getting raw connection: %v", err)
103+
return
104+
}
105+
err = rawConn.Control(func(fd uintptr) {
106+
opt, err = syscall.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_USER_TIMEOUT)
107+
})
108+
if err != nil {
109+
err = fmt.Errorf("error getting option on socket: %v", err)
110+
return
111+
}
112+
113+
return
114+
}

internal/syscall/syscall_nonlinux.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020

2121
package syscall
2222

23-
import "google.golang.org/grpc/grpclog"
23+
import (
24+
"net"
25+
"time"
26+
27+
"google.golang.org/grpc/grpclog"
28+
)
2429

2530
func init() {
2631
grpclog.Info("CPU time info is unavailable on non-linux or appengine environment.")
@@ -45,3 +50,14 @@ func GetRusage() (rusage *Rusage) {
4550
func CPUTimeDiff(first *Rusage, latest *Rusage) (float64, float64) {
4651
return 0, 0
4752
}
53+
54+
// SetTCPUserTimeout is a no-op function under non-linux or appengine environments
55+
func SetTCPUserTimeout(conn net.Conn, timeout time.Duration) error {
56+
return nil
57+
}
58+
59+
// GetTCPUserTimeout is a no-op function under non-linux or appengine environments
60+
// a negative return value indicates the operation is not supported
61+
func GetTCPUserTimeout(conn net.Conn) (int, error) {
62+
return -1, nil
63+
}

internal/transport/http2_client.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"google.golang.org/grpc/codes"
3737
"google.golang.org/grpc/credentials"
3838
"google.golang.org/grpc/internal/channelz"
39+
"google.golang.org/grpc/internal/syscall"
3940
"google.golang.org/grpc/keepalive"
4041
"google.golang.org/grpc/metadata"
4142
"google.golang.org/grpc/peer"
@@ -166,6 +167,21 @@ func newHTTP2Client(connectCtx, ctx context.Context, addr TargetInfo, opts Conne
166167
conn.Close()
167168
}
168169
}(conn)
170+
kp := opts.KeepaliveParams
171+
// Validate keepalive parameters.
172+
if kp.Time == 0 {
173+
kp.Time = defaultClientKeepaliveTime
174+
}
175+
if kp.Timeout == 0 {
176+
kp.Timeout = defaultClientKeepaliveTimeout
177+
}
178+
keepaliveEnabled := false
179+
if kp.Time != infinity {
180+
if err = syscall.SetTCPUserTimeout(conn, kp.Timeout); err != nil {
181+
return nil, connectionErrorf(false, err, "transport: failed to set TCP_USER_TIMEOUT: %v", err)
182+
}
183+
keepaliveEnabled = true
184+
}
169185
var (
170186
isSecure bool
171187
authInfo credentials.AuthInfo
@@ -189,14 +205,6 @@ func newHTTP2Client(connectCtx, ctx context.Context, addr TargetInfo, opts Conne
189205
}
190206
isSecure = true
191207
}
192-
kp := opts.KeepaliveParams
193-
// Validate keepalive parameters.
194-
if kp.Time == 0 {
195-
kp.Time = defaultClientKeepaliveTime
196-
}
197-
if kp.Timeout == 0 {
198-
kp.Timeout = defaultClientKeepaliveTimeout
199-
}
200208
dynamicWindow := true
201209
icwz := int32(initialWindowSize)
202210
if opts.InitialConnWindowSize >= defaultWindowSize {
@@ -240,6 +248,7 @@ func newHTTP2Client(connectCtx, ctx context.Context, addr TargetInfo, opts Conne
240248
czData: new(channelzData),
241249
onGoAway: onGoAway,
242250
onClose: onClose,
251+
keepaliveEnabled: keepaliveEnabled,
243252
}
244253
t.controlBuf = newControlBuffer(t.ctxDone)
245254
if opts.InitialWindowSize >= defaultWindowSize {
@@ -268,8 +277,7 @@ func newHTTP2Client(connectCtx, ctx context.Context, addr TargetInfo, opts Conne
268277
if channelz.IsOn() {
269278
t.channelzID = channelz.RegisterNormalSocket(t, opts.ChannelzParentID, fmt.Sprintf("%s -> %s", t.localAddr, t.remoteAddr))
270279
}
271-
if t.kp.Time != infinity {
272-
t.keepaliveEnabled = true
280+
if t.keepaliveEnabled {
273281
go t.keepalive()
274282
}
275283
// Start the reader goroutine for incoming message. Each transport has

internal/transport/transport_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"golang.org/x/net/http2/hpack"
4242
"google.golang.org/grpc/codes"
4343
"google.golang.org/grpc/internal/leakcheck"
44+
"google.golang.org/grpc/internal/syscall"
4445
"google.golang.org/grpc/keepalive"
4546
"google.golang.org/grpc/status"
4647
)
@@ -2317,3 +2318,61 @@ func TestHeaderTblSize(t *testing.T) {
23172318
t.Fatalf("expected len(limits) = 2 within 10s, got != 2")
23182319
}
23192320
}
2321+
2322+
// TestTCPUserTimeout tests that the TCP_USER_TIMEOUT socket option is set to the
2323+
// keepalive timeout, as detailed in proposal A18
2324+
func TestTCPUserTimeout(t *testing.T) {
2325+
tests := []struct {
2326+
time time.Duration
2327+
timeout time.Duration
2328+
}{
2329+
{
2330+
10 * time.Second,
2331+
10 * time.Second,
2332+
},
2333+
{
2334+
0,
2335+
0,
2336+
},
2337+
}
2338+
for _, tt := range tests {
2339+
server, client, cancel := setUpWithOptions(
2340+
t,
2341+
0,
2342+
&ServerConfig{
2343+
KeepaliveParams: keepalive.ServerParameters{
2344+
Time: tt.timeout,
2345+
Timeout: tt.timeout,
2346+
},
2347+
},
2348+
normal,
2349+
ConnectOptions{
2350+
KeepaliveParams: keepalive.ClientParameters{
2351+
Time: tt.time,
2352+
Timeout: tt.timeout,
2353+
},
2354+
},
2355+
)
2356+
defer cancel()
2357+
defer server.stop()
2358+
defer client.Close()
2359+
2360+
stream, err := client.NewStream(context.Background(), &CallHdr{})
2361+
if err != nil {
2362+
t.Fatalf("Client failed to create RPC request: %v", err)
2363+
}
2364+
client.closeStream(stream, io.EOF, true, http2.ErrCodeCancel, nil, nil, false)
2365+
2366+
opt, err := syscall.GetTCPUserTimeout(client.conn)
2367+
if err != nil {
2368+
t.Fatalf("GetTCPUserTimeout error: %v", err)
2369+
}
2370+
if opt < 0 {
2371+
t.Skipf("skipping test on unsupported environment")
2372+
}
2373+
if timeoutMS := int(tt.timeout / time.Millisecond); timeoutMS != opt {
2374+
t.Fatalf("wrong TCP_USER_TIMEOUT set on conn. expected %d. got %d",
2375+
timeoutMS, opt)
2376+
}
2377+
}
2378+
}

0 commit comments

Comments
 (0)