Skip to content

Commit 95686a4

Browse files
committed
feat(grpc-gcp): penalize retryable channel errors
1 parent bbbd18c commit 95686a4

4 files changed

Lines changed: 626 additions & 20 deletions

File tree

grpc-gcp-java/src/main/java/com/google/cloud/grpc/GcpManagedChannel.java

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ public class GcpManagedChannel extends ManagedChannel {
8787
private static final Logger logger = Logger.getLogger(GcpManagedChannel.class.getName());
8888
static final AtomicInteger channelPoolIndex = new AtomicInteger();
8989

90+
@FunctionalInterface
91+
interface NanoClock {
92+
long get();
93+
}
94+
9095
// Counter for tracking channel ids.
9196
final AtomicInteger nextChannelId = new AtomicInteger();
9297
static final int DEFAULT_MAX_CHANNEL = 10;
@@ -173,6 +178,8 @@ private static int stateFromChannelId(int channelId) {
173178
private int maxScaleUpPercent = 30;
174179
private int maxScaleDownChannels = 2;
175180
private Duration drainIdleGrace = Duration.ofMinutes(1);
181+
private int errorPenaltyStep = 5;
182+
private Duration errorPenaltyDuration = Duration.ofSeconds(5);
176183
private boolean isDynamicScalingEnabled = false;
177184
private int maxConcurrentStreamsLowWatermark = DEFAULT_MAX_STREAM;
178185
private GcpManagedChannelOptions.ChannelPickStrategy channelPickStrategy =
@@ -202,6 +209,7 @@ private static int stateFromChannelId(int channelId) {
202209
// One-slot scale-up signal. At most one worker mutates pool size at a time.
203210
private final AtomicBoolean scaleUpSignalPending = new AtomicBoolean();
204211
private final AtomicBoolean scaleUpWorkerRunning = new AtomicBoolean();
212+
private final AtomicLong totalErrorPenaltyLoad = new AtomicLong();
205213

206214
private volatile long lastScaleUpNanos = Long.MIN_VALUE;
207215
private int consecutiveLowLoadChecks;
@@ -295,14 +303,14 @@ private static int stateFromChannelId(int channelId) {
295303
private AtomicLong scaleDownCount = new AtomicLong();
296304

297305
// Clock supplier for nanoTime, injectable for testing.
298-
private Supplier<Long> nanoClock = System::nanoTime;
306+
private NanoClock nanoClock = System::nanoTime;
299307
private IntUnaryOperator candidateIndexPicker =
300308
bound -> ThreadLocalRandom.current().nextInt(bound);
301309
@Nullable private volatile Consumer<ChannelRef> pickerValidationHookForTest;
302310
@Nullable private volatile Runnable inactiveMappingRemovalHookForTest;
303311

304312
@VisibleForTesting
305-
void setNanoClock(Supplier<Long> nanoClock) {
313+
void setNanoClock(NanoClock nanoClock) {
306314
this.nanoClock = nanoClock;
307315
}
308316

@@ -467,6 +475,9 @@ void checkScaleDown() {
467475
int removeCount = Math.min(maxScaleDownChannels, Math.max(0, channelCount - desiredSize));
468476
removedChannels = removeChannels(removeCount);
469477
}
478+
for (ChannelRef channelRef : removedChannels) {
479+
channelRef.clearErrorPenalty();
480+
}
470481
List<String> keysToUnbind =
471482
affinityKeyToChannelRef.entrySet().stream()
472483
.filter(entry -> removedChannels.contains(entry.getValue()))
@@ -646,11 +657,12 @@ private long activeLoad(List<ChannelRef> refs) {
646657
return load;
647658
}
648659

649-
private long pickerLoad(List<ChannelRef> refs) {
660+
private long pickerLoad(List<ChannelRef> refs, long now) {
661+
// Expiry accounting is swept before dynamicUpscale acquires the pool monitor.
650662
long load = 0;
651663
for (ChannelRef channelRef : refs) {
652664
if (channelRef.isActive()) {
653-
load += channelRef.getPickerLoad();
665+
load += channelRef.getPickerLoad(now);
654666
}
655667
}
656668
return load;
@@ -689,6 +701,8 @@ private void initOptions() {
689701
maxScaleUpPercent = poolOptions.getMaxScaleUpPercent();
690702
maxScaleDownChannels = poolOptions.getMaxScaleDownChannels();
691703
drainIdleGrace = poolOptions.getDrainIdleGrace();
704+
errorPenaltyStep = poolOptions.getErrorPenaltyStep();
705+
errorPenaltyDuration = poolOptions.getErrorPenaltyDuration();
692706
isDynamicScalingEnabled =
693707
minRpcPerChannel > 0 && maxRpcPerChannel > 0 && !scaleDownInterval.isZero();
694708
channelPickStrategy = poolOptions.getChannelPickStrategy();
@@ -2058,11 +2072,12 @@ private ChannelRef pickLeastBusyChannelDifferentFrom(@Nullable ChannelRef exclud
20582072
}
20592073
ChannelRef leastBusyChannelRef = null;
20602074
int leastBusyStreams = Integer.MAX_VALUE;
2075+
long now = nanoClock.get();
20612076
for (ChannelRef candidate : channelRefs) {
20622077
if (candidate == excludedChannelRef || !candidate.isActive()) {
20632078
continue;
20642079
}
2065-
int streams = candidate.getPickerLoad();
2080+
int streams = candidate.getPickerLoad(now);
20662081
if (leastBusyChannelRef == null || streams < leastBusyStreams) {
20672082
leastBusyChannelRef = candidate;
20682083
leastBusyStreams = streams;
@@ -2131,8 +2146,10 @@ private void maybeSignalScaleUp(ChannelRef selectedChannel) {
21312146
|| activeChannels >= maxSize) {
21322147
return;
21332148
}
2134-
if (selectedChannel.getActiveStreamsCount() <= maxRpcPerChannel
2135-
&& ((double) totalActiveStreams.get() / activeChannels) <= maxRpcPerChannel) {
2149+
int selectedLoad = selectedChannel.getPickerLoad();
2150+
long totalLoad = (long) totalActiveStreams.get() + totalErrorPenaltyLoad.get();
2151+
if (selectedLoad <= maxRpcPerChannel
2152+
&& ((double) totalLoad / activeChannels) <= maxRpcPerChannel) {
21362153
return;
21372154
}
21382155
signalScaleUp();
@@ -2171,12 +2188,15 @@ private void runScaleUpWorker() {
21712188
}
21722189

21732190
private void dynamicUpscale() {
2191+
long now = nanoClock.get();
2192+
for (ChannelRef channelRef : channelRefs) {
2193+
channelRef.currentErrorPenalty(now);
2194+
}
21742195
final int channelsToBuild;
21752196
synchronized (this) {
21762197
if (!isDynamicScalingEnabled || shuttingDown || channelRefs.size() >= maxSize) {
21772198
return;
21782199
}
2179-
long now = nanoClock.get();
21802200
if (lastScaleUpNanos != Long.MIN_VALUE
21812201
&& now - lastScaleUpNanos < scaleUpCooldown.toNanos()) {
21822202
return;
@@ -2187,7 +2207,7 @@ private void dynamicUpscale() {
21872207
if (active == 0) {
21882208
return;
21892209
}
2190-
int desired = ceilDiv(pickerLoad(activeChannels), targetRpcPerChannel());
2210+
int desired = ceilDiv(pickerLoad(activeChannels, now), targetRpcPerChannel());
21912211
int add = desired - active;
21922212
// Small pools may add two channels per event before percentage growth dominates.
21932213
int percentCap = Math.max(2, ceilDiv((long) active * maxScaleUpPercent, 100));
@@ -2304,12 +2324,13 @@ private ChannelRef pickLeastBusyWithFallback(boolean forFallback) {
23042324
ChannelRef overallCandidate = null;
23052325
int overallMinStreams = Integer.MAX_VALUE;
23062326
int readyMaxStreams = 0;
2327+
long now = nanoClock.get();
23072328

23082329
for (ChannelRef channelRef : channelRefs) {
23092330
if (!channelRef.isActive()) {
23102331
continue;
23112332
}
2312-
int cnt = channelRef.getPickerLoad();
2333+
int cnt = channelRef.getPickerLoad(now);
23132334
if (overallCandidate == null || cnt < overallMinStreams) {
23142335
overallMinStreams = cnt;
23152336
overallCandidate = channelRef;
@@ -2323,7 +2344,7 @@ private ChannelRef pickLeastBusyWithFallback(boolean forFallback) {
23232344
}
23242345

23252346
if (overallCandidate == null) {
2326-
return leastLoadedActiveChannel(channelRefs);
2347+
return leastLoadedActiveChannel(channelRefs, now);
23272348
}
23282349

23292350
// For scale-up, use maxStreams among ready channels (consistent with non-fallback path).
@@ -2396,11 +2417,20 @@ ChannelRef pickFromCandidates(List<ChannelRef> candidates) {
23962417
}
23972418

23982419
private ChannelRef leastLoadedActiveChannel(List<ChannelRef> candidates) {
2420+
return leastLoadedActiveChannel(candidates, nanoClock.get());
2421+
}
2422+
2423+
private ChannelRef leastLoadedActiveChannel(List<ChannelRef> candidates, long now) {
23992424
ChannelRef best = null;
2425+
int bestLoad = Integer.MAX_VALUE;
24002426
for (ChannelRef candidate : candidates) {
2401-
if (candidate.isActive()
2402-
&& (best == null || candidate.getPickerLoad() < best.getPickerLoad())) {
2427+
if (!candidate.isActive()) {
2428+
continue;
2429+
}
2430+
int candidateLoad = candidate.getPickerLoad(now);
2431+
if (best == null || candidateLoad < bestLoad) {
24032432
best = candidate;
2433+
bestLoad = candidateLoad;
24042434
}
24052435
}
24062436
if (best != null) {
@@ -2822,6 +2852,10 @@ protected class ChannelRef {
28222852
private final long createdNanos = nanoClock.get();
28232853
private volatile long lastActivityNanos = createdNanos;
28242854
private long lastResponseNanos = createdNanos;
2855+
2856+
private volatile int errorPenaltyLoad;
2857+
private volatile long errorPenaltyExpiresAtNanos;
2858+
28252859
private final AtomicInteger deadlineExceededCount = new AtomicInteger();
28262860
private final AtomicLong okCalls = new AtomicLong();
28272861
private final AtomicLong errCalls = new AtomicLong();
@@ -2938,6 +2972,7 @@ protected void activeStreamsCountDecr(long startNanos, Status status, boolean fr
29382972
if (unresponsiveDetectionEnabled) {
29392973
detectUnresponsiveConnection(startNanos, status, fromClientSide);
29402974
}
2975+
applyErrorPenalty(status);
29412976
if (actStreams == 0 && !isActive()) {
29422977
scheduleDrain(this);
29432978
}
@@ -2957,7 +2992,98 @@ protected int getActiveStreamsCount() {
29572992
}
29582993

29592994
protected int getPickerLoad() {
2960-
return getActiveStreamsCount();
2995+
return (int)
2996+
Math.min(Integer.MAX_VALUE, (long) getActiveStreamsCount() + currentErrorPenalty());
2997+
}
2998+
2999+
@VisibleForTesting
3000+
int currentErrorPenalty() {
3001+
int penalty = errorPenaltyLoad;
3002+
if (penalty == 0) {
3003+
return 0;
3004+
}
3005+
return currentErrorPenalty(nanoClock.get());
3006+
}
3007+
3008+
private int currentErrorPenalty(long now) {
3009+
int penalty = errorPenaltyLoad;
3010+
if (penalty == 0) {
3011+
return 0;
3012+
}
3013+
if (now - errorPenaltyExpiresAtNanos < 0) {
3014+
return penalty;
3015+
}
3016+
int expiredPenalty;
3017+
synchronized (this) {
3018+
penalty = errorPenaltyLoad;
3019+
if (penalty == 0) {
3020+
return 0;
3021+
}
3022+
if (now - errorPenaltyExpiresAtNanos < 0) {
3023+
return penalty;
3024+
}
3025+
errorPenaltyLoad = 0;
3026+
errorPenaltyExpiresAtNanos = 0;
3027+
expiredPenalty = penalty;
3028+
}
3029+
totalErrorPenaltyLoad.addAndGet(-expiredPenalty);
3030+
return 0;
3031+
}
3032+
3033+
private int getPickerLoad(long now) {
3034+
int penalty = errorPenaltyLoad;
3035+
if (penalty != 0 && now - errorPenaltyExpiresAtNanos >= 0) {
3036+
penalty = 0;
3037+
}
3038+
return (int) Math.min(Integer.MAX_VALUE, (long) getActiveStreamsCount() + penalty);
3039+
}
3040+
3041+
private void applyErrorPenalty(Status status) {
3042+
if (!isDynamicScalingEnabled
3043+
|| !active
3044+
|| errorPenaltyStep == 0
3045+
|| errorPenaltyDuration.isZero()
3046+
|| errorPenaltyDuration.isNegative()
3047+
|| (status.getCode() != Code.UNAVAILABLE
3048+
&& status.getCode() != Code.RESOURCE_EXHAUSTED)) {
3049+
return;
3050+
}
3051+
long now = nanoClock.get();
3052+
long durationNanos = errorPenaltyDuration.toNanos();
3053+
long addedPenalty;
3054+
synchronized (this) {
3055+
if (!active) {
3056+
return;
3057+
}
3058+
int previousContribution = errorPenaltyLoad;
3059+
int current =
3060+
previousContribution != 0 && now - errorPenaltyExpiresAtNanos < 0
3061+
? previousContribution
3062+
: 0;
3063+
int next = (int) Math.min(maxRpcPerChannel, (long) current + errorPenaltyStep);
3064+
errorPenaltyExpiresAtNanos = now + durationNanos;
3065+
errorPenaltyLoad = next;
3066+
addedPenalty = (long) next - previousContribution;
3067+
}
3068+
if (addedPenalty != 0) {
3069+
totalErrorPenaltyLoad.addAndGet(addedPenalty);
3070+
}
3071+
if (addedPenalty > 0) {
3072+
maybeSignalScaleUp(this);
3073+
}
3074+
}
3075+
3076+
private void clearErrorPenalty() {
3077+
int clearedPenalty;
3078+
synchronized (this) {
3079+
clearedPenalty = errorPenaltyLoad;
3080+
if (clearedPenalty == 0) {
3081+
return;
3082+
}
3083+
errorPenaltyLoad = 0;
3084+
errorPenaltyExpiresAtNanos = 0;
3085+
}
3086+
totalErrorPenaltyLoad.addAndGet(-clearedPenalty);
29613087
}
29623088

29633089
protected long getAndResetOkCalls() {

0 commit comments

Comments
 (0)