Skip to content

Commit 0752206

Browse files
committed
feat(grpc-gcp): move scale-up to background worker
1 parent 07a7505 commit 0752206

7 files changed

Lines changed: 729 additions & 42 deletions

File tree

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

Lines changed: 212 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.cloud.grpc.proto.MethodConfig;
2929
import com.google.common.annotations.VisibleForTesting;
3030
import com.google.common.base.Joiner;
31+
import com.google.common.base.Preconditions;
3132
import com.google.errorprone.annotations.concurrent.GuardedBy;
3233
import com.google.protobuf.Descriptors.FieldDescriptor;
3334
import com.google.protobuf.MessageOrBuilder;
@@ -65,13 +66,15 @@
6566
import java.util.Set;
6667
import java.util.concurrent.ConcurrentHashMap;
6768
import java.util.concurrent.CopyOnWriteArrayList;
69+
import java.util.concurrent.Executor;
6870
import java.util.concurrent.ExecutorService;
6971
import java.util.concurrent.Executors;
7072
import java.util.concurrent.RejectedExecutionException;
7173
import java.util.concurrent.ScheduledFuture;
7274
import java.util.concurrent.ScheduledThreadPoolExecutor;
7375
import java.util.concurrent.ThreadLocalRandom;
7476
import java.util.concurrent.TimeUnit;
77+
import java.util.concurrent.atomic.AtomicBoolean;
7578
import java.util.concurrent.atomic.AtomicInteger;
7679
import java.util.concurrent.atomic.AtomicLong;
7780
import 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

Comments
 (0)