• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20216

23 Mar 2026 11:58AM UTC coverage: 88.7% (+0.01%) from 88.686%
#20216

push

github

web-flow
core: fix false-positive orphan warning in ManagedChannelOrphanWrapper (#12705)

This PR addresses a race condition where ManagedChannelOrphanWrapper
could incorrectly log a "not shutdown properly" warning during garbage
collection when using directExecutor().

Changes:

Reference Management: Moved phantom.clearSafely() to execute after the
super.shutdown() calls to ensure the orphan tracker isn't detached
prematurely.

Reachability Fence: Added a reachability fence in shutdown() and
shutdownNow() to ensure the wrapper remains alive until the methods
return, preventing the JIT from marking it for early collection.

Regression Test: Added a test case that simulates a reference being held
on the stack to verify the fix and prevent future regressions.

Testing:
Verified with ./gradlew :grpc-core:test --tests
ManagedChannelOrphanWrapperTest -PskipAndroid=true.

Fixes #12641

---------

Co-authored-by: Kannan J <kannanjgithub@google.com>

35489 of 40010 relevant lines covered (88.7%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.92
/../core/src/main/java/io/grpc/internal/ManagedChannelOrphanWrapper.java
1
/*
2
 * Copyright 2018 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.internal;
18

19
import com.google.common.annotations.VisibleForTesting;
20
import io.grpc.ManagedChannel;
21
import java.lang.ref.Reference;
22
import java.lang.ref.ReferenceQueue;
23
import java.lang.ref.SoftReference;
24
import java.lang.ref.WeakReference;
25
import java.util.concurrent.ConcurrentHashMap;
26
import java.util.concurrent.ConcurrentMap;
27
import java.util.concurrent.atomic.AtomicBoolean;
28
import java.util.logging.Level;
29
import java.util.logging.LogRecord;
30
import java.util.logging.Logger;
31

32
/**
33
 *  Best effort detecting channels that has not been properly cleaned up.
34
 *  Use {@link WeakReference} to avoid keeping the channel alive and retaining too much memory.
35
 *  Check lost references only on new channel creation and log message to indicate
36
 *  the previous channel (id and target) that has not been shutdown. This is done to avoid Object
37
 *  finalizers.
38
 */
39
final class ManagedChannelOrphanWrapper extends ForwardingManagedChannel {
40
  private static final ReferenceQueue<ManagedChannelOrphanWrapper> refqueue =
1✔
41
      new ReferenceQueue<>();
42
  // Retain the References so they don't get GC'd
43
  private static final ConcurrentMap<ManagedChannelReference, ManagedChannelReference> refs =
1✔
44
      new ConcurrentHashMap<>();
45
  private static final Logger logger =
1✔
46
      Logger.getLogger(ManagedChannelOrphanWrapper.class.getName());
1✔
47

48
  private final ManagedChannelReference phantom;
49

50
  ManagedChannelOrphanWrapper(ManagedChannel delegate) {
51
    this(delegate, refqueue, refs);
1✔
52
  }
1✔
53

54
  @VisibleForTesting
55
  ManagedChannelOrphanWrapper(
56
      ManagedChannel delegate,
57
      ReferenceQueue<ManagedChannelOrphanWrapper> refqueue,
58
      ConcurrentMap<ManagedChannelReference, ManagedChannelReference> refs) {
59
    super(delegate);
1✔
60
    phantom = new ManagedChannelReference(this, delegate, refqueue, refs);
1✔
61
  }
1✔
62

63
  @Override
64
  public ManagedChannel shutdown() {
65
    ManagedChannel result = super.shutdown();
1✔
66
    phantom.clearSafely();
1✔
67
    // This dummy check prevents the JIT from collecting 'this' too early
68
    if (this.getClass() == null) {
1✔
69
      throw new AssertionError();
×
70
    }
71
    return result;
1✔
72
  }
73

74
  @Override
75
  public ManagedChannel shutdownNow() {
76
    ManagedChannel result = super.shutdownNow();
1✔
77
    phantom.clearSafely();
1✔
78
    // This dummy check prevents the JIT from collecting 'this' too early
79
    if (this.getClass() == null) {
1✔
80
      throw new AssertionError();
×
81
    }
82
    return result;
1✔
83
  }
84

85
  @VisibleForTesting
86
  static final class ManagedChannelReference extends WeakReference<ManagedChannelOrphanWrapper> {
87

88
    private static final String ALLOCATION_SITE_PROPERTY_NAME =
89
        "io.grpc.ManagedChannel.enableAllocationTracking";
90

91
    private static final boolean ENABLE_ALLOCATION_TRACKING =
1✔
92
        Boolean.parseBoolean(System.getProperty(ALLOCATION_SITE_PROPERTY_NAME, "true"));
1✔
93

94
    @SuppressWarnings("StaticAssignmentOfThrowable")
95
    private static final RuntimeException missingCallSite = missingCallSite();
1✔
96

97
    private final ReferenceQueue<ManagedChannelOrphanWrapper> refqueue;
98
    private final ConcurrentMap<ManagedChannelReference, ManagedChannelReference> refs;
99

100
    private final String channelStr;
101
    private final Reference<RuntimeException> allocationSite;
102
    private final AtomicBoolean shutdown = new AtomicBoolean();
1✔
103

104
    ManagedChannelReference(
105
        ManagedChannelOrphanWrapper orphanable,
106
        ManagedChannel channel,
107
        ReferenceQueue<ManagedChannelOrphanWrapper> refqueue,
108
        ConcurrentMap<ManagedChannelReference, ManagedChannelReference> refs) {
109
      super(orphanable, refqueue);
1✔
110
      allocationSite = new SoftReference<>(
1✔
111
          ENABLE_ALLOCATION_TRACKING
1✔
112
              ? new RuntimeException("ManagedChannel allocation site")
1✔
113
              : missingCallSite);
1✔
114
      this.channelStr = channel.toString();
1✔
115
      this.refqueue = refqueue;
1✔
116
      this.refs = refs;
1✔
117
      this.refs.put(this, this);
1✔
118
      cleanQueue(refqueue);
1✔
119
    }
1✔
120

121
    /**
122
     * This clear() is *not* called automatically by the JVM.  As this is a weak ref, the reference
123
     * will be cleared automatically by the JVM, but will not be removed from {@link #refs}.
124
     * We do it here to avoid this ending up on the reference queue.
125
     */
126
    @Override
127
    public void clear() {
128
      clearInternal();
1✔
129
      // We run this here to periodically clean up the queue if at least some of the channels are
130
      // being shutdown properly.
131
      cleanQueue(refqueue);
1✔
132
    }
1✔
133

134
    /**
135
     * Safe to call concurrently.
136
     */
137
    private void clearSafely() {
138
      if (!shutdown.getAndSet(true)) {
1✔
139
        clear();
1✔
140
      }
141
    }
1✔
142

143
    // avoid reentrancy
144
    private void clearInternal() {
145
      super.clear();
1✔
146
      refs.remove(this);
1✔
147
      allocationSite.clear();
1✔
148
    }
1✔
149

150
    private static RuntimeException missingCallSite() {
151
      RuntimeException e = new RuntimeException(
1✔
152
          "ManagedChannel allocation site not recorded.  Set -D"
153
              + ALLOCATION_SITE_PROPERTY_NAME + "=true to enable it");
154
      e.setStackTrace(new StackTraceElement[0]);
1✔
155
      return e;
1✔
156
    }
157

158
    @VisibleForTesting
159
    static int cleanQueue(ReferenceQueue<ManagedChannelOrphanWrapper> refqueue) {
160
      ManagedChannelReference ref;
161
      int orphanedChannels = 0;
1✔
162
      while ((ref = (ManagedChannelReference) refqueue.poll()) != null) {
1✔
163
        RuntimeException maybeAllocationSite = ref.allocationSite.get();
1✔
164
        boolean wasShutdown = ref.shutdown.get();
1✔
165
        ref.clearInternal(); // technically the reference is gone already.
1✔
166
        if (!wasShutdown) {
1✔
167
          orphanedChannels++;
1✔
168
          Level level = Level.SEVERE;
1✔
169
          if (logger.isLoggable(level)) {
1✔
170
            String fmt =
1✔
171
                "*~*~*~ Previous channel {0} was garbage collected without being shut down! ~*~*~*"
172
                    + System.getProperty("line.separator")
1✔
173
                    + "    Make sure to call shutdown()/shutdownNow()";
174
            LogRecord lr = new LogRecord(level, fmt);
1✔
175
            lr.setLoggerName(logger.getName());
1✔
176
            lr.setParameters(new Object[] {ref.channelStr});
1✔
177
            lr.setThrown(maybeAllocationSite);
1✔
178
            logger.log(lr);
1✔
179
          }
180
        }
181
      }
1✔
182
      return orphanedChannels;
1✔
183
    }
184
  }
185
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc