Skip to content

Commit 26b06f9

Browse files
fix(gax): Remove strict validation that resize delta must be less than max channel count (#12863)
Address some comments in #12838 It is possible that users use the DEFAULT_RESIZE_DELTA (2) with a statically sized channel pool (1). We should not enforce that the resize delta needs to be less than or equal to the channel pool. Instead, the channel pool's tentative target is calculated to be clamped within the channelpoolsetting's configured bounds. --------- Co-authored-by: cloud-java-bot <cloud-java-bot@google.com>
1 parent 4ba6f86 commit 26b06f9

3 files changed

Lines changed: 171 additions & 99 deletions

File tree

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

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,7 @@ class ChannelPool extends ManagedChannel {
9696

9797
// Tracks the number of consecutive resize cycles where a resize actually occurred (either expand
9898
// or shrink). Used to detect repeated resizing activity and log a warning.
99-
// Note: This field is only accessed safely within resizeSafely() and does not need to be atomic.
100-
private int consecutiveResizes = 0;
99+
private final AtomicInteger consecutiveResizes = new AtomicInteger(0);
101100

102101
static ChannelPool create(
103102
ChannelPoolSettings settings,
@@ -304,61 +303,82 @@ void resize() {
304303
localEntries.stream().mapToInt(Entry::getAndResetMaxOutstanding).sum();
305304

306305
// Number of channels if each channel operated at max capacity
307-
int minChannels =
306+
int calculatedResizeMinChannels =
308307
(int) Math.ceil(actualOutstandingRpcs / (double) settings.getMaxRpcsPerChannel());
309308
// Limit the threshold to absolute range
310-
if (minChannels < settings.getMinChannelCount()) {
311-
minChannels = settings.getMinChannelCount();
309+
if (calculatedResizeMinChannels < settings.getMinChannelCount()) {
310+
calculatedResizeMinChannels = settings.getMinChannelCount();
311+
}
312+
// Limit in case the calculated min channel count exceeds the configured max channel count
313+
if (calculatedResizeMinChannels > settings.getMaxChannelCount()) {
314+
calculatedResizeMinChannels = settings.getMaxChannelCount();
312315
}
313316

314317
// Number of channels if each channel operated at minimum capacity
315318
// Note: getMinRpcsPerChannel() can return 0, but division by 0 shouldn't cause a problem.
316-
int maxChannels =
319+
int calculatedResizeMaxChannels =
317320
(int) Math.ceil(actualOutstandingRpcs / (double) settings.getMinRpcsPerChannel());
318321
// Limit the threshold to absolute range
319-
if (maxChannels > settings.getMaxChannelCount()) {
320-
maxChannels = settings.getMaxChannelCount();
322+
if (calculatedResizeMaxChannels > settings.getMaxChannelCount()) {
323+
calculatedResizeMaxChannels = settings.getMaxChannelCount();
321324
}
322-
if (maxChannels < minChannels) {
323-
maxChannels = minChannels;
325+
// Limit in case the calculated max channel count falls below the configured min channel count
326+
if (calculatedResizeMaxChannels < settings.getMinChannelCount()) {
327+
calculatedResizeMaxChannels = settings.getMinChannelCount();
324328
}
325329

326-
// If the pool were to be resized, try to aim for the middle of the bound, but limit rate of
327-
// change.
328-
int tentativeTarget = (maxChannels + minChannels) / 2;
330+
// If the pool were to be resized, try to aim for the middle of the bound. The tentativeTarget
331+
// is guaranteed to be between configured min and max channel bounds
332+
int tentativeTarget =
333+
calculatedResizeMinChannels
334+
+ (calculatedResizeMaxChannels - calculatedResizeMinChannels) / 2;
329335
int currentSize = localEntries.size();
336+
337+
// Calculate the desired change in pool size.
330338
int delta = tentativeTarget - currentSize;
339+
340+
// Dampen the rate of change if the desired delta exceeds the maximum allowed step size.
341+
// Ensure that the step size is capped by the max channel count.
342+
// Note: resize delta value is not enforced to be smaller than max channel count in
343+
// ChannelPoolSettings as DEFAULT_RESIZE_DELTA is 2 and max channel pool count can be 1
331344
int dampenedTarget = tentativeTarget;
332-
if (Math.abs(delta) > settings.getMaxResizeDelta()) {
333-
dampenedTarget = currentSize + (int) Math.copySign(settings.getMaxResizeDelta(), delta);
345+
int effectiveMaxResizeDelta =
346+
Math.min(settings.getMaxResizeDelta(), settings.getMaxChannelCount());
347+
348+
// Rate-limit the change to not exceed the effectiveMaxResizeDelta
349+
if (Math.abs(delta) > effectiveMaxResizeDelta) {
350+
// Maintaining the correct direction (positive or negative) to handle expand/shrink
351+
int step = delta > 0 ? effectiveMaxResizeDelta : -effectiveMaxResizeDelta;
352+
dampenedTarget = currentSize + step;
334353
}
335354

336355
// Only count as "resized" if the thresholds are crossed and Gax attempts to scale. Checking
337356
// that `dampenedTarget != currentSize` would cause false positives when the pool is within
338357
// bounds but not at the target (target aims for the middle of the bounds)
339-
boolean resized = (currentSize < minChannels || currentSize > maxChannels);
358+
boolean resized =
359+
(currentSize < calculatedResizeMinChannels || currentSize > calculatedResizeMaxChannels);
340360
if (resized) {
341-
consecutiveResizes++;
361+
consecutiveResizes.incrementAndGet();
342362
} else {
343-
consecutiveResizes = 0;
363+
consecutiveResizes.set(0);
344364
}
345365

346366
// Log warning only once when the consecutive threshold is reached to avoid spamming logs. Log
347367
// message will repeat if the number of consecutive resizes resets (e.g. stabilizes for a bit).
348368
// However, aim to log once to ensure that this does not incur log spam.
349-
if (consecutiveResizes == CONSECUTIVE_RESIZE_THRESHOLD) {
369+
if (consecutiveResizes.get() == CONSECUTIVE_RESIZE_THRESHOLD) {
350370
LOG.warning(CHANNEL_POOL_CONSECUTIVE_RESIZING_WARNING);
351371
}
352372

353373
// Only resize the pool when thresholds are crossed
354-
if (localEntries.size() < minChannels) {
374+
if (localEntries.size() < calculatedResizeMinChannels) {
355375
LOG.fine(
356376
String.format(
357377
"Detected throughput peak of %d, expanding channel pool size: %d -> %d.",
358378
actualOutstandingRpcs, currentSize, dampenedTarget));
359379

360380
expand(dampenedTarget);
361-
} else if (localEntries.size() > maxChannels) {
381+
} else if (localEntries.size() > calculatedResizeMaxChannels) {
362382
LOG.fine(
363383
String.format(
364384
"Detected throughput drop to %d, shrinking channel pool size: %d -> %d.",

sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/ChannelPoolSettings.java

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,8 @@ public static ChannelPoolSettings staticallySized(int size) {
148148
.setMaxRpcsPerChannel(Integer.MAX_VALUE)
149149
.setMinChannelCount(size)
150150
.setMaxChannelCount(size)
151-
// Static pools don't resize so this value doesn't affect operation. However,
152-
// validation still checks that resize delta doesn't exceed channel pool size.
153-
.setMaxResizeDelta(Math.min(DEFAULT_MAX_RESIZE_DELTA, size))
151+
// Static pools don't resize so this value doesn't affect operation.
152+
.setMaxResizeDelta(DEFAULT_MAX_RESIZE_DELTA)
154153
.build();
155154
}
156155

@@ -167,21 +166,57 @@ public static Builder builder() {
167166

168167
@AutoValue.Builder
169168
public abstract static class Builder {
169+
/**
170+
* Sets the minimum desired number of concurrent RPCs per channel.
171+
*
172+
* <p>This ensures channels are adequately utilized. If the average load per channel falls below
173+
* this value, the pool attempts to shrink. The resulting target channel count is a dynamic
174+
* value determined by load and is bounded by {@link #setMinChannelCount} and {@link
175+
* #setMaxChannelCount}.
176+
*/
170177
public abstract Builder setMinRpcsPerChannel(int count);
171178

179+
/**
180+
* Sets the maximum desired number of concurrent RPCs per channel.
181+
*
182+
* <p>This ensures channels do not become overloaded. If the average load per channel exceeds
183+
* this value, the pool attempts to expand. The resulting target channel count is a dynamic
184+
* value determined by load and is bounded by {@link #setMinChannelCount} and {@link
185+
* #setMaxChannelCount}.
186+
*/
172187
public abstract Builder setMaxRpcsPerChannel(int count);
173188

189+
/**
190+
* Sets the minimum number of channels the pool can shrink to.
191+
*
192+
* <p>When resizing, if the calculated resize bounds fall below this minimum configuration, the
193+
* bounds will be clamped to this value. This ensures the pool never shrinks below this absolute
194+
* minimum, even under very low load.
195+
*/
174196
public abstract Builder setMinChannelCount(int count);
175197

198+
/**
199+
* Sets the maximum number of channels the pool can expand to.
200+
*
201+
* <p>When resizing, if the calculated resize bounds exceed this maximum configuration, the
202+
* bounds will be clamped to this value. This ensures the pool never expands above this absolute
203+
* maximum, even under very high load.
204+
*/
176205
public abstract Builder setMaxChannelCount(int count);
177206

207+
/** Sets the initial number of channels in the pool. */
178208
public abstract Builder setInitialChannelCount(int count);
179209

210+
/**
211+
* Sets whether preemptive channel refresh is enabled to prevent channels from becoming idle.
212+
*/
180213
public abstract Builder setPreemptiveRefreshEnabled(boolean enabled);
181214

182215
/**
183216
* Sets the maximum number of channels that can be added or removed in a single resize cycle.
184-
* This acts as a rate limiter to prevent wild fluctuations.
217+
* This acts as a rate limiter to prevent wild fluctuations. The pool resizes periodically
218+
* according to {@link #RESIZE_INTERVAL} (default 1 minute). During resizing, this value is
219+
* effectively capped by the bound configured via {@link #setMaxChannelCount}.
185220
*
186221
* <p><b>Warning:</b> Higher values for resize delta may still result in performance degradation
187222
* during spikes due to rapid scaling.
@@ -198,7 +233,7 @@ public ChannelPoolSettings build() {
198233
Preconditions.checkState(
199234
s.getMinChannelCount() > 0, "Minimum channel count must be at least 1");
200235
Preconditions.checkState(
201-
s.getMinChannelCount() <= s.getMaxRpcsPerChannel(), "absolute channel range is invalid");
236+
s.getMinChannelCount() <= s.getMaxChannelCount(), "absolute channel range is invalid");
202237
Preconditions.checkState(
203238
s.getMinChannelCount() <= s.getInitialChannelCount(),
204239
"initial channel count be at least minChannelCount");
@@ -212,9 +247,6 @@ public ChannelPoolSettings build() {
212247
Preconditions.checkState(
213248
s.getMaxResizeDelta() <= MAX_ALLOWED_RESIZE_DELTA,
214249
"Max resize delta cannot be greater than " + MAX_ALLOWED_RESIZE_DELTA);
215-
Preconditions.checkState(
216-
s.getMaxResizeDelta() <= s.getMaxChannelCount(),
217-
"Max resize delta cannot be greater than max channel count");
218250
return s;
219251
}
220252
}

0 commit comments

Comments
 (0)