Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Sep 26, 2023. It is now read-only.

Commit db3b3f4

Browse files
introduce dynamic channel pool
1 parent d2d7830 commit db3b3f4

File tree

8 files changed

+625
-151
lines changed

8 files changed

+625
-151
lines changed

gax-grpc/src/main/java/com/google/api/gax/grpc/ChannelPool.java

Lines changed: 185 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import com.google.api.core.InternalApi;
3333
import com.google.common.annotations.VisibleForTesting;
34+
import com.google.common.base.Preconditions;
3435
import com.google.common.collect.ImmutableList;
3536
import io.grpc.CallOptions;
3637
import io.grpc.Channel;
@@ -46,14 +47,12 @@
4647
import java.util.List;
4748
import java.util.concurrent.Executors;
4849
import java.util.concurrent.ScheduledExecutorService;
49-
import java.util.concurrent.ScheduledFuture;
5050
import java.util.concurrent.TimeUnit;
5151
import java.util.concurrent.atomic.AtomicBoolean;
5252
import java.util.concurrent.atomic.AtomicInteger;
5353
import java.util.concurrent.atomic.AtomicReference;
5454
import java.util.logging.Level;
5555
import java.util.logging.Logger;
56-
import javax.annotation.Nullable;
5756
import org.threeten.bp.Duration;
5857

5958
/**
@@ -68,22 +67,16 @@
6867
*/
6968
class ChannelPool extends ManagedChannel {
7069
private static final Logger LOG = Logger.getLogger(ChannelPool.class.getName());
71-
72-
// size greater than 1 to allow multiple channel to refresh at the same time
73-
// size not too large so refreshing channels doesn't use too many threads
74-
private static final int CHANNEL_REFRESH_EXECUTOR_SIZE = 2;
7570
private static final Duration REFRESH_PERIOD = Duration.ofMinutes(50);
76-
private static final double JITTER_PERCENTAGE = 0.15;
71+
72+
private final ChannelPoolSettings settings;
73+
private final ChannelFactory channelFactory;
74+
private final ScheduledExecutorService executor;
7775

7876
private final Object entryWriteLock = new Object();
7977
private final AtomicReference<ImmutableList<Entry>> entries = new AtomicReference<>();
8078
private final AtomicInteger indexTicker = new AtomicInteger();
8179
private final String authority;
82-
// if set, ChannelPool will manage the life cycle of channelRefreshExecutorService
83-
@Nullable private final ScheduledExecutorService channelRefreshExecutorService;
84-
private final ChannelFactory channelFactory;
85-
86-
private volatile ScheduledFuture<?> nextScheduledRefresh = null;
8780

8881
/**
8982
* Factory method to create a non-refreshing channel pool
@@ -92,8 +85,9 @@ class ChannelPool extends ManagedChannel {
9285
* @param channelFactory method to create the channels
9386
* @return ChannelPool of non-refreshing channels
9487
*/
88+
@VisibleForTesting
9589
static ChannelPool create(int poolSize, ChannelFactory channelFactory) throws IOException {
96-
return new ChannelPool(channelFactory, poolSize, null);
90+
return new ChannelPool(ChannelPoolSettings.staticallySized(poolSize), channelFactory, null);
9791
}
9892

9993
/**
@@ -103,58 +97,66 @@ static ChannelPool create(int poolSize, ChannelFactory channelFactory) throws IO
10397
*
10498
* @param poolSize number of channels in the pool
10599
* @param channelFactory method to create the channels
106-
* @param channelRefreshExecutorService periodically refreshes the channels; its life cycle will
107-
* be managed by ChannelPool
100+
* @param executor used to schedule maintenance tasks like refresh channels and resizing the pool.
108101
* @return ChannelPool of refreshing channels
109102
*/
110103
@VisibleForTesting
111104
static ChannelPool createRefreshing(
112-
int poolSize,
113-
ChannelFactory channelFactory,
114-
ScheduledExecutorService channelRefreshExecutorService)
105+
int poolSize, ChannelFactory channelFactory, ScheduledExecutorService executor)
115106
throws IOException {
116-
return new ChannelPool(channelFactory, poolSize, channelRefreshExecutorService);
107+
return new ChannelPool(
108+
ChannelPoolSettings.staticallySized(poolSize)
109+
.toBuilder()
110+
.setPreemptiveReconnectEnabled(true)
111+
.build(),
112+
channelFactory,
113+
executor);
117114
}
118115

