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