• 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 io.grpc.ExperimentalApi;
26 import io.grpc.ManagedChannel;
27 import io.grpc.Server;
28 import java.util.ArrayList;
29 import java.util.Arrays;
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.TestRule;
35 import org.junit.runner.Description;
36 import org.junit.runners.model.MultipleFailureException;
37 import org.junit.runners.model.Statement;
38 
39 /**
40  * A JUnit {@link TestRule} that can register gRPC resources and manages its automatic release at
41  * the end of the test. If any of the resources registered to the rule can not be successfully
42  * released, the test will fail.
43  *
44  * @since 1.13.0
45  */
46 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2488")
47 @NotThreadSafe
48 public final class GrpcCleanupRule implements TestRule {
49 
50   private final List<Resource> resources = new ArrayList<>();
51   private long timeoutNanos = TimeUnit.SECONDS.toNanos(10L);
52   private Stopwatch stopwatch = Stopwatch.createUnstarted();
53 
54   private Throwable firstException;
55 
56   /**
57    * Sets a positive total time limit for the automatic resource cleanup. If any of the resources
58    * registered to the rule fails to be released in time, the test will fail.
59    *
60    * <p>Note that the resource cleanup duration may or may not be counted as part of the JUnit
61    * {@link org.junit.rules.Timeout Timeout} rule's test duration, depending on which rule is
62    * applied first.
63    *
64    * @return this
65    */
setTimeout(long timeout, TimeUnit timeUnit)66   public GrpcCleanupRule setTimeout(long timeout, TimeUnit timeUnit) {
67     checkArgument(timeout > 0, "timeout should be positive");
68     timeoutNanos = timeUnit.toNanos(timeout);
69     return this;
70   }
71 
72   /**
73    * Sets a specified time source for monitoring cleanup timeout.
74    *
75    * @return this
76    */
77   @SuppressWarnings("BetaApi") // Stopwatch.createUnstarted(Ticker ticker) is not Beta. Test only.
78   @VisibleForTesting
setTicker(Ticker ticker)79   GrpcCleanupRule setTicker(Ticker ticker) {
80     this.stopwatch = Stopwatch.createUnstarted(ticker);
81     return this;
82   }
83 
84   /**
85    * Registers the given channel to the rule. Once registered, the channel will be automatically
86    * shutdown at the end of the test.
87    *
88    * <p>This method need be properly synchronized if used in multiple threads. This method must
89    * not be used during the test teardown.
90    *
91    * @return the input channel
92    */
register(@onnull T channel)93   public <T extends ManagedChannel> T register(@Nonnull T channel) {
94     checkNotNull(channel, "channel");
95     register(new ManagedChannelResource(channel));
96     return channel;
97   }
98 
99   /**
100    * Registers the given server to the rule. Once registered, the server will be automatically
101    * shutdown at the end of the test.
102    *
103    * <p>This method need be properly synchronized if used in multiple threads. This method must
104    * not be used during the test teardown.
105    *
106    * @return the input server
107    */
register(@onnull T server)108   public <T extends Server> T register(@Nonnull T server) {
109     checkNotNull(server, "server");
110     register(new ServerResource(server));
111     return server;
112   }
113 
114   @VisibleForTesting
register(Resource resource)115   void register(Resource resource) {
116     resources.add(resource);
117   }
118 
119   @Override
apply(final Statement base, Description description)120   public Statement apply(final Statement base, Description description) {
121     return new Statement() {
122       @Override
123       public void evaluate() throws Throwable {
124         try {
125           base.evaluate();
126         } catch (Throwable t) {
127           firstException = t;
128 
129           try {
130             teardown();
131           } catch (Throwable t2) {
132             throw new MultipleFailureException(Arrays.asList(t, t2));
133           }
134 
135           throw t;
136         }
137 
138         teardown();
139         if (firstException != null) {
140           throw firstException;
141         }
142       }
143     };
144   }
145 
146   /**
147    * Releases all the registered resources.
148    */
149   private void teardown() {
150     stopwatch.start();
151 
152     if (firstException == null) {
153       for (int i = resources.size() - 1; i >= 0; i--) {
154         resources.get(i).cleanUp();
155       }
156     }
157 
158     for (int i = resources.size() - 1; i >= 0; i--) {
159       if (firstException != null) {
160         resources.get(i).forceCleanUp();
161         continue;
162       }
163 
164       try {
165         boolean released = resources.get(i).awaitReleased(
166             timeoutNanos - stopwatch.elapsed(TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS);
167         if (!released) {
168           firstException = new AssertionError(
169               "Resource " + resources.get(i) + " can not be released in time at the end of test");
170         }
171       } catch (InterruptedException e) {
172         Thread.currentThread().interrupt();
173         firstException = e;
174       }
175 
176       if (firstException != null) {
177         resources.get(i).forceCleanUp();
178       }
179     }
180 
181     resources.clear();
182   }
183 
184   @VisibleForTesting
185   interface Resource {
186     void cleanUp();
187 
188     /**
189      * Error already happened, try the best to clean up. Never throws.
190      */
191     void forceCleanUp();
192 
193     /**
194      * Returns true if the resource is released in time.
195      */
196     boolean awaitReleased(long duration, TimeUnit timeUnit) throws InterruptedException;
197   }
198 
199   private static final class ManagedChannelResource implements Resource {
200     final ManagedChannel channel;
201 
202     ManagedChannelResource(ManagedChannel channel) {
203       this.channel = channel;
204     }
205 
206     @Override
207     public void cleanUp() {
208       channel.shutdown();
209     }
210 
211     @Override
212     public void forceCleanUp() {
213       channel.shutdownNow();
214     }
215 
216     @Override
217     public boolean awaitReleased(long duration, TimeUnit timeUnit) throws InterruptedException {
218       return channel.awaitTermination(duration, timeUnit);
219     }
220 
221     @Override
222     public String toString() {
223       return channel.toString();
224     }
225   }
226 
227   private static final class ServerResource implements Resource {
228     final Server server;
229 
230     ServerResource(Server server) {
231       this.server = server;
232     }
233 
234     @Override
235     public void cleanUp() {
236       server.shutdown();
237     }
238 
239     @Override
240     public void forceCleanUp() {
241       server.shutdownNow();
242     }
243 
244     @Override
245     public boolean awaitReleased(long duration, TimeUnit timeUnit) throws InterruptedException {
246       return server.awaitTermination(duration, timeUnit);
247     }
248 
249     @Override
250     public String toString() {
251       return server.toString();
252     }
253   }
254 }
255