|
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. |
@@ -563,4 +564,75 @@ void testCancelledDoesNotIncrementFailures() { |
563 | 564 |
|
564 | 565 | pool.close(); |
565 | 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 | + } |
566 | 638 | } |
0 commit comments