Skip to content
This repository was archived by the owner on May 8, 2026. It is now read-only.

Commit 20e012d

Browse files
committed
feat/idle-channel-eviction
Change-Id: I62fe152c293438bf64b657b5b1fe795e22ce9c85
1 parent e8007fa commit 20e012d

3 files changed

Lines changed: 437 additions & 4 deletions

File tree

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import com.google.api.core.InternalApi;
1919
import com.google.api.gax.grpc.ChannelFactory;
2020
import com.google.api.gax.grpc.ChannelPrimer;
21+
import com.google.cloud.bigtable.data.v2.stub.BigtableChannelPrimer;
22+
import com.google.cloud.bigtable.gaxx.grpc.ChannelPoolHealthChecker.ProbeResult;
2123
import com.google.common.annotations.VisibleForTesting;
2224
import com.google.common.base.Preconditions;
2325
import com.google.common.collect.ImmutableList;
@@ -31,9 +33,11 @@
3133
import io.grpc.MethodDescriptor;
3234
import io.grpc.Status;
3335
import java.io.IOException;
36+
import java.time.Clock;
3437
import java.util.ArrayList;
3538
import java.util.List;
3639
import java.util.concurrent.CancellationException;
40+
import java.util.concurrent.ConcurrentLinkedQueue;
3741
import java.util.concurrent.Executors;
3842
import java.util.concurrent.ScheduledExecutorService;
3943
import java.util.concurrent.TimeUnit;
@@ -62,11 +66,11 @@ public class BigtableChannelPool extends ManagedChannel {
6266
private final BigtableChannelPoolSettings settings;
6367
private final ChannelFactory channelFactory;
6468

65-
private final ChannelPrimer channelPrimer;
69+
private ChannelPrimer channelPrimer;
6670
private final ScheduledExecutorService executor;
67-
6871
private final Object entryWriteLock = new Object();
6972
@VisibleForTesting final AtomicReference<ImmutableList<Entry>> entries = new AtomicReference<>();
73+
private ChannelPoolHealthChecker channelPoolHealthChecker;
7074
private final AtomicInteger indexTicker = new AtomicInteger();
7175
private final String authority;
7276

@@ -96,6 +100,11 @@ public static BigtableChannelPool create(
96100
this.settings = settings;
97101
this.channelFactory = channelFactory;
98102
this.channelPrimer = channelPrimer;
103+
Clock systemClock = Clock.systemUTC();
104+
this.channelPoolHealthChecker =
105+
new ChannelPoolHealthChecker(
106+
() -> entries.get(), (BigtableChannelPrimer) channelPrimer, executor, systemClock);
107+
this.channelPoolHealthChecker.start();
99108

100109
ImmutableList.Builder<Entry> initialListBuilder = ImmutableList.builder();
101110

@@ -445,15 +454,27 @@ static class Entry {
445454

446455
private final AtomicInteger maxOutstanding = new AtomicInteger();
447456

457+
@VisibleForTesting
458+
final ConcurrentLinkedQueue<ProbeResult> probeHistory = new ConcurrentLinkedQueue<>();
459+
460+
// we keep both so that we don't have to check size() on the ConcurrentLinkedQueue all the time
461+
AtomicInteger failedProbesInWindow = new AtomicInteger();
462+
AtomicInteger successfulProbesInWindow = new AtomicInteger();
463+
448464
// Flag that the channel should be closed once all of the outstanding RPC complete.
449465
private final AtomicBoolean shutdownRequested = new AtomicBoolean();
450466
// Flag that the channel has been closed.
451467
private final AtomicBoolean shutdownInitiated = new AtomicBoolean();
452468

453-
private Entry(ManagedChannel channel) {
469+
@VisibleForTesting
470+
Entry(ManagedChannel channel) {
454471
this.channel = channel;
455472
}
456473

474+
ManagedChannel getManagedChannel() {
475+
return this.channel;
476+
}
477+
457478
int getAndResetMaxOutstanding() {
458479
return maxOutstanding.getAndSet(outstandingRpcs.get());
459480
}
@@ -468,7 +489,7 @@ private boolean retain() {
468489
// register desire to start RPC
469490
int currentOutstanding = outstandingRpcs.incrementAndGet();
470491

471-
// Rough book keeping
492+
// Rough bookkeeping
472493
int prevMax = maxOutstanding.get();
473494
if (currentOutstanding > prevMax) {
474495
maxOutstanding.incrementAndGet();
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.gaxx.grpc;
17+
18+
import com.google.api.core.SettableApiFuture;
19+
import com.google.bigtable.v2.PingAndWarmResponse;
20+
import com.google.cloud.bigtable.data.v2.stub.BigtableChannelPrimer;
21+
import com.google.cloud.bigtable.gaxx.grpc.BigtableChannelPool.Entry;
22+
import com.google.common.annotations.VisibleForTesting;
23+
import com.google.common.collect.ImmutableList;
24+
import java.time.Clock;
25+
import java.time.Duration;
26+
import java.time.Instant;
27+
import java.util.Comparator;
28+
import java.util.List;
29+
import java.util.concurrent.ScheduledExecutorService;
30+
import java.util.concurrent.ThreadLocalRandom;
31+
import java.util.concurrent.TimeUnit;
32+
import java.util.function.Supplier;
33+
import java.util.stream.Collectors;
34+
import javax.annotation.Nullable;
35+
36+
/** Stub for a class that will manage the health checking in the BigtableChannelPool */
37+
public class ChannelPoolHealthChecker {
38+
39+
// Configuration constants
40+
private static final Duration WINDOW_DURATION = Duration.ofMinutes(5);
41+
static final Duration PROBE_RATE = Duration.ofSeconds(30);
42+
@VisibleForTesting static final Duration PROBE_DEADLINE = Duration.ofMillis(500);
43+
private static final Duration MIN_EVICTION_INTERVAL = Duration.ofMinutes(10);
44+
private static final int MIN_PROBES_FOR_EVALUATION = 4;
45+
private static final int SINGLE_CHANNEL_FAILURE_PERCENT_THRESHOLD = 60;
46+
private static final int POOLWIDE_BAD_CHANNEL_CIRCUITBREAKER_PERCENT = 70;
47+
48+
/** Inner class to represent the result of a single probe. */
49+
static class ProbeResult {
50+
final Instant startTime;
51+
final boolean success;
52+
53+
ProbeResult(Instant startTime, boolean success) {
54+
this.startTime = startTime;
55+
this.success = success;
56+
}
57+
58+
public boolean isSuccessful() {
59+
return success;
60+
}
61+
}
62+
63+
// Class fields
64+
private final Supplier<ImmutableList<Entry>> entrySupplier;
65+
private Instant lastEviction;
66+
private ScheduledExecutorService executor;
67+
68+
private BigtableChannelPrimer channelPrimer;
69+
70+
private final Clock clock;
71+
72+
/** Constructor for the pool health checker. */
73+
public ChannelPoolHealthChecker(
74+
Supplier<ImmutableList<Entry>> entrySupplier,
75+
BigtableChannelPrimer channelPrimer,
76+
ScheduledExecutorService executor,
77+
Clock clock) {
78+
this.entrySupplier = entrySupplier;
79+
this.lastEviction = Instant.MIN;
80+
this.channelPrimer = channelPrimer;
81+
this.executor = executor;
82+
this.clock = clock;
83+
}
84+
85+
void start() {
86+
Duration initialDelayProbe =
87+
Duration.ofMillis(ThreadLocalRandom.current().nextLong(PROBE_RATE.toMillis()));
88+
executor.scheduleAtFixedRate(
89+
this::runProbes,
90+
initialDelayProbe.toMillis(),
91+
PROBE_RATE.toMillis(),
92+
TimeUnit.MILLISECONDS);
93+
Duration initialDelayDetect =
94+
Duration.ofMillis(ThreadLocalRandom.current().nextLong(PROBE_RATE.toMillis()));
95+
executor.scheduleAtFixedRate(
96+
this::detectAndRemoveOutlierEntries,
97+
initialDelayDetect.toMillis(),
98+
PROBE_RATE.toMillis(),
99+
TimeUnit.MILLISECONDS);
100+
}
101+
102+
/** Stop running health checking (No-op stub) */
103+
public void stop() {
104+
executor.shutdownNow();
105+
}
106+
107+
/** Runs probes on all the channels in the pool. */
108+
@VisibleForTesting
109+
void runProbes() {
110+
// Method stub, no operation.
111+
for (Entry entry : this.entrySupplier.get()) {
112+
Instant startTime = clock.instant();
113+
SettableApiFuture<PingAndWarmResponse> probeFuture =
114+
channelPrimer.sendPrimeRequestsAsync(entry.getManagedChannel());
115+
probeFuture.addListener(() -> onComplete(entry, startTime, probeFuture), executor);
116+
}
117+
}
118+
119+
/** Callback that will update Entry data on probe complete. */
120+
@VisibleForTesting
121+
void onComplete(
122+
Entry entry, Instant startTime, SettableApiFuture<PingAndWarmResponse> probeFuture) {
123+
boolean success;
124+
try {
125+
probeFuture.get(PROBE_DEADLINE.toMillis(), TimeUnit.MILLISECONDS);
126+
success = true;
127+
} catch (Exception e) {
128+
success = false;
129+
}
130+
addProbeResult(entry, new ProbeResult(startTime, success));
131+
}
132+
133+
@VisibleForTesting
134+
void addProbeResult(Entry entry, ProbeResult result) {
135+
entry.probeHistory.add(result);
136+
if (result.isSuccessful()) {
137+
entry.successfulProbesInWindow.incrementAndGet();
138+
} else {
139+
entry.failedProbesInWindow.incrementAndGet();
140+
}
141+
}
142+
143+
@VisibleForTesting
144+
void pruneHistoryFor(Entry entry) {
145+
Instant windowStart = clock.instant().minus(WINDOW_DURATION);
146+
while (!entry.probeHistory.isEmpty()
147+
&& entry.probeHistory.peek().startTime.isBefore(windowStart)) {
148+
ProbeResult removedResult = entry.probeHistory.poll();
149+
if (removedResult.isSuccessful()) {
150+
entry.successfulProbesInWindow.decrementAndGet();
151+
} else {
152+
entry.failedProbesInWindow.decrementAndGet();
153+
}
154+
}
155+
}
156+
157+
/** Checks if a single entry is currently healthy based on its probe history. */
158+
@VisibleForTesting
159+
boolean isEntryHealthy(Entry entry) {
160+
pruneHistoryFor(entry); // Ensure window is current before calculation
161+
162+
int failedProbes = entry.failedProbesInWindow.get();
163+
int totalProbes = failedProbes + entry.successfulProbesInWindow.get();
164+
165+
if (totalProbes < MIN_PROBES_FOR_EVALUATION) {
166+
return true; // Not enough data, assume healthy.
167+
}
168+
169+
double failureRate = ((double) failedProbes / totalProbes) * 100.0;
170+
return failureRate < SINGLE_CHANNEL_FAILURE_PERCENT_THRESHOLD;
171+
}
172+
173+
/**
174+
* Finds a channel that is an outlier in terms of health.
175+
*
176+
* @return Entry
177+
*/
178+
@Nullable
179+
@VisibleForTesting
180+
Entry findOutlierEntry() {
181+
if (lastEviction.plus(WINDOW_DURATION).isAfter(clock.instant())) {
182+
return null;
183+
}
184+
185+
List<Entry> unhealthyEntries =
186+
this.entrySupplier.get().stream()
187+
.peek(this::pruneHistoryFor)
188+
.filter(entry -> !isEntryHealthy(entry))
189+
.collect(Collectors.toList());
190+
191+
int poolSize = this.entrySupplier.get().size();
192+
if (unhealthyEntries.isEmpty() || poolSize == 0) {
193+
return null;
194+
}
195+
196+
// If more than CIRCUITBREAKER_PERCENT of channels are unhealthy we won't evict
197+
double unhealthyPercent = (double) unhealthyEntries.size() / poolSize * 100.0;
198+
if (unhealthyPercent >= POOLWIDE_BAD_CHANNEL_CIRCUITBREAKER_PERCENT) {
199+
return null;
200+
}
201+
202+
return unhealthyEntries.stream()
203+
.max(Comparator.comparingInt(entry -> entry.failedProbesInWindow.get()))
204+
.orElse(null);
205+
}
206+
207+
/** Periodically detects and removes outlier channels from the pool. (No-op stub) */
208+
@VisibleForTesting
209+
void detectAndRemoveOutlierEntries() {
210+
if (clock.instant().isBefore(lastEviction.plus(MIN_EVICTION_INTERVAL))) {
211+
// Primitive but effective rate-limiting.
212+
return;
213+
}
214+
Entry outlier = findOutlierEntry();
215+
if (outlier != null) {
216+
this.lastEviction = clock.instant();
217+
outlier.getManagedChannel().enterIdle();
218+
}
219+
}
220+
}

0 commit comments

Comments
 (0)