|
30 | 30 | import com.google.cloud.bigtable.data.v2.internal.channels.SessionStream.Listener; |
31 | 31 | import com.google.cloud.bigtable.data.v2.internal.csm.NoopMetrics; |
32 | 32 | import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DebugTagTracer; |
| 33 | +import com.google.common.base.Ticker; |
33 | 34 | import io.grpc.Attributes; |
34 | 35 | import io.grpc.CallOptions; |
35 | 36 | import io.grpc.ClientCall; |
|
41 | 42 | import java.util.Base64; |
42 | 43 | import java.util.List; |
43 | 44 | import java.util.concurrent.ScheduledExecutorService; |
| 45 | +import java.util.concurrent.TimeUnit; |
| 46 | +import java.util.concurrent.atomic.AtomicLong; |
44 | 47 | import java.util.function.Supplier; |
45 | 48 | import org.junit.jupiter.api.Test; |
46 | 49 | import org.junit.jupiter.api.extension.ExtendWith; |
@@ -235,7 +238,6 @@ void testDownsize() { |
235 | 238 | listener.onClose(Status.OK, new Metadata()); |
236 | 239 | } |
237 | 240 |
|
238 | | - when(clock.instant()).thenReturn(Instant.now()); |
239 | 241 | pool.serviceChannels(); |
240 | 242 | verify(channel, times(numChannels - pool.minGroups)).shutdown(); |
241 | 243 |
|
@@ -295,7 +297,6 @@ void testDownsizeToOptimal() { |
295 | 297 | // I.e. dumpState |
296 | 298 | // FINE: ChannelPool channelGroups: 5, channels: 5, starting channels: 0, totalStreams: 19, |
297 | 299 | // AFEs: 5, distribution: [4, 4, 4, 4, 3] |
298 | | - when(clock.instant()).thenReturn(Instant.now()); |
299 | 300 | pool.serviceChannels(); |
300 | 301 |
|
301 | 302 | // Should scale down to 4 channels. 19 / 5 round up = 4. |
@@ -469,4 +470,169 @@ void testRecycledChannelDoesNotRejoinPool() throws InterruptedException { |
469 | 470 |
|
470 | 471 | pool.close(); |
471 | 472 | } |
| 473 | + |
| 474 | + @Test |
| 475 | + void testRecycleChannelOnConsecutiveFailures() { |
| 476 | + when(channelSupplier.get()).thenReturn(channel); |
| 477 | + when(channel.newCall(any(), any())).thenReturn(clientCall); |
| 478 | + doNothing().when(clientCall).start(listener.capture(), any()); |
| 479 | + |
| 480 | + ChannelPoolDpImpl pool = |
| 481 | + new ChannelPoolDpImpl(channelSupplier, defaultConfig, debugTagTracer, bgExecutor); |
| 482 | + |
| 483 | + for (int i = 0; i < 4; i++) { |
| 484 | + pool.newStream(FakeSessionGrpc.getOpenSessionMethod(), CallOptions.DEFAULT) |
| 485 | + .start(mock(Listener.class), new Metadata()); |
| 486 | + listener.getValue().onClose(Status.UNAVAILABLE, new Metadata()); |
| 487 | + |
| 488 | + // Should not be recycled yet |
| 489 | + verify(channel, times(0)).shutdown(); |
| 490 | + verify(channelSupplier, times(1)).get(); |
| 491 | + } |
| 492 | + |
| 493 | + // 5th failure |
| 494 | + pool.newStream(FakeSessionGrpc.getOpenSessionMethod(), CallOptions.DEFAULT) |
| 495 | + .start(mock(Listener.class), new Metadata()); |
| 496 | + listener.getValue().onClose(Status.UNAVAILABLE, new Metadata()); |
| 497 | + |
| 498 | + // Now it should be recycled |
| 499 | + verify(channel, times(1)).shutdown(); |
| 500 | + verify(channelSupplier, times(2)).get(); |
| 501 | + |
| 502 | + pool.close(); |
| 503 | + } |
| 504 | + |
| 505 | + @Test |
| 506 | + void testResetConsecutiveFailuresOnSuccess() { |
| 507 | + when(channelSupplier.get()).thenReturn(channel); |
| 508 | + when(channel.newCall(any(), any())).thenReturn(clientCall); |
| 509 | + doNothing().when(clientCall).start(listener.capture(), any()); |
| 510 | + doReturn(Attributes.EMPTY).when(clientCall).getAttributes(); |
| 511 | + |
| 512 | + ChannelPoolDpImpl pool = |
| 513 | + new ChannelPoolDpImpl(channelSupplier, defaultConfig, debugTagTracer, bgExecutor); |
| 514 | + |
| 515 | + // 4 failures |
| 516 | + for (int i = 0; i < 4; i++) { |
| 517 | + pool.newStream(FakeSessionGrpc.getOpenSessionMethod(), CallOptions.DEFAULT) |
| 518 | + .start(mock(Listener.class), new Metadata()); |
| 519 | + listener.getValue().onClose(Status.UNAVAILABLE, new Metadata()); |
| 520 | + } |
| 521 | + verify(channel, times(0)).shutdown(); |
| 522 | + |
| 523 | + // A success: onHeaders (which calls onBeforeSessionStart) |
| 524 | + pool.newStream(FakeSessionGrpc.getOpenSessionMethod(), CallOptions.DEFAULT) |
| 525 | + .start(mock(Listener.class), new Metadata()); |
| 526 | + |
| 527 | + PeerInfo peerInfo = PeerInfo.newBuilder().setApplicationFrontendId(555).build(); |
| 528 | + Metadata headers = new Metadata(); |
| 529 | + headers.put( |
| 530 | + SessionStreamImpl.PEER_INFO_KEY, |
| 531 | + Base64.getEncoder().encodeToString(peerInfo.toByteArray())); |
| 532 | + listener.getValue().onHeaders(headers); |
| 533 | + listener.getValue().onClose(Status.OK, new Metadata()); |
| 534 | + |
| 535 | + // Another 4 failures - should still not recycle because counter was reset |
| 536 | + for (int i = 0; i < 4; i++) { |
| 537 | + pool.newStream(FakeSessionGrpc.getOpenSessionMethod(), CallOptions.DEFAULT) |
| 538 | + .start(mock(Listener.class), new Metadata()); |
| 539 | + listener.getValue().onClose(Status.UNAVAILABLE, new Metadata()); |
| 540 | + } |
| 541 | + verify(channel, times(0)).shutdown(); |
| 542 | + |
| 543 | + pool.close(); |
| 544 | + } |
| 545 | + |
| 546 | + @Test |
| 547 | + void testCancelledDoesNotIncrementFailures() { |
| 548 | + when(channelSupplier.get()).thenReturn(channel); |
| 549 | + when(channel.newCall(any(), any())).thenReturn(clientCall); |
| 550 | + doNothing().when(clientCall).start(listener.capture(), any()); |
| 551 | + |
| 552 | + ChannelPoolDpImpl pool = |
| 553 | + new ChannelPoolDpImpl(channelSupplier, defaultConfig, debugTagTracer, bgExecutor); |
| 554 | + |
| 555 | + for (int i = 0; i < 10; i++) { |
| 556 | + pool.newStream(FakeSessionGrpc.getOpenSessionMethod(), CallOptions.DEFAULT) |
| 557 | + .start(mock(Listener.class), new Metadata()); |
| 558 | + listener.getValue().onClose(Status.CANCELLED, new Metadata()); |
| 559 | + } |
| 560 | + |
| 561 | + // Should never be recycled |
| 562 | + verify(channel, times(0)).shutdown(); |
| 563 | + verify(channelSupplier, times(1)).get(); |
| 564 | + |
| 565 | + pool.close(); |
| 566 | + } |
| 567 | + |
| 568 | + @Test |
| 569 | + void testRecycleChannelBackoff() { |
| 570 | + when(channelSupplier.get()).thenReturn(channel); |
| 571 | + when(channel.newCall(any(), any())).thenReturn(clientCall); |
| 572 | + doNothing().when(clientCall).start(listener.capture(), any()); |
| 573 | + |
| 574 | + Ticker ticker = mock(Ticker.class); |
| 575 | + long startNanos = TimeUnit.SECONDS.toNanos(1); |
| 576 | + final AtomicLong time = new AtomicLong(startNanos); |
| 577 | + when(ticker.read()).thenAnswer(invocation -> time.get()); |
| 578 | + |
| 579 | + ChannelPoolDpImpl pool = |
| 580 | + new ChannelPoolDpImpl( |
| 581 | + channelSupplier, |
| 582 | + defaultConfig, |
| 583 | + "pool", |
| 584 | + debugTagTracer, |
| 585 | + bgExecutor, |
| 586 | + Clock.systemUTC(), |
| 587 | + ticker); |
| 588 | + |
| 589 | + // --- First Recycle --- |
| 590 | + for (int i = 0; i < 5; i++) { |
| 591 | + pool.newStream(FakeSessionGrpc.getOpenSessionMethod(), CallOptions.DEFAULT) |
| 592 | + .start(mock(Listener.class), new Metadata()); |
| 593 | + listener.getValue().onClose(Status.UNAVAILABLE, new Metadata()); |
| 594 | + } |
| 595 | + // Should be recycled once |
| 596 | + verify(channel, times(1)).shutdown(); |
| 597 | + verify(channelSupplier, times(2)).get(); // 1 initial + 1 recycle |
| 598 | + |
| 599 | + // --- Second Recycle (Immediate, same time) --- |
| 600 | + // Time has not advanced. Backoff is now 2ms. |
| 601 | + for (int i = 0; i < 5; i++) { |
| 602 | + pool.newStream(FakeSessionGrpc.getOpenSessionMethod(), CallOptions.DEFAULT) |
| 603 | + .start(mock(Listener.class), new Metadata()); |
| 604 | + listener.getValue().onClose(Status.UNAVAILABLE, new Metadata()); |
| 605 | + } |
| 606 | + // Should NOT be recycled again because of backoff |
| 607 | + verify(channel, times(1)).shutdown(); |
| 608 | + verify(channelSupplier, times(2)).get(); |
| 609 | + |
| 610 | + // --- Third Recycle (After partial backoff, still blocked) --- |
| 611 | + // Advance time by 1ms (backoff is 2ms, so still blocked) |
| 612 | + time.addAndGet(TimeUnit.MILLISECONDS.toNanos(1)); |
| 613 | + |
| 614 | + for (int i = 0; i < 5; i++) { |
| 615 | + pool.newStream(FakeSessionGrpc.getOpenSessionMethod(), CallOptions.DEFAULT) |
| 616 | + .start(mock(Listener.class), new Metadata()); |
| 617 | + listener.getValue().onClose(Status.UNAVAILABLE, new Metadata()); |
| 618 | + } |
| 619 | + // Should still NOT be recycled |
| 620 | + verify(channel, times(1)).shutdown(); |
| 621 | + verify(channelSupplier, times(2)).get(); |
| 622 | + |
| 623 | + // --- Fourth Recycle (After full backoff) --- |
| 624 | + // Advance time by another 2ms (total 3ms from last recycle, which is > 2ms backoff) |
| 625 | + time.addAndGet(TimeUnit.MILLISECONDS.toNanos(2)); |
| 626 | + |
| 627 | + for (int i = 0; i < 5; i++) { |
| 628 | + pool.newStream(FakeSessionGrpc.getOpenSessionMethod(), CallOptions.DEFAULT) |
| 629 | + .start(mock(Listener.class), new Metadata()); |
| 630 | + listener.getValue().onClose(Status.UNAVAILABLE, new Metadata()); |
| 631 | + } |
| 632 | + // Now it should be recycled again |
| 633 | + verify(channel, times(2)).shutdown(); |
| 634 | + verify(channelSupplier, times(3)).get(); |
| 635 | + |
| 636 | + pool.close(); |
| 637 | + } |
472 | 638 | } |
0 commit comments