2828import com .google .cloud .grpc .proto .MethodConfig ;
2929import com .google .common .annotations .VisibleForTesting ;
3030import com .google .common .base .Joiner ;
31+ import com .google .common .base .Preconditions ;
3132import com .google .errorprone .annotations .concurrent .GuardedBy ;
3233import com .google .protobuf .Descriptors .FieldDescriptor ;
3334import com .google .protobuf .MessageOrBuilder ;
6566import java .util .Set ;
6667import java .util .concurrent .ConcurrentHashMap ;
6768import java .util .concurrent .CopyOnWriteArrayList ;
69+ import java .util .concurrent .Executor ;
6870import java .util .concurrent .ExecutorService ;
6971import java .util .concurrent .Executors ;
7072import java .util .concurrent .RejectedExecutionException ;
7173import java .util .concurrent .ScheduledFuture ;
7274import java .util .concurrent .ScheduledThreadPoolExecutor ;
7375import java .util .concurrent .ThreadLocalRandom ;
7476import java .util .concurrent .TimeUnit ;
77+ import java .util .concurrent .atomic .AtomicBoolean ;
7578import java .util .concurrent .atomic .AtomicInteger ;
7679import java .util .concurrent .atomic .AtomicLong ;
7780import java .util .function .IntUnaryOperator ;
@@ -162,7 +165,9 @@ private static int stateFromChannelId(int channelId) {
162165 private int minRpcPerChannel = 0 ;
163166 private int maxRpcPerChannel = 0 ;
164167 private Duration scaleDownInterval = Duration .ZERO ;
168+ private Duration scaleUpCooldown = Duration .ofSeconds (10 );
165169 private int scaleDownConsecutiveLowLoadChecks = 3 ;
170+ private int maxScaleUpPercent = 30 ;
166171 private int consecutiveLowLoadChecks ;
167172 private int maxScaleDownChannels = 2 ;
168173 private boolean isDynamicScalingEnabled = false ;
@@ -273,6 +278,12 @@ private static int stateFromChannelId(int channelId) {
273278 private AtomicLong maxUnresponsiveDrops = new AtomicLong ();
274279 private AtomicLong scaleUpCount = new AtomicLong ();
275280 private AtomicLong scaleDownCount = new AtomicLong ();
281+ private final AtomicBoolean scaleUpSignalPending = new AtomicBoolean ();
282+ private final AtomicBoolean scaleUpWorkerRunning = new AtomicBoolean ();
283+ private final AtomicLong scaleUpSignalVersion = new AtomicLong ();
284+ private Executor scaleUpExecutor = SHARED_BACKGROUND_SERVICE ;
285+ private volatile long lastScaleUpNanos = Long .MIN_VALUE ;
286+ private volatile boolean shuttingDown ;
276287
277288 // Clock supplier for nanoTime, injectable for testing.
278289 private Supplier <Long > nanoClock = System ::nanoTime ;
@@ -294,6 +305,11 @@ int readyChannelCountForTest() {
294305 return readyChannels .get ();
295306 }
296307
308+ @ VisibleForTesting
309+ void setScaleUpExecutorForTest (Executor scaleUpExecutor ) {
310+ this .scaleUpExecutor = Preconditions .checkNotNull (scaleUpExecutor );
311+ }
312+
297313 @ VisibleForTesting
298314 void setNanoClock (Supplier <Long > nanoClock ) {
299315 this .nanoClock = nanoClock ;
@@ -489,7 +505,9 @@ private void initOptions() {
489505 minRpcPerChannel = poolOptions .getMinRpcPerChannel ();
490506 maxRpcPerChannel = poolOptions .getMaxRpcPerChannel ();
491507 scaleDownInterval = poolOptions .getScaleDownInterval ();
508+ scaleUpCooldown = poolOptions .getScaleUpCooldown ();
492509 scaleDownConsecutiveLowLoadChecks = poolOptions .getScaleDownConsecutiveLowLoadChecks ();
510+ maxScaleUpPercent = poolOptions .getMaxScaleUpPercent ();
493511 maxScaleDownChannels = poolOptions .getMaxScaleDownChannels ();
494512 isDynamicScalingEnabled =
495513 minRpcPerChannel > 0 && maxRpcPerChannel > 0 && !scaleDownInterval .isZero ();
@@ -517,7 +535,13 @@ private synchronized void initScaleDownChecker(Duration scaleDownInterval) {
517535
518536 scaleDownTask =
519537 SHARED_BACKGROUND_SERVICE .scheduleAtFixedRate (
520- this ::checkScaleDown ,
538+ () -> {
539+ try {
540+ checkScaleDown ();
541+ } catch (Throwable failure ) {
542+ logger .log (Level .WARNING , log ("Scale-down check failed" ), failure );
543+ }
544+ },
521545 scaleDownInterval .toMillis (),
522546 scaleDownInterval .toMillis (),
523547 MILLISECONDS );
@@ -1650,11 +1674,19 @@ public int getStreamsLowWatermark() {
16501674 }
16511675
16521676 public int getMinActiveStreams () {
1653- return channelRefs .stream ().mapToInt (ChannelRef ::getActiveStreamsCount ).min ().orElse (0 );
1677+ return channelRefs .stream ()
1678+ .filter (ChannelRef ::isActive )
1679+ .mapToInt (ChannelRef ::getActiveStreamsCount )
1680+ .min ()
1681+ .orElse (0 );
16541682 }
16551683
16561684 public int getMaxActiveStreams () {
1657- return channelRefs .stream ().mapToInt (ChannelRef ::getActiveStreamsCount ).max ().orElse (0 );
1685+ return channelRefs .stream ()
1686+ .filter (ChannelRef ::isActive )
1687+ .mapToInt (ChannelRef ::getActiveStreamsCount )
1688+ .max ()
1689+ .orElse (0 );
16581690 }
16591691
16601692 /**
@@ -1691,12 +1723,15 @@ protected synchronized ChannelRef getChannelRefRoundRobin() {
16911723 if (!isDynamicScalingEnabled && channelRefs .size () < maxSize ) {
16921724 return createNewChannel ();
16931725 }
1694- maybeDynamicUpscale ();
1695- bindingIndex ++;
1696- if (bindingIndex >= channelRefs .size ()) {
1697- bindingIndex = 0 ;
1726+ Object [] snapshot = channelRefs .toArray ();
1727+ for (int attempts = 0 ; attempts < snapshot .length ; attempts ++) {
1728+ bindingIndex = (bindingIndex + 1 ) % snapshot .length ;
1729+ ChannelRef candidate = (ChannelRef ) snapshot [bindingIndex ];
1730+ if (candidate .isActive ()) {
1731+ return candidate ;
1732+ }
16981733 }
1699- return channelRefs . get ( bindingIndex );
1734+ return pickFromCandidates ( channelRefs );
17001735 }
17011736
17021737 /**
@@ -1710,7 +1745,6 @@ protected synchronized ChannelRef getChannelRefRoundRobin() {
17101745 * Otherwise pick the one with the smallest number of streams.
17111746 */
17121747 protected ChannelRef getChannelRef (@ Nullable String key ) {
1713- maybeDynamicUpscale ();
17141748 if (key == null || key .isEmpty ()) {
17151749 return pickLeastBusyChannel (/* forFallback= */ false );
17161750 }
@@ -1777,7 +1811,6 @@ protected ChannelRef getChannelRef(@Nullable String key) {
17771811 * Pick a {@link ChannelRef} using a caller-owned reference instead of grpc-gcp's affinity map.
17781812 */
17791813 protected ChannelRef getChannelRefByAffinityRef (ChannelAffinityRef affinityRef ) {
1780- maybeDynamicUpscale ();
17811814 // Retry if another thread updates the caller-owned affinity ref while we are picking a channel.
17821815 while (true ) {
17831816 int state = affinityRef .state .get ();
@@ -1894,24 +1927,158 @@ private ChannelRef tryCreateNewChannel() {
18941927 return null ;
18951928 }
18961929
1897- private void maybeDynamicUpscale () {
1898- if (!isDynamicScalingEnabled || channelRefs .size () >= maxSize ) {
1930+ private void maybeSignalScaleUp (ChannelRef selectedChannel ) {
1931+ int activeChannels = channelRefs .size ();
1932+ if (!selectedChannel .isActive ()
1933+ || !isDynamicScalingEnabled
1934+ || shuttingDown
1935+ || activeChannels == 0
1936+ || activeChannels >= maxSize ) {
18991937 return ;
19001938 }
1901-
1902- if (( totalActiveStreams .get () / channelRefs . size ()) > = maxRpcPerChannel ) {
1903- dynamicUpscale () ;
1939+ if ( selectedChannel . getActiveStreamsCount () <= maxRpcPerChannel
1940+ && (( double ) totalActiveStreams .get () / activeChannels ) < = maxRpcPerChannel ) {
1941+ return ;
19041942 }
1943+ signalScaleUp ();
19051944 }
19061945
1907- private synchronized void dynamicUpscale () {
1908- if (!isDynamicScalingEnabled || channelRefs .size () >= maxSize ) {
1946+ private void signalScaleUp () {
1947+ long signalVersion = scaleUpSignalVersion .incrementAndGet ();
1948+ scaleUpSignalPending .set (true );
1949+ if (!scaleUpWorkerRunning .compareAndSet (false , true )) {
19091950 return ;
19101951 }
1952+ submitScaleUpWorker (signalVersion );
1953+ }
19111954
1912- if ((totalActiveStreams .get () / channelRefs .size ()) >= maxRpcPerChannel ) {
1913- createNewChannel ();
1914- scaleUpCount .incrementAndGet ();
1955+ private void submitScaleUpWorker (long signalVersion ) {
1956+ try {
1957+ scaleUpExecutor .execute (this ::runScaleUpWorker );
1958+ } catch (Throwable failure ) {
1959+ scaleUpWorkerRunning .set (false );
1960+ if (scaleUpSignalVersion .get () == signalVersion ) {
1961+ scaleUpSignalPending .set (false );
1962+ } else if (!shuttingDown && scaleUpWorkerRunning .compareAndSet (false , true )) {
1963+ submitScaleUpWorker (scaleUpSignalVersion .get ());
1964+ }
1965+ logger .log (Level .FINE , log ("Scale-up task rejected" ), failure );
1966+ }
1967+ }
1968+
1969+ private void runScaleUpWorker () {
1970+ try {
1971+ do {
1972+ scaleUpSignalPending .set (false );
1973+ try {
1974+ dynamicUpscale ();
1975+ } catch (Throwable failure ) {
1976+ logger .log (Level .WARNING , log ("Scale-up failed" ), failure );
1977+ }
1978+ } while (scaleUpSignalPending .get () && !shuttingDown );
1979+ } finally {
1980+ scaleUpWorkerRunning .set (false );
1981+ if (scaleUpSignalPending .get () && !shuttingDown ) {
1982+ signalScaleUp ();
1983+ }
1984+ }
1985+ }
1986+
1987+ private void dynamicUpscale () {
1988+ final int channelsToBuild ;
1989+ final long scaleUpNanos ;
1990+ int reused = 0 ;
1991+ synchronized (this ) {
1992+ if (!isDynamicScalingEnabled || shuttingDown || channelRefs .size () >= maxSize ) {
1993+ return ;
1994+ }
1995+ long now = nanoClock .get ();
1996+ if (lastScaleUpNanos != Long .MIN_VALUE
1997+ && now - lastScaleUpNanos < scaleUpCooldown .toNanos ()) {
1998+ return ;
1999+ }
2000+ int active = channelRefs .size ();
2001+ int targetRpcPerChannel = Math .max (1 , (minRpcPerChannel + maxRpcPerChannel ) / 2 );
2002+ long load = totalActiveStreams .get ();
2003+ int desired =
2004+ load == 0
2005+ ? active
2006+ : (int ) Math .min (Integer .MAX_VALUE , 1 + ((load - 1 ) / targetRpcPerChannel ));
2007+ int percentCap = Math .max (2 , (int ) (1 + (((long ) active * maxScaleUpPercent - 1 ) / 100 )));
2008+ int add = Math .min (Math .max (0 , desired - active ), percentCap );
2009+ add = Math .min (add , maxSize - active );
2010+ while (reused < add ) {
2011+ Optional <ChannelRef > reusable = pickChannelForReuse ();
2012+ if (!reusable .isPresent ()) {
2013+ break ;
2014+ }
2015+ ChannelRef channelRef = reusable .get ();
2016+ removedChannelRefs .remove (channelRef );
2017+ channelRefs .add (channelRef );
2018+ channelIdToChannelRef .put (channelRef .getId (), channelRef );
2019+ channelRef .activateAndAccountReadiness ();
2020+ reused ++;
2021+ scaleUpCount .incrementAndGet ();
2022+ lastScaleUpNanos = now ;
2023+ maxChannels .accumulateAndGet (getNumberOfChannels (), Math ::max );
2024+ }
2025+ channelsToBuild = add - reused ;
2026+ scaleUpNanos = now ;
2027+ }
2028+
2029+ List <ManagedChannel > builtChannels = new ArrayList <>(channelsToBuild );
2030+ try {
2031+ for (int i = 0 ; i < channelsToBuild ; i ++) {
2032+ builtChannels .add (
2033+ Preconditions .checkNotNull (
2034+ delegateChannelBuilder .build (), "Delegate channel builder returned null." ));
2035+ }
2036+ } catch (Throwable failure ) {
2037+ builtChannels .forEach (this ::shutdownUnpublishedChannel );
2038+ throw failure ;
2039+ }
2040+
2041+ for (int i = 0 ; i < builtChannels .size (); i ++) {
2042+ ManagedChannel channel = builtChannels .get (i );
2043+ try {
2044+ boolean published = false ;
2045+ synchronized (this ) {
2046+ if (!shuttingDown && channelRefs .size () < maxSize ) {
2047+ ChannelRef channelRef = new ChannelRef (channel );
2048+ channelRefs .add (channelRef );
2049+ try {
2050+ channelIdToChannelRef .put (channelRef .getId (), channelRef );
2051+ channelRef .activateAndAccountReadiness ();
2052+ published = true ;
2053+ } catch (Throwable failure ) {
2054+ channelRefs .remove (channelRef );
2055+ channelIdToChannelRef .remove (channelRef .getId (), channelRef );
2056+ throw failure ;
2057+ }
2058+ }
2059+ }
2060+ if (published ) {
2061+ scaleUpCount .incrementAndGet ();
2062+ lastScaleUpNanos = scaleUpNanos ;
2063+ maxChannels .accumulateAndGet (getNumberOfChannels (), Math ::max );
2064+ } else {
2065+ shutdownUnpublishedChannel (channel );
2066+ }
2067+ } catch (Throwable failure ) {
2068+ shutdownUnpublishedChannel (channel );
2069+ for (int j = i + 1 ; j < builtChannels .size (); j ++) {
2070+ shutdownUnpublishedChannel (builtChannels .get (j ));
2071+ }
2072+ throw failure ;
2073+ }
2074+ }
2075+ }
2076+
2077+ private void shutdownUnpublishedChannel (ManagedChannel channel ) {
2078+ try {
2079+ channel .shutdownNow ();
2080+ } catch (Throwable failure ) {
2081+ logger .log (Level .WARNING , log ("Failed to close unpublished scale-up channel" ), failure );
19152082 }
19162083 }
19172084
@@ -1982,13 +2149,16 @@ private ChannelRef pickLeastBusyNoFallback() {
19822149 private ChannelRef pickLeastBusyWithFallback (boolean forFallback ) {
19832150 // Full scan to collect eligible ("ready") channels not in fallbackMap and under max streams.
19842151 List <ChannelRef > readyCandidates = new ArrayList <>();
1985- ChannelRef overallCandidate = channelRefs . get ( 0 ) ;
1986- int overallMinStreams = overallCandidate . getActiveStreamsCount () ;
2152+ ChannelRef overallCandidate = null ;
2153+ int overallMinStreams = Integer . MAX_VALUE ;
19872154 int readyMaxStreams = 0 ;
19882155
19892156 for (ChannelRef channelRef : channelRefs ) {
2157+ if (!channelRef .isActive ()) {
2158+ continue ;
2159+ }
19902160 int cnt = channelRef .getActiveStreamsCount ();
1991- if (cnt < overallMinStreams ) {
2161+ if (overallCandidate == null || cnt < overallMinStreams ) {
19922162 overallMinStreams = cnt ;
19932163 overallCandidate = channelRef ;
19942164 }
@@ -2000,6 +2170,10 @@ private ChannelRef pickLeastBusyWithFallback(boolean forFallback) {
20002170 }
20012171 }
20022172
2173+ if (overallCandidate == null ) {
2174+ return pickFromCandidates (channelRefs );
2175+ }
2176+
20032177 // For scale-up, use maxStreams among ready channels (consistent with non-fallback path).
20042178 int scaleUpStreams = readyCandidates .isEmpty () ? Integer .MAX_VALUE : readyMaxStreams ;
20052179 if (shouldScaleUp (scaleUpStreams )) {
@@ -2084,8 +2258,10 @@ ChannelRef pickFromCandidates(List<ChannelRef> candidates) {
20842258
20852259 @ Override
20862260 public String authority () {
2087- if (!channelRefs .isEmpty ()) {
2088- return channelRefs .get (0 ).getChannel ().authority ();
2261+ for (ChannelRef channelRef : channelRefs ) {
2262+ if (channelRef .isActive ()) {
2263+ return channelRef .getChannel ().authority ();
2264+ }
20892265 }
20902266 final ManagedChannel channel = delegateChannelBuilder .build ();
20912267 final String authority = channel .authority ();
@@ -2163,6 +2339,8 @@ private synchronized void cancelBackgroundTasks() {
21632339 @ Override
21642340 public ManagedChannel shutdownNow () {
21652341 logger .finer (log ("Shutdown now started." ));
2342+ shuttingDown = true ;
2343+ scaleUpSignalPending .set (false );
21662344 for (ChannelRef channelRef : channelRefs ) {
21672345 if (!channelRef .getChannel ().isTerminated ()) {
21682346 channelRef .getChannel ().shutdownNow ();
@@ -2183,6 +2361,8 @@ public ManagedChannel shutdownNow() {
21832361 @ Override
21842362 public ManagedChannel shutdown () {
21852363 logger .finer (log ("Shutdown started." ));
2364+ shuttingDown = true ;
2365+ scaleUpSignalPending .set (false );
21862366 for (ChannelRef channelRef : channelRefs ) {
21872367 channelRef .getChannel ().shutdown ();
21882368 }
@@ -2540,6 +2720,7 @@ protected void activeStreamsCountIncr() {
25402720 maxActiveStreams .accumulateAndGet (actStreams , Math ::max );
25412721 int totalActStreams = totalActiveStreams .incrementAndGet ();
25422722 maxTotalActiveStreams .accumulateAndGet (totalActStreams , Math ::max );
2723+ maybeSignalScaleUp (this );
25432724 }
25442725
25452726 protected void activeStreamsCountDecr (long startNanos , Status status , boolean fromClientSide ) {
@@ -2572,6 +2753,12 @@ protected int getActiveStreamsCount() {
25722753 return activeStreamsCount .get ();
25732754 }
25742755
2756+ @ VisibleForTesting
2757+ void setActiveStreamsForTest (int streams ) {
2758+ int previous = activeStreamsCount .getAndSet (streams );
2759+ totalActiveStreams .addAndGet (streams - previous );
2760+ }
2761+
25752762 protected long getAndResetOkCalls () {
25762763 return okCalls .getAndSet (0 );
25772764 }
0 commit comments