@@ -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