github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/internal/jsonrpc2_v2/serve_test.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package jsonrpc2_test 6 7 import ( 8 "context" 9 "errors" 10 "fmt" 11 "runtime/debug" 12 "testing" 13 "time" 14 15 jsonrpc2 "golang.org/x/tools/internal/jsonrpc2_v2" 16 "golang.org/x/tools/internal/stack/stacktest" 17 ) 18 19 func TestIdleTimeout(t *testing.T) { 20 stacktest.NoLeak(t) 21 22 // Use a panicking time.AfterFunc instead of context.WithTimeout so that we 23 // get a goroutine dump on failure. We expect the test to take on the order of 24 // a few tens of milliseconds at most, so 10s should be several orders of 25 // magnitude of headroom. 26 timer := time.AfterFunc(10*time.Second, func() { 27 debug.SetTraceback("all") 28 panic("TestIdleTimeout deadlocked") 29 }) 30 defer timer.Stop() 31 32 ctx := context.Background() 33 34 try := func(d time.Duration) (longEnough bool) { 35 listener, err := jsonrpc2.NetListener(ctx, "tcp", "localhost:0", jsonrpc2.NetListenOptions{}) 36 if err != nil { 37 t.Fatal(err) 38 } 39 40 idleStart := time.Now() 41 listener = jsonrpc2.NewIdleListener(d, listener) 42 defer listener.Close() 43 44 server := jsonrpc2.NewServer(ctx, listener, jsonrpc2.ConnectionOptions{}) 45 46 // Exercise some connection/disconnection patterns, and then assert that when 47 // our timer fires, the server exits. 48 conn1, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) 49 if err != nil { 50 if since := time.Since(idleStart); since < d { 51 t.Fatalf("conn1 failed to connect after %v: %v", since, err) 52 } 53 t.Log("jsonrpc2.Dial:", err) 54 return false // Took to long to dial, so the failure could have been due to the idle timeout. 55 } 56 // On the server side, Accept can race with the connection timing out. 57 // Send a call and wait for the response to ensure that the connection was 58 // actually fully accepted. 59 ac := conn1.Call(ctx, "ping", nil) 60 if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { 61 if since := time.Since(idleStart); since < d { 62 t.Fatalf("conn1 broken after %v: %v", since, err) 63 } 64 t.Log(`conn1.Call(ctx, "ping", nil):`, err) 65 conn1.Close() 66 return false 67 } 68 69 // Since conn1 was successfully accepted and remains open, the server is 70 // definitely non-idle. Dialing another simultaneous connection should 71 // succeed. 72 conn2, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) 73 if err != nil { 74 conn1.Close() 75 t.Fatalf("conn2 failed to connect while non-idle after %v: %v", time.Since(idleStart), err) 76 return false 77 } 78 // Ensure that conn2 is also accepted on the server side before we close 79 // conn1. Otherwise, the connection can appear idle if the server processes 80 // the closure of conn1 and the idle timeout before it finally notices conn2 81 // in the accept queue. 82 // (That failure mode may explain the failure noted in 83 // https://go.dev/issue/49387#issuecomment-1303979877.) 84 ac = conn2.Call(ctx, "ping", nil) 85 if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { 86 t.Fatalf("conn2 broken while non-idle after %v: %v", time.Since(idleStart), err) 87 } 88 89 if err := conn1.Close(); err != nil { 90 t.Fatalf("conn1.Close failed with error: %v", err) 91 } 92 idleStart = time.Now() 93 if err := conn2.Close(); err != nil { 94 t.Fatalf("conn2.Close failed with error: %v", err) 95 } 96 97 conn3, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) 98 if err != nil { 99 if since := time.Since(idleStart); since < d { 100 t.Fatalf("conn3 failed to connect after %v: %v", since, err) 101 } 102 t.Log("jsonrpc2.Dial:", err) 103 return false // Took to long to dial, so the failure could have been due to the idle timeout. 104 } 105 106 ac = conn3.Call(ctx, "ping", nil) 107 if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { 108 if since := time.Since(idleStart); since < d { 109 t.Fatalf("conn3 broken after %v: %v", since, err) 110 } 111 t.Log(`conn3.Call(ctx, "ping", nil):`, err) 112 conn3.Close() 113 return false 114 } 115 116 idleStart = time.Now() 117 if err := conn3.Close(); err != nil { 118 t.Fatalf("conn3.Close failed with error: %v", err) 119 } 120 121 serverError := server.Wait() 122 123 if !errors.Is(serverError, jsonrpc2.ErrIdleTimeout) { 124 t.Errorf("run() returned error %v, want %v", serverError, jsonrpc2.ErrIdleTimeout) 125 } 126 if since := time.Since(idleStart); since < d { 127 t.Errorf("server shut down after %v idle; want at least %v", since, d) 128 } 129 return true 130 } 131 132 d := 1 * time.Millisecond 133 for { 134 t.Logf("testing with idle timout %v", d) 135 if !try(d) { 136 d *= 2 137 continue 138 } 139 break 140 } 141 } 142 143 type msg struct { 144 Msg string 145 } 146 147 type fakeHandler struct{} 148 149 func (fakeHandler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) { 150 switch req.Method { 151 case "ping": 152 return &msg{"pong"}, nil 153 default: 154 return nil, jsonrpc2.ErrNotHandled 155 } 156 } 157 158 func TestServe(t *testing.T) { 159 stacktest.NoLeak(t) 160 ctx := context.Background() 161 162 tests := []struct { 163 name string 164 factory func(context.Context) (jsonrpc2.Listener, error) 165 }{ 166 {"tcp", func(ctx context.Context) (jsonrpc2.Listener, error) { 167 return jsonrpc2.NetListener(ctx, "tcp", "localhost:0", jsonrpc2.NetListenOptions{}) 168 }}, 169 {"pipe", func(ctx context.Context) (jsonrpc2.Listener, error) { 170 return jsonrpc2.NetPipeListener(ctx) 171 }}, 172 } 173 174 for _, test := range tests { 175 t.Run(test.name, func(t *testing.T) { 176 fake, err := test.factory(ctx) 177 if err != nil { 178 t.Fatal(err) 179 } 180 conn, shutdown, err := newFake(t, ctx, fake) 181 if err != nil { 182 t.Fatal(err) 183 } 184 defer shutdown() 185 var got msg 186 if err := conn.Call(ctx, "ping", &msg{"ting"}).Await(ctx, &got); err != nil { 187 t.Fatal(err) 188 } 189 if want := "pong"; got.Msg != want { 190 t.Errorf("conn.Call(...): returned %q, want %q", got, want) 191 } 192 }) 193 } 194 } 195 196 func newFake(t *testing.T, ctx context.Context, l jsonrpc2.Listener) (*jsonrpc2.Connection, func(), error) { 197 server := jsonrpc2.NewServer(ctx, l, jsonrpc2.ConnectionOptions{ 198 Handler: fakeHandler{}, 199 }) 200 201 client, err := jsonrpc2.Dial(ctx, 202 l.Dialer(), 203 jsonrpc2.ConnectionOptions{ 204 Handler: fakeHandler{}, 205 }) 206 if err != nil { 207 return nil, nil, err 208 } 209 return client, func() { 210 if err := l.Close(); err != nil { 211 t.Fatal(err) 212 } 213 if err := client.Close(); err != nil { 214 t.Fatal(err) 215 } 216 server.Wait() 217 }, nil 218 } 219 220 // TestIdleListenerAcceptCloseRace checks for the Accept/Close race fixed in CL 388597. 221 // 222 // (A bug in the idleListener implementation caused a successful Accept to block 223 // on sending to a background goroutine that could have already exited.) 224 func TestIdleListenerAcceptCloseRace(t *testing.T) { 225 ctx := context.Background() 226 227 n := 10 228 229 // Each iteration of the loop appears to take around a millisecond, so to 230 // avoid spurious failures we'll set the watchdog for three orders of 231 // magnitude longer. When the bug was present, this reproduced the deadlock 232 // reliably on a Linux workstation when run with -count=100, which should be 233 // frequent enough to show up on the Go build dashboard if it regresses. 234 watchdog := time.Duration(n) * 1000 * time.Millisecond 235 timer := time.AfterFunc(watchdog, func() { 236 debug.SetTraceback("all") 237 panic(fmt.Sprintf("%s deadlocked after %v", t.Name(), watchdog)) 238 }) 239 defer timer.Stop() 240 241 for ; n > 0; n-- { 242 listener, err := jsonrpc2.NetPipeListener(ctx) 243 if err != nil { 244 t.Fatal(err) 245 } 246 listener = jsonrpc2.NewIdleListener(24*time.Hour, listener) 247 248 done := make(chan struct{}) 249 go func() { 250 conn, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) 251 listener.Close() 252 if err == nil { 253 conn.Close() 254 } 255 close(done) 256 }() 257 258 // Accept may return a non-nil error if Close closes the underlying network 259 // connection before the wrapped Accept call unblocks. However, it must not 260 // deadlock! 261 c, err := listener.Accept(ctx) 262 if err == nil { 263 c.Close() 264 } 265 <-done 266 } 267 } 268 269 // TestCloseCallRace checks for a race resulting in a deadlock when a Call on 270 // one side of the connection races with a Close (or otherwise broken 271 // connection) initiated from the other side. 272 // 273 // (The Call method was waiting for a result from the Read goroutine to 274 // determine which error value to return, but the Read goroutine was waiting for 275 // in-flight calls to complete before reporting that result.) 276 func TestCloseCallRace(t *testing.T) { 277 ctx := context.Background() 278 n := 10 279 280 watchdog := time.Duration(n) * 1000 * time.Millisecond 281 timer := time.AfterFunc(watchdog, func() { 282 debug.SetTraceback("all") 283 panic(fmt.Sprintf("%s deadlocked after %v", t.Name(), watchdog)) 284 }) 285 defer timer.Stop() 286 287 for ; n > 0; n-- { 288 listener, err := jsonrpc2.NetPipeListener(ctx) 289 if err != nil { 290 t.Fatal(err) 291 } 292 293 pokec := make(chan *jsonrpc2.AsyncCall, 1) 294 295 s := jsonrpc2.NewServer(ctx, listener, jsonrpc2.BinderFunc(func(_ context.Context, srvConn *jsonrpc2.Connection) jsonrpc2.ConnectionOptions { 296 h := jsonrpc2.HandlerFunc(func(ctx context.Context, _ *jsonrpc2.Request) (interface{}, error) { 297 // Start a concurrent call from the server to the client. 298 // The point of this test is to ensure this doesn't deadlock 299 // if the client shuts down the connection concurrently. 300 // 301 // The racing Call may or may not receive a response: it should get a 302 // response if it is sent before the client closes the connection, and 303 // it should fail with some kind of "connection closed" error otherwise. 304 go func() { 305 pokec <- srvConn.Call(ctx, "poke", nil) 306 }() 307 308 return &msg{"pong"}, nil 309 }) 310 return jsonrpc2.ConnectionOptions{Handler: h} 311 })) 312 313 dialConn, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) 314 if err != nil { 315 listener.Close() 316 s.Wait() 317 t.Fatal(err) 318 } 319 320 // Calling any method on the server should provoke it to asynchronously call 321 // us back. While it is starting that call, we will close the connection. 322 if err := dialConn.Call(ctx, "ping", nil).Await(ctx, nil); err != nil { 323 t.Error(err) 324 } 325 if err := dialConn.Close(); err != nil { 326 t.Error(err) 327 } 328 329 // Ensure that the Call on the server side did not block forever when the 330 // connection closed. 331 pokeCall := <-pokec 332 if err := pokeCall.Await(ctx, nil); err == nil { 333 t.Errorf("unexpected nil error from server-initited call") 334 } else if errors.Is(err, jsonrpc2.ErrMethodNotFound) { 335 // The call completed before the Close reached the handler. 336 } else { 337 // The error was something else. 338 t.Logf("server-initiated call completed with expected error: %v", err) 339 } 340 341 listener.Close() 342 s.Wait() 343 } 344 }