119-
/**
120-
* Factory method to create a refreshing channel pool
121-
*
122-
* @param poolSize number of channels in the pool
123-
* @param channelFactory method to create the channels
124-
* @return ChannelPool of refreshing channels
125-
*/
126-
static ChannelPool createRefreshing(int poolSize, final ChannelFactory channelFactory)
116+
static ChannelPool create(ChannelPoolSettings settings, ChannelFactory channelFactory)
127117
throws IOException {
128-
return createRefreshing(
129-
poolSize, channelFactory, Executors.newScheduledThreadPool(CHANNEL_REFRESH_EXECUTOR_SIZE));
118+
return new ChannelPool(settings, channelFactory, Executors.newSingleThreadScheduledExecutor());
130119
}
131120

132121
/**
133122
* Initializes the channel pool. Assumes that all channels have the same authority.
134123
*
124+
* @param settings options for controling the ChannelPool sizing behavior
135125
* @param channelFactory method to create the channels
136-
* @param poolSize number of channels in the pool
137-
* @param channelRefreshExecutorService periodically refreshes the channels
126+
* @param executor periodically refreshes the channels. Must be single threaded
138127
*/
139-
private ChannelPool(
128+
@InternalApi("VisibleForTesting")
129+
ChannelPool(
130+
ChannelPoolSettings settings,
140131
ChannelFactory channelFactory,
141-
int poolSize,
142-
@Nullable ScheduledExecutorService channelRefreshExecutorService)
132+
ScheduledExecutorService executor)
143133
throws IOException {
134+
this.settings = settings;
144135
this.channelFactory = channelFactory;
145136

146137
ImmutableList.Builder<Entry> initialListBuilder = ImmutableList.builder();
147138

148-
for (int i = 0; i < poolSize; i++) {
139+
for (int i = 0; i < settings.getInitialChannelCount(); i++) {
149140
initialListBuilder.add(new Entry(channelFactory.createSingleChannel()));
150141
}
151142

152143
entries.set(initialListBuilder.build());
153144
authority = entries.get().get(0).channel.authority();
154-
this.channelRefreshExecutorService = channelRefreshExecutorService;
155-
156-
if (channelRefreshExecutorService != null) {
157-
nextScheduledRefresh = scheduleNextRefresh();
145+
this.executor = executor;
146+
147+
if (!settings.isStaticSize()) {
148+
executor.scheduleAtFixedRate(
149+
this::resizeSafely,
150+
ChannelPoolSettings.RESIZE_INTERVAL.getSeconds(),
151+
ChannelPoolSettings.RESIZE_INTERVAL.getSeconds(),
152+
TimeUnit.SECONDS);
153+
}
154+
if (settings.isPreemptiveReconnectEnabled()) {
155+
executor.scheduleAtFixedRate(
156+
this::refreshSafely,
157+
REFRESH_PERIOD.getSeconds(),
158+
REFRESH_PERIOD.getSeconds(),
159+
TimeUnit.SECONDS);
158160
}
159161
}
160162

@@ -187,12 +189,9 @@ public ManagedChannel shutdown() {
187189
for (Entry entry : localEntries) {
188190
entry.channel.shutdown();
189191
}
190-
if (nextScheduledRefresh != null) {
191-
nextScheduledRefresh.cancel(true);
192-
}
193-
if (channelRefreshExecutorService != null) {
192+
if (executor != null) {
194193
// shutdownNow will cancel scheduled tasks
195-
channelRefreshExecutorService.shutdownNow();
194+
executor.shutdownNow();
196195
}
197196
return this;
198197
}
@@ -206,7 +205,7 @@ public boolean isShutdown() {
206205
return false;
207206
}
208207
}
209-
return channelRefreshExecutorService == null || channelRefreshExecutorService.isShutdown();
208+
return executor == null || executor.isShutdown();
210209
}
211210

212211
/** {@inheritDoc} */
@@ -218,7 +217,8 @@ public boolean isTerminated() {
218217
return false;
219218
}
220219
}
221-
return channelRefreshExecutorService == null || channelRefreshExecutorService.isTerminated();
220+
221+
return executor == null || executor.isTerminated();
222222
}
223223

