From 0572ea8e8778ee074166a55e06e802283929b10b Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Thu, 5 Feb 2026 12:52:02 +0530 Subject: [PATCH] fix: Use daemon threads for grpc-gcp background executors by default Switch internal grpc-gcp executors to use a shared daemon-aware ThreadFactory to avoid keeping the JVM alive when only background threads remain. Add a system property (com.google.cloud.grpc.use_daemon_threads) to opt out and preserve legacy non-daemon behavior. --- .../google/cloud/grpc/GcpManagedChannel.java | 5 +-- .../cloud/grpc/GcpMultiEndpointChannel.java | 3 +- .../google/cloud/grpc/GcpThreadFactory.java | 44 +++++++++++++++++++ .../grpc/fallback/GcpFallbackChannel.java | 7 ++- .../grpc/multiendpoint/MultiEndpoint.java | 4 +- 5 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 grpc-gcp/src/main/java/com/google/cloud/grpc/GcpThreadFactory.java diff --git a/grpc-gcp/src/main/java/com/google/cloud/grpc/GcpManagedChannel.java b/grpc-gcp/src/main/java/com/google/cloud/grpc/GcpManagedChannel.java index 5674320..f24f2b8 100644 --- a/grpc-gcp/src/main/java/com/google/cloud/grpc/GcpManagedChannel.java +++ b/grpc-gcp/src/main/java/com/google/cloud/grpc/GcpManagedChannel.java @@ -28,7 +28,6 @@ import com.google.cloud.grpc.proto.MethodConfig; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.errorprone.annotations.concurrent.GuardedBy; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.MessageOrBuilder; @@ -139,7 +138,7 @@ public class GcpManagedChannel extends ManagedChannel { private final ExecutorService stateNotificationExecutor = Executors.newCachedThreadPool( - new ThreadFactoryBuilder().setNameFormat("gcp-mc-state-notifications-%d").build()); + GcpThreadFactory.newThreadFactory("gcp-mc-state-notifications-%d")); // Callbacks to call when state changes. @GuardedBy("this") @@ -177,7 +176,7 @@ public class GcpManagedChannel extends ManagedChannel { String.format("pool-%d", channelPoolIndex.incrementAndGet()); private final Map cumulativeMetricValues = new ConcurrentHashMap<>(); private final ScheduledExecutorService backgroundService = - Executors.newSingleThreadScheduledExecutor(); + Executors.newSingleThreadScheduledExecutor(GcpThreadFactory.newThreadFactory("gcp-mc-bg-%d")); // Metrics counters. private final AtomicInteger readyChannels = new AtomicInteger(); diff --git a/grpc-gcp/src/main/java/com/google/cloud/grpc/GcpMultiEndpointChannel.java b/grpc-gcp/src/main/java/com/google/cloud/grpc/GcpMultiEndpointChannel.java index 3f28e80..1bf2c24 100644 --- a/grpc-gcp/src/main/java/com/google/cloud/grpc/GcpMultiEndpointChannel.java +++ b/grpc-gcp/src/main/java/com/google/cloud/grpc/GcpMultiEndpointChannel.java @@ -168,7 +168,8 @@ public class GcpMultiEndpointChannel extends ManagedChannel { @GuardedBy("this") private final Set currentEndpoints = new HashSet<>(); - private final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1); + private final ScheduledExecutorService executor = + new ScheduledThreadPoolExecutor(1, GcpThreadFactory.newThreadFactory("gcp-me-%d")); /** * Constructor for {@link GcpMultiEndpointChannel}. diff --git a/grpc-gcp/src/main/java/com/google/cloud/grpc/GcpThreadFactory.java b/grpc-gcp/src/main/java/com/google/cloud/grpc/GcpThreadFactory.java new file mode 100644 index 0000000..40b92b8 --- /dev/null +++ b/grpc-gcp/src/main/java/com/google/cloud/grpc/GcpThreadFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.grpc; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.ThreadFactory; + +/** Thread factory helper for grpc-gcp background executors. */ +public final class GcpThreadFactory { + /** System property to control daemon threads for grpc-gcp background executors. */ + public static final String USE_DAEMON_THREADS_PROPERTY = + "com.google.cloud.grpc.use_daemon_threads"; + + private GcpThreadFactory() {} + + /** + * Creates a {@link ThreadFactory} that names threads and honors the daemon-thread setting. + * + *

Defaults to daemon threads to avoid keeping the JVM alive when only background work remains. + * Set {@code -Dcom.google.cloud.grpc.use_daemon_threads=false} to use non-daemon threads. + */ + public static ThreadFactory newThreadFactory(String nameFormat) { + boolean useDaemon = true; + String prop = System.getProperty(USE_DAEMON_THREADS_PROPERTY); + if (prop != null) { + useDaemon = Boolean.parseBoolean(prop); + } + return new ThreadFactoryBuilder().setNameFormat(nameFormat).setDaemon(useDaemon).build(); + } +} diff --git a/grpc-gcp/src/main/java/com/google/cloud/grpc/fallback/GcpFallbackChannel.java b/grpc-gcp/src/main/java/com/google/cloud/grpc/fallback/GcpFallbackChannel.java index 42aeef2..0cf1261 100644 --- a/grpc-gcp/src/main/java/com/google/cloud/grpc/fallback/GcpFallbackChannel.java +++ b/grpc-gcp/src/main/java/com/google/cloud/grpc/fallback/GcpFallbackChannel.java @@ -20,6 +20,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.NANOSECONDS; +import com.google.cloud.grpc.GcpThreadFactory; import com.google.common.annotations.VisibleForTesting; import io.grpc.CallOptions; import io.grpc.Channel; @@ -84,7 +85,8 @@ public GcpFallbackChannel( if (execService != null) { this.execService = execService; } else { - this.execService = Executors.newScheduledThreadPool(3); + this.execService = + Executors.newScheduledThreadPool(3, GcpThreadFactory.newThreadFactory("gcp-fallback-%d")); } this.options = options; if (options.getGcpOpenTelemetry() != null) { @@ -150,7 +152,8 @@ public GcpFallbackChannel( if (execService != null) { this.execService = execService; } else { - this.execService = Executors.newScheduledThreadPool(3); + this.execService = + Executors.newScheduledThreadPool(3, GcpThreadFactory.newThreadFactory("gcp-fallback-%d")); } this.options = options; if (options.getGcpOpenTelemetry() != null) { diff --git a/grpc-gcp/src/main/java/com/google/cloud/grpc/multiendpoint/MultiEndpoint.java b/grpc-gcp/src/main/java/com/google/cloud/grpc/multiendpoint/MultiEndpoint.java index 2d51773..3f7e32a 100644 --- a/grpc-gcp/src/main/java/com/google/cloud/grpc/multiendpoint/MultiEndpoint.java +++ b/grpc-gcp/src/main/java/com/google/cloud/grpc/multiendpoint/MultiEndpoint.java @@ -19,6 +19,7 @@ import static java.util.Comparator.comparingInt; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import com.google.cloud.grpc.GcpThreadFactory; import com.google.cloud.grpc.multiendpoint.Endpoint.EndpointState; import com.google.common.base.Preconditions; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -78,7 +79,8 @@ public final class MultiEndpoint { private long recoverCnt = 0; private long replaceCnt = 0; - final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1); + final ScheduledExecutorService executor = + new ScheduledThreadPoolExecutor(1, GcpThreadFactory.newThreadFactory("gcp-me-core-%d")); private MultiEndpoint(Builder builder) { this.recoveryTimeout = builder.recoveryTimeout;