|
61 | 61 | import io.grpc.Server; |
62 | 62 | import io.grpc.Status; |
63 | 63 | import java.io.IOException; |
| 64 | +import java.util.ArrayList; |
| 65 | +import java.util.Collections; |
| 66 | +import java.util.List; |
64 | 67 | import java.util.concurrent.ExecutionException; |
| 68 | +import java.util.concurrent.Executor; |
65 | 69 | import java.util.concurrent.Executors; |
66 | 70 | import java.util.concurrent.ScheduledExecutorService; |
67 | 71 | import java.util.concurrent.TimeUnit; |
| 72 | +import java.util.concurrent.atomic.AtomicInteger; |
| 73 | +import javax.annotation.Nullable; |
68 | 74 | import org.junit.jupiter.api.AfterEach; |
69 | 75 | import org.junit.jupiter.api.BeforeEach; |
70 | 76 | import org.junit.jupiter.api.Test; |
@@ -414,4 +420,124 @@ void cancelInScheduledState() throws Exception { |
414 | 420 | VRpcException vRpcException = (VRpcException) executionException.getCause(); |
415 | 421 | assertThat(vRpcException.getStatus().getCode()).isEqualTo(Status.Code.CANCELLED); |
416 | 422 | } |
| 423 | + |
| 424 | + @Test |
| 425 | + void retryTransportFailureWithExponentialBackoff() throws Exception { |
| 426 | + List<Long> scheduledDelaysMs = Collections.synchronizedList(new ArrayList<>()); |
| 427 | + BigtableTimer recordingTimer = |
| 428 | + new BigtableTimer() { |
| 429 | + @Override |
| 430 | + public Timeout newTimeout(Runnable task, Executor exec, long delay, TimeUnit unit) { |
| 431 | + scheduledDelaysMs.add(unit.toMillis(delay)); |
| 432 | + return timer.newTimeout(task, exec, delay, unit); |
| 433 | + } |
| 434 | + |
| 435 | + @Override |
| 436 | + public void stop() { |
| 437 | + timer.stop(); |
| 438 | + } |
| 439 | + |
| 440 | + @Override |
| 441 | + public Registration onStop(Runnable hook) { |
| 442 | + return timer.onStop(hook); |
| 443 | + } |
| 444 | + }; |
| 445 | + |
| 446 | + AtomicInteger attemptCount = new AtomicInteger(); |
| 447 | + RetryingVRpc<SessionFakeScriptedRequest, SessionFakeScriptedResponse> retrying = |
| 448 | + new RetryingVRpc<>( |
| 449 | + () -> |
| 450 | + new VRpc<SessionFakeScriptedRequest, SessionFakeScriptedResponse>() { |
| 451 | + @Override |
| 452 | + public void start( |
| 453 | + SessionFakeScriptedRequest req, |
| 454 | + VRpc.VRpcCallContext ctx, |
| 455 | + VRpc.VRpcListener<SessionFakeScriptedResponse> listener) { |
| 456 | + int attempt = attemptCount.getAndIncrement(); |
| 457 | + ctx.getExecutor() |
| 458 | + .execute( |
| 459 | + () -> { |
| 460 | + if (attempt < 3) { |
| 461 | + listener.onClose( |
| 462 | + VRpc.VRpcResult.createLocalTransportError( |
| 463 | + Status.UNAVAILABLE.withDescription("missed heartbeat"))); |
| 464 | + } else { |
| 465 | + listener.onMessage( |
| 466 | + SessionFakeScriptedResponse.getDefaultInstance()); |
| 467 | + listener.onClose( |
| 468 | + VRpc.VRpcResult.createServerOk( |
| 469 | + VirtualRpcResponse.getDefaultInstance())); |
| 470 | + } |
| 471 | + }); |
| 472 | + } |
| 473 | + |
| 474 | + @Override |
| 475 | + public void cancel(@Nullable String message, @Nullable Throwable cause) {} |
| 476 | + |
| 477 | + @Override |
| 478 | + public boolean isDone() { |
| 479 | + return true; |
| 480 | + } |
| 481 | + |
| 482 | + @Override |
| 483 | + public void requestNext() {} |
| 484 | + }, |
| 485 | + recordingTimer); |
| 486 | + |
| 487 | + UnaryResponseFuture<SessionFakeScriptedResponse> f = new UnaryResponseFuture<>(); |
| 488 | + retrying.start( |
| 489 | + SessionFakeScriptedRequest.getDefaultInstance(), |
| 490 | + VRpc.VRpcCallContext.create(Deadline.after(1, TimeUnit.MINUTES), true, tracer), |
| 491 | + f); |
| 492 | + |
| 493 | + assertThat(f.get()).isEqualTo(SessionFakeScriptedResponse.getDefaultInstance()); |
| 494 | + assertThat(attemptCount.get()).isEqualTo(4); |
| 495 | + assertThat(scheduledDelaysMs).containsExactly(10L, 20L, 40L).inOrder(); |
| 496 | + } |
| 497 | + |
| 498 | + @Test |
| 499 | + void retryTransportFailureDeadlineExceeded() { |
| 500 | + AtomicInteger attemptCount = new AtomicInteger(); |
| 501 | + RetryingVRpc<SessionFakeScriptedRequest, SessionFakeScriptedResponse> retrying = |
| 502 | + new RetryingVRpc<>( |
| 503 | + () -> |
| 504 | + new VRpc<SessionFakeScriptedRequest, SessionFakeScriptedResponse>() { |
| 505 | + @Override |
| 506 | + public void start( |
| 507 | + SessionFakeScriptedRequest req, |
| 508 | + VRpc.VRpcCallContext ctx, |
| 509 | + VRpc.VRpcListener<SessionFakeScriptedResponse> listener) { |
| 510 | + attemptCount.incrementAndGet(); |
| 511 | + ctx.getExecutor() |
| 512 | + .execute( |
| 513 | + () -> |
| 514 | + listener.onClose( |
| 515 | + VRpc.VRpcResult.createLocalTransportError( |
| 516 | + Status.UNAVAILABLE.withDescription("missed heartbeat")))); |
| 517 | + } |
| 518 | + |
| 519 | + @Override |
| 520 | + public void cancel(@Nullable String message, @Nullable Throwable cause) {} |
| 521 | + |
| 522 | + @Override |
| 523 | + public boolean isDone() { |
| 524 | + return true; |
| 525 | + } |
| 526 | + |
| 527 | + @Override |
| 528 | + public void requestNext() {} |
| 529 | + }, |
| 530 | + timer); |
| 531 | + |
| 532 | + UnaryResponseFuture<SessionFakeScriptedResponse> f = new UnaryResponseFuture<>(); |
| 533 | + // Initial transport failure retry delay is 10ms, which exceeds a 5ms deadline. |
| 534 | + retrying.start( |
| 535 | + SessionFakeScriptedRequest.getDefaultInstance(), |
| 536 | + VRpc.VRpcCallContext.create(Deadline.after(5, TimeUnit.MILLISECONDS), true, tracer), |
| 537 | + f); |
| 538 | + |
| 539 | + ExecutionException exception = assertThrows(ExecutionException.class, f::get); |
| 540 | + assertThat(exception).hasMessageThat().contains("missed heartbeat"); |
| 541 | + assertThat(attemptCount.get()).isEqualTo(1); |
| 542 | + } |
417 | 543 | } |
0 commit comments