224224
/** {@inheritDoc} */
@@ -228,11 +228,8 @@ public ManagedChannel shutdownNow() {
228228
for (Entry entry : localEntries) {
229229
entry.channel.shutdownNow();
230230
}
231-
if (nextScheduledRefresh != null) {
232-
nextScheduledRefresh.cancel(true);
233-
}
234-
if (channelRefreshExecutorService != null) {
235-
channelRefreshExecutorService.shutdownNow();
231+
if (executor != null) {
232+
executor.shutdownNow();
236233
}
237234
return this;
238235
}
@@ -249,25 +246,129 @@ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedE
249246
}
250247
entry.channel.awaitTermination(awaitTimeNanos, TimeUnit.NANOSECONDS);
251248
}
252-
if (channelRefreshExecutorService != null) {
249+
if (executor != null) {
253250
long awaitTimeNanos = endTimeNanos - System.nanoTime();
254-
channelRefreshExecutorService.awaitTermination(awaitTimeNanos, TimeUnit.NANOSECONDS);
251+
executor.awaitTermination(awaitTimeNanos, TimeUnit.NANOSECONDS);
255252
}
256253
return isTerminated();
257254
}
258255

259-
/** Scheduling loop. */
260-
private ScheduledFuture<?> scheduleNextRefresh() {
261-
long delayPeriod = REFRESH_PERIOD.toMillis();
262-
long jitter = (long) ((Math.random() - 0.5) * JITTER_PERCENTAGE * delayPeriod);
263-
long delay = jitter + delayPeriod;
264-
return channelRefreshExecutorService.schedule(
265-
() -> {
266-
scheduleNextRefresh();
267-
refresh();
268-
},
269-
delay,
270-
TimeUnit.MILLISECONDS);
256+
void resizeSafely() {
257+
try {
258+
synchronized (entryWriteLock) {
259+
resize();
260+
}
261+
} catch (Exception e) {
262+
LOG.log(Level.WARNING, "Failed to resize channel pool", e);
263+
}
264+
}
265+
266+
/**
267+
* Resize the number of channels based on the number of outstanding RPCs.
268+
*
269+
* <p>This method is expected to be called on a fixed interval. On every invocation it will:
270+
*
271+
* <ul>
272+
* <li>Get the maximum number of outstanding RPCs since last invocation
273+
* <li>Determine a valid range of number of channels to handle that many outstanding RPCs
274+
* <li>If the current number of channel falls outside of that range, add or remove at most
275+
* {@link ChannelPoolSettings#MAX_RESIZE_DELTA} to get closer to middle of that range.
276+
* </ul>
277+
*
278+
* <p>Not threadsafe, must be called under the entryWriteLock monitor
279+
*/
280+
void resize() {
281+
List<Entry> localEntries = entries.get();
282+
// Estimate the peak of RPCs in the last interval by summing the peak of RPCs per channel
283+
int actualOutstandingRpcs =
284+
localEntries.stream().mapToInt(Entry::getAndResetMaxOutstanding).sum();
285+
286+
// Number of channels if each channel operated at max capacity
287+
int minChannels =
288+
(int) Math.ceil(actualOutstandingRpcs / (double) settings.getMaxRpcsPerChannel());
289+
// Limit the threshold to absolute range
290+
if (minChannels < settings.getMinChannelCount()) {
291+
minChannels = settings.getMinChannelCount();
292+
}
293+
294+
// Number of channels if each channel operated at minimum capacity
295+
int maxChannels =
296+
(int) Math.ceil(actualOutstandingRpcs / (double) settings.getMinRpcsPerChannel());
297+
// Limit the threshold to absolute range
298+
if (maxChannels > settings.getMaxChannelCount()) {
299+
maxChannels = settings.getMaxChannelCount();
300+
}
301+
if (maxChannels < minChannels) {
302+
maxChannels = minChannels;
303+
}
304+
305+
// If the pool were to be resized, try to aim for the middle of the bound, but limit rate of
306+
// change.
307+
int tentativeTarget = (maxChannels + minChannels) / 2;
308+
int currentSize = localEntries.size();
309+
int delta = tentativeTarget - currentSize;
310+
int dampenedTarget = tentativeTarget;
311+
if (Math.abs(delta) > ChannelPoolSettings.MAX_RESIZE_DELTA) {
312+
dampenedTarget =
313+
currentSize + (int) Math.copySign(ChannelPoolSettings.MAX_RESIZE_DELTA, delta);
314+
}
315+
316+
// Only resize the pool when thresholds are crossed
317+
if (localEntries.size() < minChannels) {
318+
LOG.fine(
319+
String.format(
320+
"Detected throughput peak of %d, expanding channel pool size: %d -> %d.",
321+
actualOutstandingRpcs, currentSize, dampenedTarget));
322+
323+
expand(tentativeTarget);
324+
} else if (localEntries.size() > maxChannels) {
325+
LOG.fine(
326+
String.format(
327+
"Detected throughput drop to %d, shrinking channel pool size: %d -> %d.",
328+
actualOutstandingRpcs, currentSize, dampenedTarget));
329+
330+
shrink(tentativeTarget);
331+
}
332+
}
333+
334+
/** Not threadsafe, must be called under the entryWriteLock monitor */
335+
private void shrink(int desiredSize) {
336+
List<Entry> localEntries = entries.get();
337+
Preconditions.checkState(
338+
localEntries.size() >= desiredSize, "desired size is already smaller than the current");
339+
340+
// Set the new list
341+
entries.set(ImmutableList.copyOf(localEntries.subList(0, desiredSize)));
342+
// clean up removed entries
343+
List<Entry> removed = localEntries.subList(desiredSize, localEntries.size());
344+
removed.forEach(Entry::requestShutdown);
345+
}
346+
347+
/** Not threadsafe, must be called under the entryWriteLock monitor */
348+
private void expand(int desiredSize) {
349+
List<Entry> localEntries = entries.get();
350+
Preconditions.checkState(
351+
localEntries.size() <= desiredSize, "desired size is already bigger than the current");
352+
353+
ImmutableList.Builder<Entry> newEntries = ImmutableList.<Entry>builder().addAll(localEntries);
354+
355+
for (int i = 0; i < desiredSize - localEntries.size(); i++) {
356+
try {
357+
newEntries.add(new Entry(channelFactory.createSingleChannel()));
358+
} catch (IOException e) {
359+
LOG.log(Level.WARNING, "Failed to add channel", e);
360+
}
361+
}
362+
363+
entries.set(newEntries.build());
364+
}
365+
366+
private void refreshSafely() {
367+
try {
368+
refresh();
369+
} catch (Exception e) {
370+
LOG.log(Level.WARNING, "Failed to pre-emptively refresh channnels", e);
371+
}
271372
}
272373

273374
/**
@@ -340,14 +441,21 @@ Entry getRetainedEntry(int affinity) {
340441
private Entry getEntry(int affinity) {
341442
List<Entry> localEntries = entries.get();
342443

343-
int index = Math.abs(affinity % localEntries.size());
444+
int index = affinity % localEntries.size();
445+
index = Math.abs(index);
446+
// If index is the most negative int, abs(index) is still negative.
447+
if (index < 0) {
448+
index = 0;
449+
}
450+
344451
return localEntries.get(index);
345452
}
346453

347454
/** Bundles a gRPC {@link ManagedChannel} with some usage accounting. */
348455
private static class Entry {
349456
private final ManagedChannel channel;
350457
private final AtomicInteger outstandingRpcs = new AtomicInteger(0);
458+
private final AtomicInteger maxOutstanding = new AtomicInteger();
351459

352460
// Flag that the channel should be closed once all of the outstanding RPC complete.
353461
private final AtomicBoolean shutdownRequested = new AtomicBoolean();
@@ -358,6 +466,10 @@ private Entry(ManagedChannel channel) {
358466
this.channel = channel;
359467
}
360468

469+
int getAndResetMaxOutstanding() {
470+
return maxOutstanding.getAndSet(outstandingRpcs.get());
471+
}
472+
361473
/**
362474
* Try to increment the outstanding RPC count. The method will return false if the channel is
363475
* closing and the caller should pick a different channel. If the method returned true, the
@@ -366,7 +478,13 @@ private Entry(ManagedChannel channel) {
366478
*/
367479
private boolean retain() {
368480
// register desire to start RPC
369-
outstandingRpcs.incrementAndGet();
481+
int currentOutstanding = outstandingRpcs.incrementAndGet();
482+
483+
// Rough book keeping
484+
int prevMax = maxOutstanding.get();
485+
if (currentOutstanding > prevMax) {
486+
maxOutstanding.incrementAndGet();
487+
}
370488

371489
// abort if the channel is closing
372490
if (shutdownRequested.get()) {

0 commit comments

Comments
 (0)