| // Copyright 2020 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package jsonrpc2_test |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "runtime/debug" |
| "testing" |
| "time" |
| |
| jsonrpc2 "golang.org/x/tools/internal/jsonrpc2_v2" |
| "golang.org/x/tools/internal/stack/stacktest" |
| "golang.org/x/tools/internal/testenv" |
| ) |
| |
| func TestIdleTimeout(t *testing.T) { |
| testenv.NeedsLocalhostNet(t) |
| stacktest.NoLeak(t) |
| |
| // Use a panicking time.AfterFunc instead of context.WithTimeout so that we |
| // get a goroutine dump on failure. We expect the test to take on the order of |
| // a few tens of milliseconds at most, so 10s should be several orders of |
| // magnitude of headroom. |
| timer := time.AfterFunc(10*time.Second, func() { |
| debug.SetTraceback("all") |
| panic("TestIdleTimeout deadlocked") |
| }) |
| defer timer.Stop() |
| |
| ctx := context.Background() |
| |
| try := func(d time.Duration) (longEnough bool) { |
| listener, err := jsonrpc2.NetListener(ctx, "tcp", "localhost:0", jsonrpc2.NetListenOptions{}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| idleStart := time.Now() |
| listener = jsonrpc2.NewIdleListener(d, listener) |
| defer listener.Close() |
| |
| server := jsonrpc2.NewServer(ctx, listener, jsonrpc2.ConnectionOptions{}) |
| |
| // Exercise some connection/disconnection patterns, and then assert that when |
| // our timer fires, the server exits. |
| conn1, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) |
| if err != nil { |
| if since := time.Since(idleStart); since < d { |
| t.Fatalf("conn1 failed to connect after %v: %v", since, err) |
| } |
| t.Log("jsonrpc2.Dial:", err) |
| return false // Took to long to dial, so the failure could have been due to the idle timeout. |
| } |
| // On the server side, Accept can race with the connection timing out. |
| // Send a call and wait for the response to ensure that the connection was |
| // actually fully accepted. |
| ac := conn1.Call(ctx, "ping", nil) |
| if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { |
| if since := time.Since(idleStart); since < d { |
| t.Fatalf("conn1 broken after %v: %v", since, err) |
| } |
| t.Log(`conn1.Call(ctx, "ping", nil):`, err) |
| conn1.Close() |
| return false |
| } |
| |
| // Since conn1 was successfully accepted and remains open, the server is |
| // definitely non-idle. Dialing another simultaneous connection should |
| // succeed. |
| conn2, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) |
| if err != nil { |
| conn1.Close() |
| t.Fatalf("conn2 failed to connect while non-idle after %v: %v", time.Since(idleStart), err) |
| return false |
| } |
| // Ensure that conn2 is also accepted on the server side before we close |
| // conn1. Otherwise, the connection can appear idle if the server processes |
| // the closure of conn1 and the idle timeout before it finally notices conn2 |
| // in the accept queue. |
| // (That failure mode may explain the failure noted in |
| // https://go.dev/issue/49387#issuecomment-1303979877.) |
| ac = conn2.Call(ctx, "ping", nil) |
| if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { |
| t.Fatalf("conn2 broken while non-idle after %v: %v", time.Since(idleStart), err) |
| } |
| |
| if err := conn1.Close(); err != nil { |
| t.Fatalf("conn1.Close failed with error: %v", err) |
| } |
| idleStart = time.Now() |
| if err := conn2.Close(); err != nil { |
| t.Fatalf("conn2.Close failed with error: %v", err) |
| } |
| |
| conn3, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) |
| if err != nil { |
| if since := time.Since(idleStart); since < d { |
| t.Fatalf("conn3 failed to connect after %v: %v", since, err) |
| } |
| t.Log("jsonrpc2.Dial:", err) |
| return false // Took to long to dial, so the failure could have been due to the idle timeout. |
| } |
| |
| ac = conn3.Call(ctx, "ping", nil) |
| if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { |
| if since := time.Since(idleStart); since < d { |
| t.Fatalf("conn3 broken after %v: %v", since, err) |
| } |
| t.Log(`conn3.Call(ctx, "ping", nil):`, err) |
| conn3.Close() |
| return false |
| } |
| |
| idleStart = time.Now() |
| if err := conn3.Close(); err != nil { |
| t.Fatalf("conn3.Close failed with error: %v", err) |
| } |
| |
| serverError := server.Wait() |
| |
| if !errors.Is(serverError, jsonrpc2.ErrIdleTimeout) { |
| t.Errorf("run() returned error %v, want %v", serverError, jsonrpc2.ErrIdleTimeout) |
| } |
| if since := time.Since(idleStart); since < d { |
| t.Errorf("server shut down after %v idle; want at least %v", since, d) |
| } |
| return true |
| } |
| |
| d := 1 * time.Millisecond |
| for { |
| t.Logf("testing with idle timeout %v", d) |
| if !try(d) { |
| d *= 2 |
| continue |
| } |
| break |
| } |
| } |
| |
| type msg struct { |
| Msg string |
| } |
| |
| type fakeHandler struct{} |
| |
| func (fakeHandler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) { |
| switch req.Method { |
| case "ping": |
| return &msg{"pong"}, nil |
| default: |
| return nil, jsonrpc2.ErrNotHandled |
| } |
| } |
| |
| func TestServe(t *testing.T) { |
| stacktest.NoLeak(t) |
| ctx := context.Background() |
| |
| tests := []struct { |
| name string |
| factory func(context.Context, testing.TB) (jsonrpc2.Listener, error) |
| }{ |
| {"tcp", func(ctx context.Context, t testing.TB) (jsonrpc2.Listener, error) { |
| testenv.NeedsLocalhostNet(t) |
| return jsonrpc2.NetListener(ctx, "tcp", "localhost:0", jsonrpc2.NetListenOptions{}) |
| }}, |
| {"pipe", func(ctx context.Context, t testing.TB) (jsonrpc2.Listener, error) { |
| return jsonrpc2.NetPipeListener(ctx) |
| }}, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| fake, err := test.factory(ctx, t) |
| if err != nil { |
| t.Fatal(err) |
| } |
| conn, shutdown, err := newFake(t, ctx, fake) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer shutdown() |
| var got msg |
| if err := conn.Call(ctx, "ping", &msg{"ting"}).Await(ctx, &got); err != nil { |
| t.Fatal(err) |
| } |
| if want := "pong"; got.Msg != want { |
| t.Errorf("conn.Call(...): returned %q, want %q", got, want) |
| } |
| }) |
| } |
| } |
| |
| func newFake(t *testing.T, ctx context.Context, l jsonrpc2.Listener) (*jsonrpc2.Connection, func(), error) { |
| server := jsonrpc2.NewServer(ctx, l, jsonrpc2.ConnectionOptions{ |
| Handler: fakeHandler{}, |
| }) |
| |
| client, err := jsonrpc2.Dial(ctx, |
| l.Dialer(), |
| jsonrpc2.ConnectionOptions{ |
| Handler: fakeHandler{}, |
| }) |
| if err != nil { |
| return nil, nil, err |
| } |
| return client, func() { |
| if err := l.Close(); err != nil { |
| t.Fatal(err) |
| } |
| if err := client.Close(); err != nil { |
| t.Fatal(err) |
| } |
| server.Wait() |
| }, nil |
| } |
| |
| // TestIdleListenerAcceptCloseRace checks for the Accept/Close race fixed in CL 388597. |
| // |
| // (A bug in the idleListener implementation caused a successful Accept to block |
| // on sending to a background goroutine that could have already exited.) |
| func TestIdleListenerAcceptCloseRace(t *testing.T) { |
| ctx := context.Background() |
| |
| n := 10 |
| |
| // Each iteration of the loop appears to take around a millisecond, so to |
| // avoid spurious failures we'll set the watchdog for three orders of |
| // magnitude longer. When the bug was present, this reproduced the deadlock |
| // reliably on a Linux workstation when run with -count=100, which should be |
| // frequent enough to show up on the Go build dashboard if it regresses. |
| watchdog := time.Duration(n) * 1000 * time.Millisecond |
| timer := time.AfterFunc(watchdog, func() { |
| debug.SetTraceback("all") |
| panic(fmt.Sprintf("%s deadlocked after %v", t.Name(), watchdog)) |
| }) |
| defer timer.Stop() |
| |
| for ; n > 0; n-- { |
| listener, err := jsonrpc2.NetPipeListener(ctx) |
| if err != nil { |
| t.Fatal(err) |
| } |
| listener = jsonrpc2.NewIdleListener(24*time.Hour, listener) |
| |
| done := make(chan struct{}) |
| go func() { |
| conn, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) |
| listener.Close() |
| if err == nil { |
| conn.Close() |
| } |
| close(done) |
| }() |
| |
| // Accept may return a non-nil error if Close closes the underlying network |
| // connection before the wrapped Accept call unblocks. However, it must not |
| // deadlock! |
| c, err := listener.Accept(ctx) |
| if err == nil { |
| c.Close() |
| } |
| <-done |
| } |
| } |
| |
| // TestCloseCallRace checks for a race resulting in a deadlock when a Call on |
| // one side of the connection races with a Close (or otherwise broken |
| // connection) initiated from the other side. |
| // |
| // (The Call method was waiting for a result from the Read goroutine to |
| // determine which error value to return, but the Read goroutine was waiting for |
| // in-flight calls to complete before reporting that result.) |
| func TestCloseCallRace(t *testing.T) { |
| ctx := context.Background() |
| n := 10 |
| |
| watchdog := time.Duration(n) * 1000 * time.Millisecond |
| timer := time.AfterFunc(watchdog, func() { |
| debug.SetTraceback("all") |
| panic(fmt.Sprintf("%s deadlocked after %v", t.Name(), watchdog)) |
| }) |
| defer timer.Stop() |
| |
| for ; n > 0; n-- { |
| listener, err := jsonrpc2.NetPipeListener(ctx) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| pokec := make(chan *jsonrpc2.AsyncCall, 1) |
| |
| s := jsonrpc2.NewServer(ctx, listener, jsonrpc2.BinderFunc(func(_ context.Context, srvConn *jsonrpc2.Connection) jsonrpc2.ConnectionOptions { |
| h := jsonrpc2.HandlerFunc(func(ctx context.Context, _ *jsonrpc2.Request) (interface{}, error) { |
| // Start a concurrent call from the server to the client. |
| // The point of this test is to ensure this doesn't deadlock |
| // if the client shuts down the connection concurrently. |
| // |
| // The racing Call may or may not receive a response: it should get a |
| // response if it is sent before the client closes the connection, and |
| // it should fail with some kind of "connection closed" error otherwise. |
| go func() { |
| pokec <- srvConn.Call(ctx, "poke", nil) |
| }() |
| |
| return &msg{"pong"}, nil |
| }) |
| return jsonrpc2.ConnectionOptions{Handler: h} |
| })) |
| |
| dialConn, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) |
| if err != nil { |
| listener.Close() |
| s.Wait() |
| t.Fatal(err) |
| } |
| |
| // Calling any method on the server should provoke it to asynchronously call |
| // us back. While it is starting that call, we will close the connection. |
| if err := dialConn.Call(ctx, "ping", nil).Await(ctx, nil); err != nil { |
| t.Error(err) |
| } |
| if err := dialConn.Close(); err != nil { |
| t.Error(err) |
| } |
| |
| // Ensure that the Call on the server side did not block forever when the |
| // connection closed. |
| pokeCall := <-pokec |
| if err := pokeCall.Await(ctx, nil); err == nil { |
| t.Errorf("unexpected nil error from server-initited call") |
| } else if errors.Is(err, jsonrpc2.ErrMethodNotFound) { |
| // The call completed before the Close reached the handler. |
| } else { |
| // The error was something else. |
| t.Logf("server-initiated call completed with expected error: %v", err) |
| } |
| |
| listener.Close() |
| s.Wait() |
| } |
| } |