• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.testing;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static com.google.common.base.Preconditions.checkNotNull;
21 
22 import com.google.common.annotations.VisibleForTesting;
23 import com.google.common.base.Stopwatch;
24 import com.google.common.base.Ticker;
25 import com.google.common.collect.Lists;
26 import io.grpc.ExperimentalApi;
27 import io.grpc.ManagedChannel;
28 import io.grpc.Server;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.concurrent.TimeUnit;
32 import javax.annotation.Nonnull;
33 import javax.annotation.concurrent.NotThreadSafe;
34 import org.junit.rules.ExternalResource;
35 import org.junit.runner.Description;
36 import org.junit.runners.model.Statement;
37 
38 /**
39  * A JUnit {@link ExternalResource} that can register gRPC resources and manages its automatic
40  * release at the end of the test. If any of the resources registered to the rule can not be
41  * successfully released, the test will fail.
42  *
43  * <p>Example usage:
44  * <pre>{@code @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
45  * ...
46  * // The Channel and Server can be created in any order
47  * grpcCleanup.register(
48  *     InProcessServerBuilder.forName("my-test-case")
49  *         .directExecutor()
50  *         .addService(serviceImpl)
51  *         .build()
52  *         .start());
53  * ManagedChannel channel = grpcCleanup.register(
54  *     InProcessChannelBuilder.forName("my-test-case")
55  *         .directExecutor()
56  *         .build());
57  * }</pre>
58  *
59  * <p>To use as a replacement for {@link GrpcServerRule}:
60  * <pre>{@code String serverName = InProcessServerBuilder.generateName();
61  * MutableHandlerRegistry serviceRegistry = new MutableHandlerRegistry();
62  * Server server = grpcCleanup.register(
63  *     InProcessServerBuilder.forName(serverName)
64  *         .fallbackHandlerRegistry(serviceRegistry)
65  *         .build()
66  *         .start());
67  * ManagedChannel channel = grpcCleanup.register(
68  *     InProcessChannelBuilder.forName(serverName).build());
69  * }</pre>
70  *
71  * @since 1.13.0
72  */
73 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2488")
74 @NotThreadSafe
75 public final class GrpcCleanupRule extends ExternalResource {
76 
77   private final List<Resource> resources = new ArrayList<>();
78   private long timeoutNanos = TimeUnit.SECONDS.toNanos(10L);
79   private Stopwatch stopwatch = Stopwatch.createUnstarted();
80 
81   private boolean abruptShutdown;
82 
83   /**
84    * Sets a positive total time limit for the automatic resource cleanup. If any of the resources
85    * registered to the rule fails to be released in time, the test will fail.
86    *
87    * <p>Note that the resource cleanup duration may or may not be counted as part of the JUnit
88    * {@link org.junit.rules.Timeout Timeout} rule's test duration, depending on which rule is
89    * applied first.
90    *
91    * @return this
92    */
setTimeout(long timeout, TimeUnit timeUnit)93   public GrpcCleanupRule setTimeout(long timeout, TimeUnit timeUnit) {
94     checkArgument(timeout > 0, "timeout should be positive");
95     timeoutNanos = timeUnit.toNanos(timeout);
96     return this;
97   }
98 
99   /**
100    * Sets a specified time source for monitoring cleanup timeout.
101    *
102    * @return this
103    */
104   @SuppressWarnings("BetaApi") // Test only.
105   @VisibleForTesting
setTicker(Ticker ticker)106   GrpcCleanupRule setTicker(Ticker ticker) {
107     this.stopwatch = Stopwatch.createUnstarted(ticker);
108     return this;
109   }
110 
111   /**
112    * Registers the given channel to the rule. Once registered, the channel will be automatically
113    * shutdown at the end of the test.
114    *
115    * <p>This method need be properly synchronized if used in multiple threads. This method must
116    * not be used during the test teardown.
117    *
118    * @return the input channel
119    */
register(@onnull T channel)120   public <T extends ManagedChannel> T register(@Nonnull T channel) {
121     checkNotNull(channel, "channel");
122     register(new ManagedChannelResource(channel));
123     return channel;
124   }
125 
126   /**
127    * Registers the given server to the rule. Once registered, the server will be automatically
128    * shutdown at the end of the test.
129    *
130    * <p>This method need be properly synchronized if used in multiple threads. This method must
131    * not be used during the test teardown.
132    *
133    * @return the input server
134    */
register(@onnull T server)135   public <T extends Server> T register(@Nonnull T server) {
136     checkNotNull(server, "server");
137     register(new ServerResource(server));
138     return server;
139   }
140 
141   @VisibleForTesting
register(Resource resource)142   void register(Resource resource) {
143     resources.add(resource);
144   }
145 
146   // The class extends ExternalResource so it can be used in JUnit 5. But JUnit 5 will only call
147   // before() and after(), thus code cannot assume this method will be called.
148   @Override
apply(final Statement base, Description description)149   public Statement apply(final Statement base, Description description) {
150     return super.apply(new Statement() {
151       @Override
152       public void evaluate() throws Throwable {
153         abruptShutdown = false;
154         try {
155           base.evaluate();
156         } catch (Throwable t) {
157           abruptShutdown = true;
158           throw t;
159         }
160       }
161     }, description);
162   }
163 
164   /**
165    * Releases all the registered resources.
166    */
167   @Override
168   protected void after() {
169     stopwatch.reset();
170     stopwatch.start();
171 
172     InterruptedException interrupted = null;
173     if (!abruptShutdown) {
174       for (Resource resource : Lists.reverse(resources)) {
175         resource.cleanUp();
176       }
177 
178       for (int i = resources.size() - 1; i >= 0; i--) {
179         try {
180           boolean released = resources.get(i).awaitReleased(
181               timeoutNanos - stopwatch.elapsed(TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS);
182           if (released) {
183             resources.remove(i);
184           }
185         } catch (InterruptedException e) {
186           Thread.currentThread().interrupt();
187           interrupted = e;
188           break;
189         }
190       }
191     }
192 
193     if (!resources.isEmpty()) {
194       for (Resource resource : Lists.reverse(resources)) {
195         resource.forceCleanUp();
196       }
197 
198       try {
199         if (interrupted != null) {
200           throw new AssertionError(
201               "Thread interrupted before resources gracefully released", interrupted);
202         } else if (!abruptShutdown) {
203           throw new AssertionError(
204             "Resources could not be released in time at the end of test: " + resources);
205         }
206       } finally {
207         resources.clear();
208       }
209     }
210   }
211 
212   @VisibleForTesting
213   interface Resource {
214     void cleanUp();
215 
216     /**
217      * Error already happened, try the best to clean up. Never throws.
218      */
219     void forceCleanUp();
220 
221     /**
222      * Returns true if the resource is released in time.
223      */
224     boolean awaitReleased(long duration, TimeUnit timeUnit) throws InterruptedException;
225   }
226 
227   private static final class ManagedChannelResource implements Resource {
228     final ManagedChannel channel;
229 
230     ManagedChannelResource(ManagedChannel channel) {
231       this.channel = channel;
232     }
233 
234     @Override
235     public void cleanUp() {
236       channel.shutdown();
237     }
238 
239     @Override
240     public void forceCleanUp() {
241       channel.shutdownNow();
242     }
243 
244     @Override
245     public boolean awaitReleased(long duration, TimeUnit timeUnit) throws InterruptedException {
246       return channel.awaitTermination(duration, timeUnit);
247     }
248 
249     @Override
250     public String toString() {
251       return channel.toString();
252     }
253   }
254 
255   private static final class ServerResource implements Resource {
256     final Server server;
257 
258     ServerResource(Server server) {
259       this.server = server;
260     }
261 
262     @Override
263     public void cleanUp() {
264       server.shutdown();
265     }
266 
267     @Override
268     public void forceCleanUp() {
269       server.shutdownNow();
270     }
271 
272     @Override
273     public boolean awaitReleased(long duration, TimeUnit timeUnit) throws InterruptedException {
274       return server.awaitTermination(duration, timeUnit);
275     }
276 
277     @Override
278     public String toString() {
279       return server.toString();
280     }
281   }
282 }
283