Home | History | Annotate | Download | only in testing
      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    */
     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
     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    */
     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    */
    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
    115   void register(Resource resource) {
    116     resources.add(resource);
    117   }
    118 
    119   @Override
    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