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

grpc / grpc-java / #20067

12 Nov 2025 07:34PM UTC coverage: 88.522% (-0.03%) from 88.552%
#20067

push

github

web-flow
core: Release lock before closing shared resource

If a resource has dependencies that also use SharedResourceHolder, and
its close() blocks waiting for processing on another thread, then the
threads could become deadlocked on the SharedResourceHolder lock. Our
answer should be "don't block," but it's also good to avoid calling
arbitrary code with a lock held.

create() is still called with the lock held, but that seems less likely
to do work on another thread, and it is harder to avoid the lock.
close() is very easy to call without the lock.

See d50098f80 and b/458736211

35069 of 39616 relevant lines covered (88.52%)

0.89 hits per line

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

97.73
/../core/src/main/java/io/grpc/internal/SharedResourceHolder.java
1
/*
2
 * Copyright 2014 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.base.Preconditions;
20
import java.util.IdentityHashMap;
21
import java.util.concurrent.Executors;
22
import java.util.concurrent.ScheduledExecutorService;
23
import java.util.concurrent.ScheduledFuture;
24
import java.util.concurrent.TimeUnit;
25
import javax.annotation.concurrent.ThreadSafe;
26

27
/**
28
 * A holder for shared resource singletons.
29
 *
30
 * <p>Components like client channels and servers need certain resources, e.g. a thread pool, to
31
 * run. If the user has not provided such resources, these components will use a default one, which
32
 * is shared as a static resource. This class holds these default resources and manages their
33
 * life-cycles.
34
 *
35
 * <p>A resource is identified by the reference of a {@link Resource} object, which is typically a
36
 * singleton, provided to the get() and release() methods. Each Resource object (not its class) maps
37
 * to an object cached in the holder.
38
 *
39
 * <p>Resources are ref-counted and shut down after a delay when the ref-count reaches zero.
40
 */
41
@ThreadSafe
42
public final class SharedResourceHolder {
43
  static final long DESTROY_DELAY_SECONDS = 1;
44

45
  // The sole holder instance.
46
  private static final SharedResourceHolder holder = new SharedResourceHolder(
1✔
47
      new ScheduledExecutorFactory() {
1✔
48
        @Override
49
        public ScheduledExecutorService createScheduledExecutor() {
50
          return Executors.newSingleThreadScheduledExecutor(
1✔
51
              GrpcUtil.getThreadFactory("grpc-shared-destroyer-%d", true));
1✔
52
        }
53
      });
54

55
  private final IdentityHashMap<Resource<?>, Instance> instances =
1✔
56
      new IdentityHashMap<>();
57

58
  private final ScheduledExecutorFactory destroyerFactory;
59

60
  private ScheduledExecutorService destroyer;
61

62
  // Visible to tests that would need to create instances of the holder.
63
  SharedResourceHolder(ScheduledExecutorFactory destroyerFactory) {
1✔
64
    this.destroyerFactory = destroyerFactory;
1✔
65
  }
1✔
66

67
  /**
68
   * Try to get an existing instance of the given resource. If an instance does not exist, create a
69
   * new one with the given factory.
70
   *
71
   * @param resource the singleton object that identifies the requested static resource
72
   */
73
  public static <T> T get(Resource<T> resource) {
74
    return holder.getInternal(resource);
1✔
75
  }
76

77
  /**
78
   * Releases an instance of the given resource.
79
   *
80
   * <p>The instance must have been obtained from {@link #get(Resource)}. Otherwise will throw
81
   * IllegalArgumentException.
82
   *
83
   * <p>Caller must not release a reference more than once. It's advisory that you clear the
84
   * reference to the instance with the null returned by this method.
85
   *
86
   * @param resource the singleton Resource object that identifies the released static resource
87
   * @param instance the released static resource
88
   *
89
   * @return a null which the caller can use to clear the reference to that instance.
90
   */
91
  public static <T> T release(final Resource<T> resource, final T instance) {
92
    return holder.releaseInternal(resource, instance);
1✔
93
  }
94

95
  /**
96
   * Visible to unit tests.
97
   *
98
   * @see #get(Resource)
99
   */
100
  @SuppressWarnings("unchecked")
101
  synchronized <T> T getInternal(Resource<T> resource) {
102
    Instance instance = instances.get(resource);
1✔
103
    if (instance == null) {
1✔
104
      instance = new Instance(resource.create());
1✔
105
      instances.put(resource, instance);
1✔
106
    }
107
    if (instance.destroyTask != null) {
1✔
108
      instance.destroyTask.cancel(false);
1✔
109
      instance.destroyTask = null;
1✔
110
    }
111
    instance.refcount++;
1✔
112
    return (T) instance.payload;
1✔
113
  }
114

115
  /**
116
   * Visible to unit tests.
117
   */
118
  synchronized <T> T releaseInternal(final Resource<T> resource, final T instance) {
119
    final Instance cached = instances.get(resource);
1✔
120
    if (cached == null) {
1✔
121
      throw new IllegalArgumentException("No cached instance found for " + resource);
1✔
122
    }
123
    Preconditions.checkArgument(instance == cached.payload, "Releasing the wrong instance");
1✔
124
    Preconditions.checkState(cached.refcount > 0, "Refcount has already reached zero");
1✔
125
    cached.refcount--;
1✔
126
    if (cached.refcount == 0) {
1✔
127
      Preconditions.checkState(cached.destroyTask == null, "Destroy task already scheduled");
1✔
128
      // Schedule a delayed task to destroy the resource.
129
      if (destroyer == null) {
1✔
130
        destroyer = destroyerFactory.createScheduledExecutor();
1✔
131
      }
132
      cached.destroyTask = destroyer.schedule(new LogExceptionRunnable(new Runnable() {
1✔
133
        @Override
134
        public void run() {
135
          synchronized (SharedResourceHolder.this) {
1✔
136
            // Refcount may have gone up since the task was scheduled. Re-check it.
137
            if (cached.refcount != 0) {
1✔
138
              return;
×
139
            }
140
            instances.remove(resource);
1✔
141
            if (instances.isEmpty()) {
1✔
142
              destroyer.shutdown();
1✔
143
              destroyer = null;
1✔
144
            }
145
          }
1✔
146
          resource.close(instance);
1✔
147
        }
1✔
148
      }), DESTROY_DELAY_SECONDS, TimeUnit.SECONDS);
149
    }
150
    // Always returning null
151
    return null;
1✔
152
  }
153

154
  /**
155
   * Defines a resource, and the way to create and destroy instances of it.
156
   */
157
  public interface Resource<T> {
158
    /**
159
     * Create a new instance of the resource.
160
     */
161
    T create();
162

163
    /**
164
     * Destroy the given instance.
165
     */
166
    void close(T instance);
167
  }
168

169
  interface ScheduledExecutorFactory {
170
    ScheduledExecutorService createScheduledExecutor();
171
  }
172

173
  private static class Instance {
174
    final Object payload;
175
    int refcount;
176
    ScheduledFuture<?> destroyTask;
177

178
    Instance(Object payload) {
1✔
179
      this.payload = payload;
1✔
180
    }
1✔
181
  }
182
}
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