k8s.io/client-go@v0.22.2/rest/connection_test.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package rest 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "net" 24 "net/http" 25 "net/http/httptest" 26 "net/url" 27 "os" 28 "strconv" 29 "sync/atomic" 30 "testing" 31 "time" 32 33 "k8s.io/apimachinery/pkg/runtime/schema" 34 "k8s.io/apimachinery/pkg/runtime/serializer" 35 utilnet "k8s.io/apimachinery/pkg/util/net" 36 ) 37 38 type tcpLB struct { 39 t *testing.T 40 ln net.Listener 41 serverURL string 42 dials int32 43 } 44 45 func (lb *tcpLB) handleConnection(in net.Conn, stopCh chan struct{}) { 46 out, err := net.Dial("tcp", lb.serverURL) 47 if err != nil { 48 lb.t.Log(err) 49 return 50 } 51 go io.Copy(out, in) 52 go io.Copy(in, out) 53 <-stopCh 54 if err := out.Close(); err != nil { 55 lb.t.Fatalf("failed to close connection: %v", err) 56 } 57 } 58 59 func (lb *tcpLB) serve(stopCh chan struct{}) { 60 conn, err := lb.ln.Accept() 61 if err != nil { 62 lb.t.Fatalf("failed to accept: %v", err) 63 } 64 atomic.AddInt32(&lb.dials, 1) 65 go lb.handleConnection(conn, stopCh) 66 } 67 68 func newLB(t *testing.T, serverURL string) *tcpLB { 69 ln, err := net.Listen("tcp", "127.0.0.1:0") 70 if err != nil { 71 t.Fatalf("failed to bind: %v", err) 72 } 73 lb := tcpLB{ 74 serverURL: serverURL, 75 ln: ln, 76 t: t, 77 } 78 return &lb 79 } 80 81 func setEnv(key, value string) func() { 82 originalValue := os.Getenv(key) 83 os.Setenv(key, value) 84 return func() { 85 os.Setenv(key, originalValue) 86 } 87 } 88 89 const ( 90 readIdleTimeout int = 1 91 pingTimeout int = 1 92 ) 93 94 func TestReconnectBrokenTCP(t *testing.T) { 95 defer setEnv("HTTP2_READ_IDLE_TIMEOUT_SECONDS", strconv.Itoa(readIdleTimeout))() 96 defer setEnv("HTTP2_PING_TIMEOUT_SECONDS", strconv.Itoa(pingTimeout))() 97 defer setEnv("DISABLE_HTTP2", "")() 98 ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 99 fmt.Fprintf(w, "Hello, %s", r.Proto) 100 })) 101 ts.EnableHTTP2 = true 102 ts.StartTLS() 103 defer ts.Close() 104 105 u, err := url.Parse(ts.URL) 106 if err != nil { 107 t.Fatalf("failed to parse URL from %q: %v", ts.URL, err) 108 } 109 lb := newLB(t, u.Host) 110 defer lb.ln.Close() 111 stopCh := make(chan struct{}) 112 go lb.serve(stopCh) 113 transport, ok := ts.Client().Transport.(*http.Transport) 114 if !ok { 115 t.Fatalf("failed to assert *http.Transport") 116 } 117 config := &Config{ 118 Host: "https://" + lb.ln.Addr().String(), 119 Transport: utilnet.SetTransportDefaults(transport), 120 Timeout: 1 * time.Second, 121 // These fields are required to create a REST client. 122 ContentConfig: ContentConfig{ 123 GroupVersion: &schema.GroupVersion{}, 124 NegotiatedSerializer: &serializer.CodecFactory{}, 125 }, 126 } 127 client, err := RESTClientFor(config) 128 if err != nil { 129 t.Fatalf("failed to create REST client: %v", err) 130 } 131 data, err := client.Get().AbsPath("/").DoRaw(context.TODO()) 132 if err != nil { 133 t.Fatalf("unexpected err: %s: %v", data, err) 134 } 135 if string(data) != "Hello, HTTP/2.0" { 136 t.Fatalf("unexpected response: %s", data) 137 } 138 139 // Deliberately let the LB stop proxying traffic for the current 140 // connection. This mimics a broken TCP connection that's not properly 141 // closed. 142 close(stopCh) 143 144 stopCh = make(chan struct{}) 145 go lb.serve(stopCh) 146 // Sleep enough time for the HTTP/2 health check to detect and close 147 // the broken TCP connection. 148 time.Sleep(time.Duration(1+readIdleTimeout+pingTimeout) * time.Second) 149 // If the HTTP/2 health check were disabled, the broken connection 150 // would still be in the connection pool, the following request would 151 // then reuse the broken connection instead of creating a new one, and 152 // thus would fail. 153 data, err = client.Get().AbsPath("/").DoRaw(context.TODO()) 154 if err != nil { 155 t.Fatalf("unexpected err: %v", err) 156 } 157 if string(data) != "Hello, HTTP/2.0" { 158 t.Fatalf("unexpected response: %s", data) 159 } 160 dials := atomic.LoadInt32(&lb.dials) 161 if dials != 2 { 162 t.Fatalf("expected %d dials, got %d", 2, dials) 163 } 164 }