| 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 | } |
| 345 |
Members