github.com/core-coin/go-core/v2@v2.1.9/rpc/client_test.go (about) 1 // Copyright 2016 by the Authors 2 // This file is part of the go-core library. 3 // 4 // The go-core library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-core library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-core library. If not, see <http://www.gnu.org/licenses/>. 16 17 package rpc 18 19 import ( 20 "context" 21 "fmt" 22 "math/rand" 23 "net" 24 "net/http" 25 "net/http/httptest" 26 "os" 27 "reflect" 28 "runtime" 29 "strings" 30 "sync" 31 "testing" 32 "time" 33 34 "github.com/davecgh/go-spew/spew" 35 36 "github.com/core-coin/go-core/v2/log" 37 ) 38 39 func TestClientRequest(t *testing.T) { 40 server := newTestServer() 41 defer server.Stop() 42 client := DialInProc(server) 43 defer client.Close() 44 45 var resp echoResult 46 if err := client.Call(&resp, "test_echo", "hello", 10, &echoArgs{"world"}); err != nil { 47 t.Fatal(err) 48 } 49 if !reflect.DeepEqual(resp, echoResult{"hello", 10, &echoArgs{"world"}}) { 50 t.Errorf("incorrect result %#v", resp) 51 } 52 } 53 54 func TestClientResponseType(t *testing.T) { 55 server := newTestServer() 56 defer server.Stop() 57 client := DialInProc(server) 58 defer client.Close() 59 60 if err := client.Call(nil, "test_echo", "hello", 10, &echoArgs{"world"}); err != nil { 61 t.Errorf("Passing nil as result should be fine, but got an error: %v", err) 62 } 63 var resultVar echoResult 64 // Note: passing the var, not a ref 65 err := client.Call(resultVar, "test_echo", "hello", 10, &echoArgs{"world"}) 66 if err == nil { 67 t.Error("Passing a var as result should be an error") 68 } 69 } 70 71 // This test checks that server-returned errors with code and data come out of Client.Call. 72 func TestClientErrorData(t *testing.T) { 73 server := newTestServer() 74 defer server.Stop() 75 client := DialInProc(server) 76 defer client.Close() 77 78 var resp interface{} 79 err := client.Call(&resp, "test_returnError") 80 if err == nil { 81 t.Fatal("expected error") 82 } 83 84 // Check code. 85 if e, ok := err.(Error); !ok { 86 t.Fatalf("client did not return rpc.Error, got %#v", e) 87 } else if e.ErrorCode() != (testError{}.ErrorCode()) { 88 t.Fatalf("wrong error code %d, want %d", e.ErrorCode(), testError{}.ErrorCode()) 89 } 90 // Check data. 91 if e, ok := err.(DataError); !ok { 92 t.Fatalf("client did not return rpc.DataError, got %#v", e) 93 } else if e.ErrorData() != (testError{}.ErrorData()) { 94 t.Fatalf("wrong error data %#v, want %#v", e.ErrorData(), testError{}.ErrorData()) 95 } 96 } 97 98 func TestClientBatchRequest(t *testing.T) { 99 server := newTestServer() 100 defer server.Stop() 101 client := DialInProc(server) 102 defer client.Close() 103 104 batch := []BatchElem{ 105 { 106 Method: "test_echo", 107 Args: []interface{}{"hello", 10, &echoArgs{"world"}}, 108 Result: new(echoResult), 109 }, 110 { 111 Method: "test_echo", 112 Args: []interface{}{"hello2", 11, &echoArgs{"world"}}, 113 Result: new(echoResult), 114 }, 115 { 116 Method: "no_such_method", 117 Args: []interface{}{1, 2, 3}, 118 Result: new(int), 119 }, 120 } 121 if err := client.BatchCall(batch); err != nil { 122 t.Fatal(err) 123 } 124 wantResult := []BatchElem{ 125 { 126 Method: "test_echo", 127 Args: []interface{}{"hello", 10, &echoArgs{"world"}}, 128 Result: &echoResult{"hello", 10, &echoArgs{"world"}}, 129 }, 130 { 131 Method: "test_echo", 132 Args: []interface{}{"hello2", 11, &echoArgs{"world"}}, 133 Result: &echoResult{"hello2", 11, &echoArgs{"world"}}, 134 }, 135 { 136 Method: "no_such_method", 137 Args: []interface{}{1, 2, 3}, 138 Result: new(int), 139 Error: &jsonError{Code: -32601, Message: "the method no_such_method does not exist/is not available"}, 140 }, 141 } 142 if !reflect.DeepEqual(batch, wantResult) { 143 t.Errorf("batch results mismatch:\ngot %swant %s", spew.Sdump(batch), spew.Sdump(wantResult)) 144 } 145 } 146 147 func TestClientNotify(t *testing.T) { 148 server := newTestServer() 149 defer server.Stop() 150 client := DialInProc(server) 151 defer client.Close() 152 153 if err := client.Notify(context.Background(), "test_echo", "hello", 10, &echoArgs{"world"}); err != nil { 154 t.Fatal(err) 155 } 156 } 157 158 // func TestClientCancelInproc(t *testing.T) { testClientCancel("inproc", t) } 159 func TestClientCancelWebsocket(t *testing.T) { testClientCancel("ws", t) } 160 func TestClientCancelHTTP(t *testing.T) { testClientCancel("http", t) } 161 func TestClientCancelIPC(t *testing.T) { testClientCancel("ipc", t) } 162 163 // This test checks that requests made through CallContext can be canceled by canceling 164 // the context. 165 func testClientCancel(transport string, t *testing.T) { 166 // These tests take a lot of time, run them all at once. 167 // You probably want to run with -parallel 1 or comment out 168 // the call to t.Parallel if you enable the logging. 169 t.Parallel() 170 171 server := newTestServer() 172 defer server.Stop() 173 174 // What we want to achieve is that the context gets canceled 175 // at various stages of request processing. The interesting cases 176 // are: 177 // - cancel during dial 178 // - cancel while performing a HTTP request 179 // - cancel while waiting for a response 180 // 181 // To trigger those, the times are chosen such that connections 182 // are killed within the deadline for every other call (maxKillTimeout 183 // is 2x maxCancelTimeout). 184 // 185 // Once a connection is dead, there is a fair chance it won't connect 186 // successfully because the accept is delayed by 1s. 187 maxContextCancelTimeout := 300 * time.Millisecond 188 fl := &flakeyListener{ 189 maxAcceptDelay: 1 * time.Second, 190 maxKillTimeout: 600 * time.Millisecond, 191 } 192 193 var client *Client 194 switch transport { 195 case "ws", "http": 196 c, hs := httpTestClient(server, transport, fl) 197 defer hs.Close() 198 client = c 199 case "ipc": 200 c, l := ipcTestClient(server, fl) 201 defer l.Close() 202 client = c 203 default: 204 panic("unknown transport: " + transport) 205 } 206 207 // The actual test starts here. 208 var ( 209 wg sync.WaitGroup 210 nreqs = 10 211 ncallers = 10 212 ) 213 caller := func(index int) { 214 defer wg.Done() 215 for i := 0; i < nreqs; i++ { 216 var ( 217 ctx context.Context 218 cancel func() 219 timeout = time.Duration(rand.Int63n(int64(maxContextCancelTimeout))) 220 ) 221 if index < ncallers/2 { 222 // For half of the callers, create a context without deadline 223 // and cancel it later. 224 ctx, cancel = context.WithCancel(context.Background()) 225 time.AfterFunc(timeout, cancel) 226 } else { 227 // For the other half, create a context with a deadline instead. This is 228 // different because the context deadline is used to set the socket write 229 // deadline. 230 ctx, cancel = context.WithTimeout(context.Background(), timeout) 231 } 232 233 // Now perform a call with the context. 234 // The key thing here is that no call will ever complete successfully. 235 err := client.CallContext(ctx, nil, "test_block") 236 switch { 237 case err == nil: 238 _, hasDeadline := ctx.Deadline() 239 t.Errorf("no error for call with %v wait time (deadline: %v)", timeout, hasDeadline) 240 // default: 241 // t.Logf("got expected error with %v wait time: %v", timeout, err) 242 } 243 cancel() 244 } 245 } 246 wg.Add(ncallers) 247 for i := 0; i < ncallers; i++ { 248 go caller(i) 249 } 250 wg.Wait() 251 } 252 253 func TestClientSubscribeInvalidArg(t *testing.T) { 254 server := newTestServer() 255 defer server.Stop() 256 client := DialInProc(server) 257 defer client.Close() 258 259 check := func(shouldPanic bool, arg interface{}) { 260 defer func() { 261 err := recover() 262 if shouldPanic && err == nil { 263 t.Errorf("XcbSubscribe should've panicked for %#v", arg) 264 } 265 if !shouldPanic && err != nil { 266 t.Errorf("XcbSubscribe shouldn't have panicked for %#v", arg) 267 buf := make([]byte, 1024*1024) 268 buf = buf[:runtime.Stack(buf, false)] 269 t.Error(err) 270 t.Error(string(buf)) 271 } 272 }() 273 client.XcbSubscribe(context.Background(), arg, "foo_bar") 274 } 275 check(true, nil) 276 check(true, 1) 277 check(true, (chan int)(nil)) 278 check(true, make(<-chan int)) 279 check(false, make(chan int)) 280 check(false, make(chan<- int)) 281 } 282 283 func TestClientSubscribe(t *testing.T) { 284 server := newTestServer() 285 defer server.Stop() 286 client := DialInProc(server) 287 defer client.Close() 288 289 nc := make(chan int) 290 count := 10 291 sub, err := client.Subscribe(context.Background(), "nftest", nc, "someSubscription", count, 0) 292 if err != nil { 293 t.Fatal("can't subscribe:", err) 294 } 295 for i := 0; i < count; i++ { 296 if val := <-nc; val != i { 297 t.Fatalf("value mismatch: got %d, want %d", val, i) 298 } 299 } 300 301 sub.Unsubscribe() 302 select { 303 case v := <-nc: 304 t.Fatal("received value after unsubscribe:", v) 305 case err := <-sub.Err(): 306 if err != nil { 307 t.Fatalf("Err returned a non-nil error after explicit unsubscribe: %q", err) 308 } 309 case <-time.After(1 * time.Second): 310 t.Fatalf("subscription not closed within 1s after unsubscribe") 311 } 312 } 313 314 // In this test, the connection drops while Subscribe is waiting for a response. 315 func TestClientSubscribeClose(t *testing.T) { 316 server := newTestServer() 317 service := ¬ificationTestService{ 318 gotHangSubscriptionReq: make(chan struct{}), 319 unblockHangSubscription: make(chan struct{}), 320 } 321 if err := server.RegisterName("nftest2", service); err != nil { 322 t.Fatal(err) 323 } 324 325 defer server.Stop() 326 client := DialInProc(server) 327 defer client.Close() 328 329 var ( 330 nc = make(chan int) 331 errc = make(chan error, 1) 332 sub *ClientSubscription 333 err error 334 ) 335 go func() { 336 sub, err = client.Subscribe(context.Background(), "nftest2", nc, "hangSubscription", 999) 337 errc <- err 338 }() 339 340 <-service.gotHangSubscriptionReq 341 client.Close() 342 service.unblockHangSubscription <- struct{}{} 343 344 select { 345 case err := <-errc: 346 if err == nil { 347 t.Errorf("Subscribe returned nil error after Close") 348 } 349 if sub != nil { 350 t.Error("Subscribe returned non-nil subscription after Close") 351 } 352 case <-time.After(1 * time.Second): 353 t.Fatalf("Subscribe did not return within 1s after Close") 354 } 355 } 356 357 // This test reproduces https://github.com/core-coin/go-core/v2/issues/17837 where the 358 // client hangs during shutdown when Unsubscribe races with Client.Close. 359 func TestClientCloseUnsubscribeRace(t *testing.T) { 360 server := newTestServer() 361 defer server.Stop() 362 363 for i := 0; i < 20; i++ { 364 client := DialInProc(server) 365 nc := make(chan int) 366 sub, err := client.Subscribe(context.Background(), "nftest", nc, "someSubscription", 3, 1) 367 if err != nil { 368 t.Fatal(err) 369 } 370 go client.Close() 371 go sub.Unsubscribe() 372 select { 373 case <-sub.Err(): 374 case <-time.After(5 * time.Second): 375 t.Fatal("subscription not closed within timeout") 376 } 377 } 378 } 379 380 // This test checks that Client doesn't lock up when a single subscriber 381 // doesn't read subscription events. 382 func TestClientNotificationStorm(t *testing.T) { 383 server := newTestServer() 384 defer server.Stop() 385 386 doTest := func(count int, wantError bool) { 387 client := DialInProc(server) 388 defer client.Close() 389 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 390 defer cancel() 391 392 // Subscribe on the server. It will start sending many notifications 393 // very quickly. 394 nc := make(chan int) 395 sub, err := client.Subscribe(ctx, "nftest", nc, "someSubscription", count, 0) 396 if err != nil { 397 t.Fatal("can't subscribe:", err) 398 } 399 defer sub.Unsubscribe() 400 401 // Process each notification, try to run a call in between each of them. 402 for i := 0; i < count; i++ { 403 select { 404 case val := <-nc: 405 if val != i { 406 t.Fatalf("(%d/%d) unexpected value %d", i, count, val) 407 } 408 case err := <-sub.Err(): 409 if wantError && err != ErrSubscriptionQueueOverflow { 410 t.Fatalf("(%d/%d) got error %q, want %q", i, count, err, ErrSubscriptionQueueOverflow) 411 } else if !wantError { 412 t.Fatalf("(%d/%d) got unexpected error %q", i, count, err) 413 } 414 return 415 } 416 var r int 417 err := client.CallContext(ctx, &r, "nftest_echo", i) 418 if err != nil { 419 if !wantError { 420 t.Fatalf("(%d/%d) call error: %v", i, count, err) 421 } 422 return 423 } 424 } 425 if wantError { 426 t.Fatalf("didn't get expected error") 427 } 428 } 429 430 doTest(8000, false) 431 doTest(23000, true) 432 } 433 434 func TestClientSetHeader(t *testing.T) { 435 var gotHeader bool 436 srv := newTestServer() 437 httpsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 438 if r.Header.Get("test") == "ok" { 439 gotHeader = true 440 } 441 srv.ServeHTTP(w, r) 442 })) 443 defer httpsrv.Close() 444 defer srv.Stop() 445 446 client, err := Dial(httpsrv.URL) 447 if err != nil { 448 t.Fatal(err) 449 } 450 defer client.Close() 451 452 client.SetHeader("test", "ok") 453 if _, err := client.SupportedModules(); err != nil { 454 t.Fatal(err) 455 } 456 if !gotHeader { 457 t.Fatal("client did not set custom header") 458 } 459 460 // Check that Content-Type can be replaced. 461 client.SetHeader("content-type", "application/x-garbage") 462 _, err = client.SupportedModules() 463 if err == nil { 464 t.Fatal("no error for invalid content-type header") 465 } else if !strings.Contains(err.Error(), "Unsupported Media Type") { 466 t.Fatalf("error is not related to content-type: %q", err) 467 } 468 } 469 470 func TestClientHTTP(t *testing.T) { 471 server := newTestServer() 472 defer server.Stop() 473 474 client, hs := httpTestClient(server, "http", nil) 475 defer hs.Close() 476 defer client.Close() 477 478 // Launch concurrent requests. 479 var ( 480 results = make([]echoResult, 100) 481 errc = make(chan error, len(results)) 482 wantResult = echoResult{"a", 1, new(echoArgs)} 483 ) 484 defer client.Close() 485 for i := range results { 486 i := i 487 go func() { 488 errc <- client.Call(&results[i], "test_echo", wantResult.String, wantResult.Int, wantResult.Args) 489 }() 490 } 491 492 // Wait for all of them to complete. 493 timeout := time.NewTimer(5 * time.Second) 494 defer timeout.Stop() 495 for i := range results { 496 select { 497 case err := <-errc: 498 if err != nil { 499 t.Fatal(err) 500 } 501 case <-timeout.C: 502 t.Fatalf("timeout (got %d/%d) results)", i+1, len(results)) 503 } 504 } 505 506 // Check results. 507 for i := range results { 508 if !reflect.DeepEqual(results[i], wantResult) { 509 t.Errorf("result %d mismatch: got %#v, want %#v", i, results[i], wantResult) 510 } 511 } 512 } 513 514 func TestClientReconnect(t *testing.T) { 515 startServer := func(addr string) (*Server, net.Listener) { 516 srv := newTestServer() 517 l, err := net.Listen("tcp", addr) 518 if err != nil { 519 t.Fatal("can't listen:", err) 520 } 521 go http.Serve(l, srv.WebsocketHandler([]string{"*"})) 522 return srv, l 523 } 524 525 ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) 526 defer cancel() 527 528 // Start a server and corresponding client. 529 s1, l1 := startServer("127.0.0.1:0") 530 client, err := DialContext(ctx, "ws://"+l1.Addr().String()) 531 if err != nil { 532 t.Fatal("can't dial", err) 533 } 534 535 // Perform a call. This should work because the server is up. 536 var resp echoResult 537 if err := client.CallContext(ctx, &resp, "test_echo", "", 1, nil); err != nil { 538 t.Fatal(err) 539 } 540 541 // Shut down the server and allow for some cool down time so we can listen on the same 542 // address again. 543 l1.Close() 544 s1.Stop() 545 time.Sleep(2 * time.Second) 546 547 // Try calling again. It shouldn't work. 548 if err := client.CallContext(ctx, &resp, "test_echo", "", 2, nil); err == nil { 549 t.Error("successful call while the server is down") 550 t.Logf("resp: %#v", resp) 551 } 552 553 // Start it up again and call again. The connection should be reestablished. 554 // We spawn multiple calls here to check whether this hangs somehow. 555 s2, l2 := startServer(l1.Addr().String()) 556 defer l2.Close() 557 defer s2.Stop() 558 559 start := make(chan struct{}) 560 errors := make(chan error, 20) 561 for i := 0; i < cap(errors); i++ { 562 go func() { 563 <-start 564 var resp echoResult 565 errors <- client.CallContext(ctx, &resp, "test_echo", "", 3, nil) 566 }() 567 } 568 close(start) 569 errcount := 0 570 for i := 0; i < cap(errors); i++ { 571 if err = <-errors; err != nil { 572 errcount++ 573 } 574 } 575 t.Logf("%d errors, last error: %v", errcount, err) 576 if errcount > 1 { 577 t.Errorf("expected one error after disconnect, got %d", errcount) 578 } 579 } 580 581 func httpTestClient(srv *Server, transport string, fl *flakeyListener) (*Client, *httptest.Server) { 582 // Create the HTTP server. 583 var hs *httptest.Server 584 switch transport { 585 case "ws": 586 hs = httptest.NewUnstartedServer(srv.WebsocketHandler([]string{"*"})) 587 case "http": 588 hs = httptest.NewUnstartedServer(srv) 589 default: 590 panic("unknown HTTP transport: " + transport) 591 } 592 // Wrap the listener if required. 593 if fl != nil { 594 fl.Listener = hs.Listener 595 hs.Listener = fl 596 } 597 // Connect the client. 598 hs.Start() 599 client, err := Dial(transport + "://" + hs.Listener.Addr().String()) 600 if err != nil { 601 panic(err) 602 } 603 return client, hs 604 } 605 606 func ipcTestClient(srv *Server, fl *flakeyListener) (*Client, net.Listener) { 607 // Listen on a random endpoint. 608 endpoint := fmt.Sprintf("go-core-test-ipc-%d-%d", os.Getpid(), rand.Int63()) 609 if runtime.GOOS == "windows" { 610 endpoint = `\\.\pipe\` + endpoint 611 } else { 612 endpoint = os.TempDir() + "/" + endpoint 613 } 614 l, err := ipcListen(endpoint) 615 if err != nil { 616 panic(err) 617 } 618 // Connect the listener to the server. 619 if fl != nil { 620 fl.Listener = l 621 l = fl 622 } 623 go srv.ServeListener(l) 624 // Connect the client. 625 client, err := Dial(endpoint) 626 if err != nil { 627 panic(err) 628 } 629 return client, l 630 } 631 632 // flakeyListener kills accepted connections after a random timeout. 633 type flakeyListener struct { 634 net.Listener 635 maxKillTimeout time.Duration 636 maxAcceptDelay time.Duration 637 } 638 639 func (l *flakeyListener) Accept() (net.Conn, error) { 640 delay := time.Duration(rand.Int63n(int64(l.maxAcceptDelay))) 641 time.Sleep(delay) 642 643 c, err := l.Listener.Accept() 644 if err == nil { 645 timeout := time.Duration(rand.Int63n(int64(l.maxKillTimeout))) 646 time.AfterFunc(timeout, func() { 647 log.Debug(fmt.Sprintf("killing conn %v after %v", c.LocalAddr(), timeout)) 648 c.Close() 649 }) 650 } 651 return c, err 652 }