LCOV - code coverage report
Current view: top level - server/plugincontext - PluginContext.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 67 112 59.8 %
Date: 2022-11-19 15:00:39 Functions: 11 16 68.8 %

          Line data    Source code
       1             : // Copyright (C) 2018 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.plugincontext;
      16             : 
      17             : import static java.util.Objects.requireNonNull;
      18             : 
      19             : import com.google.common.base.Strings;
      20             : import com.google.common.base.Throwables;
      21             : import com.google.common.flogger.FluentLogger;
      22             : import com.google.gerrit.extensions.registration.DynamicItem;
      23             : import com.google.gerrit.extensions.registration.DynamicMap;
      24             : import com.google.gerrit.extensions.registration.DynamicSet;
      25             : import com.google.gerrit.extensions.registration.Extension;
      26             : import com.google.gerrit.metrics.Counter3;
      27             : import com.google.gerrit.metrics.Description;
      28             : import com.google.gerrit.metrics.Description.Units;
      29             : import com.google.gerrit.metrics.DisabledMetricMaker;
      30             : import com.google.gerrit.metrics.Field;
      31             : import com.google.gerrit.metrics.MetricMaker;
      32             : import com.google.gerrit.metrics.Timer3;
      33             : import com.google.gerrit.server.cancellation.RequestCancelledException;
      34             : import com.google.gerrit.server.logging.Metadata;
      35             : import com.google.gerrit.server.logging.TraceContext;
      36             : import com.google.inject.Inject;
      37             : import com.google.inject.Singleton;
      38             : 
      39             : /**
      40             :  * Context for invoking plugin extensions.
      41             :  *
      42             :  * <p>Invoking a plugin extension through a PluginContext sets a logging tag with the plugin name is
      43             :  * set. This way any errors that are triggered by the plugin extension (even if they happen in
      44             :  * Gerrit code which is called by the plugin extension) can be easily attributed to the plugin.
      45             :  *
      46             :  * <p>If possible plugin extensions should be invoked through:
      47             :  *
      48             :  * <ul>
      49             :  *   <li>{@link PluginItemContext} for extensions from {@link DynamicItem}
      50             :  *   <li>{@link PluginSetContext} for extensions from {@link DynamicSet}
      51             :  *   <li>{@link PluginMapContext} for extensions from {@link DynamicMap}
      52             :  * </ul>
      53             :  *
      54             :  * <p>A plugin context can be manually opened by invoking the newTrace methods. This should only be
      55             :  * needed if an extension throws multiple exceptions that need to be handled:
      56             :  *
      57             :  * <pre>{@code
      58             :  * public interface Foo {
      59             :  *   void doFoo() throws Exception1, Exception2, Exception3;
      60             :  * }
      61             :  *
      62             :  * ...
      63             :  *
      64             :  * for (Extension<Foo> fooExtension : fooDynamicMap) {
      65             :  *   try (TraceContext traceContext = PluginContext.newTrace(fooExtension)) {
      66             :  *     fooExtension.get().doFoo();
      67             :  *   }
      68             :  * }
      69             :  * }</pre>
      70             :  *
      71             :  * <p>This class hosts static methods with generic functionality to invoke plugin extensions with a
      72             :  * trace context that are commonly used by {@link PluginItemContext}, {@link PluginSetContext} and
      73             :  * {@link PluginMapContext}.
      74             :  *
      75             :  * <p>The run* methods execute an extension but don't deliver a result back to the caller.
      76             :  * Exceptions can be caught and logged.
      77             :  *
      78             :  * <p>The call* methods execute an extension and deliver a result back to the caller.
      79             :  */
      80           0 : public class PluginContext<T> {
      81         151 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      82             : 
      83             :   @FunctionalInterface
      84             :   public interface ExtensionImplConsumer<T> {
      85             :     void run(T t) throws Exception;
      86             :   }
      87             : 
      88             :   @FunctionalInterface
      89             :   public interface ExtensionImplFunction<T, R> {
      90             :     R call(T input);
      91             :   }
      92             : 
      93             :   @FunctionalInterface
      94             :   public interface CheckedExtensionImplFunction<T, R, X extends Exception> {
      95             :     R call(T input) throws X;
      96             :   }
      97             : 
      98             :   @FunctionalInterface
      99             :   public interface ExtensionConsumer<T extends Extension<?>> {
     100             :     void run(T extension) throws Exception;
     101             :   }
     102             : 
     103             :   @FunctionalInterface
     104             :   public interface ExtensionFunction<T extends Extension<?>, R> {
     105             :     R call(T extension);
     106             :   }
     107             : 
     108             :   @FunctionalInterface
     109             :   public interface CheckedExtensionFunction<T extends Extension<?>, R, X extends Exception> {
     110             :     R call(T extension) throws X;
     111             :   }
     112             : 
     113             :   @Singleton
     114             :   public static class PluginMetrics {
     115         152 :     public static final PluginMetrics DISABLED_INSTANCE =
     116             :         new PluginMetrics(new DisabledMetricMaker());
     117             : 
     118             :     final Timer3<String, String, String> latency;
     119             :     final Counter3<String, String, String> errorCount;
     120             : 
     121             :     @Inject
     122         152 :     PluginMetrics(MetricMaker metricMaker) {
     123         152 :       Field<String> pluginNameField =
     124         152 :           Field.ofString("plugin_name", Metadata.Builder::pluginName)
     125         152 :               .description("The name of the plugin.")
     126         152 :               .build();
     127         152 :       Field<String> classNameField =
     128         152 :           Field.ofString("class_name", Metadata.Builder::className)
     129         152 :               .description("The class of the plugin that was invoked.")
     130         152 :               .build();
     131         152 :       Field<String> exportValueField =
     132         152 :           Field.ofString("export_value", Metadata.Builder::exportValue)
     133         152 :               .description("The export name under which the invoked class is registered.")
     134         152 :               .build();
     135             : 
     136         152 :       this.latency =
     137         152 :           metricMaker.newTimer(
     138             :               "plugin/latency",
     139             :               new Description("Latency for plugin invocation")
     140         152 :                   .setCumulative()
     141         152 :                   .setUnit(Units.MILLISECONDS),
     142             :               pluginNameField,
     143             :               classNameField,
     144             :               exportValueField);
     145         152 :       this.errorCount =
     146         152 :           metricMaker.newCounter(
     147             :               "plugin/error_count",
     148         152 :               new Description("Number of plugin errors").setCumulative().setUnit("errors"),
     149             :               pluginNameField,
     150             :               classNameField,
     151             :               exportValueField);
     152         152 :     }
     153             : 
     154             :     Timer3.Context<String, String, String> startLatency(Extension<?> extension) {
     155         151 :       return latency.start(
     156         151 :           extension.getPluginName(),
     157         151 :           extension.get().getClass().getName(),
     158         151 :           Strings.nullToEmpty(extension.getExportName()));
     159             :     }
     160             : 
     161             :     void incrementErrorCount(Extension<?> extension) {
     162           3 :       errorCount.increment(
     163           3 :           extension.getPluginName(),
     164           3 :           extension.get().getClass().getName(),
     165           3 :           Strings.nullToEmpty(extension.getExportName()));
     166           3 :     }
     167             :   }
     168             : 
     169             :   /**
     170             :    * Opens a new trace context for invoking a plugin extension.
     171             :    *
     172             :    * @param dynamicItem dynamic item that holds the extension implementation that is being invoked
     173             :    *     from within the trace context
     174             :    * @return the created trace context
     175             :    */
     176             :   public static <T> TraceContext newTrace(DynamicItem<T> dynamicItem) {
     177          53 :     Extension<T> extension = dynamicItem.getEntry();
     178          53 :     if (extension == null) {
     179           0 :       return TraceContext.open();
     180             :     }
     181          53 :     return newTrace(extension);
     182             :   }
     183             : 
     184             :   /**
     185             :    * Opens a new trace context for invoking a plugin extension.
     186             :    *
     187             :    * @param extension extension that is being invoked from within the trace context
     188             :    * @return the created trace context
     189             :    */
     190             :   public static <T> TraceContext newTrace(Extension<T> extension) {
     191         151 :     return TraceContext.open().addPluginTag(requireNonNull(extension).getPluginName());
     192             :   }
     193             : 
     194             :   /**
     195             :    * Runs a plugin extension. All exceptions from the plugin extension are caught and logged (except
     196             :    * {@link RequestCancelledException}.
     197             :    *
     198             :    * <p>The consumer gets the extension implementation provided that should be invoked.
     199             :    *
     200             :    * @param pluginMetrics the plugin metrics
     201             :    * @param extension extension that is being invoked
     202             :    * @param extensionImplConsumer the consumer that invokes the extension
     203             :    */
     204             :   static <T> void runLogExceptions(
     205             :       PluginMetrics pluginMetrics,
     206             :       Extension<T> extension,
     207             :       ExtensionImplConsumer<T> extensionImplConsumer) {
     208         151 :     T extensionImpl = extension.get();
     209         151 :     if (extensionImpl == null) {
     210           0 :       return;
     211             :     }
     212         151 :     try (TraceContext traceContext = newTrace(extension);
     213         151 :         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
     214         151 :       extensionImplConsumer.run(extensionImpl);
     215           5 :     } catch (Exception e) {
     216           3 :       Throwables.throwIfInstanceOf(e, RequestCancelledException.class);
     217           3 :       pluginMetrics.incrementErrorCount(extension);
     218           3 :       logger.atWarning().withCause(e).log(
     219           3 :           "Failure in %s of plugin %s", extensionImpl.getClass(), extension.getPluginName());
     220         151 :     }
     221         151 :   }
     222             : 
     223             :   /**
     224             :    * Runs a plugin extension. All exceptions from the plugin extension are caught and logged (except
     225             :    * {@link RequestCancelledException}.
     226             :    *
     227             :    * <p>The consumer get the {@link Extension} provided that should be invoked. The extension
     228             :    * provides access to the plugin name and the export name.
     229             :    *
     230             :    * @param pluginMetrics the plugin metrics
     231             :    * @param extension extension that is being invoked
     232             :    * @param extensionConsumer the consumer that invokes the extension
     233             :    */
     234             :   static <T> void runLogExceptions(
     235             :       PluginMetrics pluginMetrics,
     236             :       Extension<T> extension,
     237             :       ExtensionConsumer<Extension<T>> extensionConsumer) {
     238           1 :     T extensionImpl = extension.get();
     239           1 :     if (extensionImpl == null) {
     240           0 :       return;
     241             :     }
     242             : 
     243           1 :     try (TraceContext traceContext = newTrace(extension);
     244           1 :         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
     245           1 :       extensionConsumer.run(extension);
     246           0 :     } catch (Exception e) {
     247           0 :       Throwables.throwIfInstanceOf(e, RequestCancelledException.class);
     248           0 :       pluginMetrics.incrementErrorCount(extension);
     249           0 :       logger.atWarning().withCause(e).log(
     250           0 :           "Failure in %s of plugin %s", extensionImpl.getClass(), extension.getPluginName());
     251           1 :     }
     252           1 :   }
     253             : 
     254             :   /**
     255             :    * Runs a plugin extension. All exceptions from the plugin extension except exceptions of the
     256             :    * specified type are caught and logged. Exceptions of the specified type are thrown and must be
     257             :    * handled by the caller.
     258             :    *
     259             :    * <p>The consumer gets the extension implementation provided that should be invoked.
     260             :    *
     261             :    * @param pluginMetrics the plugin metrics
     262             :    * @param extension extension that is being invoked
     263             :    * @param extensionImplConsumer the consumer that invokes the extension
     264             :    * @param exceptionClass type of the exceptions that should be thrown
     265             :    * @throws X expected exception from the plugin extension
     266             :    */
     267             :   static <T, X extends Exception> void runLogExceptions(
     268             :       PluginMetrics pluginMetrics,
     269             :       Extension<T> extension,
     270             :       ExtensionImplConsumer<T> extensionImplConsumer,
     271             :       Class<X> exceptionClass)
     272             :       throws X {
     273         148 :     T extensionImpl = extension.get();
     274         148 :     if (extensionImpl == null) {
     275           0 :       return;
     276             :     }
     277             : 
     278         148 :     try (TraceContext traceContext = newTrace(extension);
     279         148 :         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
     280         148 :       extensionImplConsumer.run(extensionImpl);
     281          13 :     } catch (Exception e) {
     282           2 :       Throwables.throwIfInstanceOf(e, exceptionClass);
     283           0 :       Throwables.throwIfUnchecked(e);
     284           0 :       pluginMetrics.incrementErrorCount(extension);
     285           0 :       logger.atWarning().withCause(e).log(
     286           0 :           "Failure in %s of plugin %s", extensionImpl.getClass(), extension.getPluginName());
     287         148 :     }
     288         148 :   }
     289             : 
     290             :   /**
     291             :    * Runs a plugin extension. All exceptions from the plugin extension except exceptions of the
     292             :    * specified type are caught and logged. Exceptions of the specified type are thrown and must be
     293             :    * handled by the caller.
     294             :    *
     295             :    * <p>The consumer get the {@link Extension} provided that should be invoked. The extension
     296             :    * provides access to the plugin name and the export name.
     297             :    *
     298             :    * @param pluginMetrics the plugin metrics
     299             :    * @param extension extension that is being invoked
     300             :    * @param extensionConsumer the consumer that invokes the extension
     301             :    * @param exceptionClass type of the exceptions that should be thrown
     302             :    * @throws X expected exception from the plugin extension
     303             :    */
     304             :   static <T, X extends Exception> void runLogExceptions(
     305             :       PluginMetrics pluginMetrics,
     306             :       Extension<T> extension,
     307             :       ExtensionConsumer<Extension<T>> extensionConsumer,
     308             :       Class<X> exceptionClass)
     309             :       throws X {
     310           0 :     T extensionImpl = extension.get();
     311           0 :     if (extensionImpl == null) {
     312           0 :       return;
     313             :     }
     314             : 
     315           0 :     try (TraceContext traceContext = newTrace(extension);
     316           0 :         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
     317           0 :       extensionConsumer.run(extension);
     318           0 :     } catch (Exception e) {
     319           0 :       Throwables.throwIfInstanceOf(e, exceptionClass);
     320           0 :       Throwables.throwIfUnchecked(e);
     321           0 :       pluginMetrics.incrementErrorCount(extension);
     322           0 :       logger.atWarning().withCause(e).log(
     323           0 :           "Failure in %s of plugin %s", extensionImpl.getClass(), extension.getPluginName());
     324           0 :     }
     325           0 :   }
     326             : 
     327             :   /**
     328             :    * Calls a plugin extension and returns the result from the plugin extension call.
     329             :    *
     330             :    * <p>The function gets the extension implementation provided that should be invoked.
     331             :    *
     332             :    * @param pluginMetrics the plugin metrics
     333             :    * @param extension extension that is being invoked
     334             :    * @param extensionImplFunction function that invokes the extension
     335             :    * @return the result from the plugin extension
     336             :    */
     337             :   static <T, R> R call(
     338             :       PluginMetrics pluginMetrics,
     339             :       Extension<T> extension,
     340             :       ExtensionImplFunction<T, R> extensionImplFunction) {
     341         150 :     try (TraceContext traceContext = newTrace(extension);
     342         150 :         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
     343         150 :       return extensionImplFunction.call(extension.get());
     344             :     }
     345             :   }
     346             : 
     347             :   /**
     348             :    * Calls a plugin extension and returns the result from the plugin extension call. Exceptions of
     349             :    * the specified type are thrown and must be handled by the caller.
     350             :    *
     351             :    * <p>The function gets the extension implementation provided that should be invoked.
     352             :    *
     353             :    * @param pluginMetrics the plugin metrics
     354             :    * @param extension extension that is being invoked
     355             :    * @param checkedExtensionImplFunction function that invokes the extension
     356             :    * @param exceptionClass type of the exceptions that should be thrown
     357             :    * @return the result from the plugin extension
     358             :    * @throws X expected exception from the plugin extension
     359             :    */
     360             :   static <T, R, X extends Exception> R call(
     361             :       PluginMetrics pluginMetrics,
     362             :       Extension<T> extension,
     363             :       CheckedExtensionImplFunction<T, R, X> checkedExtensionImplFunction,
     364             :       Class<X> exceptionClass)
     365             :       throws X {
     366           0 :     try (TraceContext traceContext = newTrace(extension);
     367           0 :         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
     368             :       try {
     369           0 :         return checkedExtensionImplFunction.call(extension.get());
     370           0 :       } catch (Exception e) {
     371             :         // The only exception that can be thrown is X, but we cannot catch X since it is a generic
     372             :         // type.
     373           0 :         Throwables.throwIfInstanceOf(e, exceptionClass);
     374           0 :         Throwables.throwIfUnchecked(e);
     375           0 :         throw new IllegalStateException("unexpected exception: " + e.getMessage(), e);
     376             :       }
     377             :     }
     378             :   }
     379             : 
     380             :   /**
     381             :    * Calls a plugin extension and returns the result from the plugin extension call.
     382             :    *
     383             :    * <p>The function get the {@link Extension} provided that should be invoked. The extension
     384             :    * provides access to the plugin name and the export name.
     385             :    *
     386             :    * @param pluginMetrics the plugin metrics
     387             :    * @param extension extension that is being invoked
     388             :    * @param extensionFunction function that invokes the extension
     389             :    * @return the result from the plugin extension
     390             :    */
     391             :   static <T, R> R call(
     392             :       PluginMetrics pluginMetrics,
     393             :       Extension<T> extension,
     394             :       ExtensionFunction<Extension<T>, R> extensionFunction) {
     395           0 :     try (TraceContext traceContext = newTrace(extension);
     396           0 :         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
     397           0 :       return extensionFunction.call(extension);
     398             :     }
     399             :   }
     400             : 
     401             :   /**
     402             :    * Calls a plugin extension and returns the result from the plugin extension call. Exceptions of
     403             :    * the specified type are thrown and must be handled by the caller.
     404             :    *
     405             :    * <p>The function get the {@link Extension} provided that should be invoked. The extension
     406             :    * provides access to the plugin name and the export name.
     407             :    *
     408             :    * @param pluginMetrics the plugin metrics
     409             :    * @param extension extension that is being invoked
     410             :    * @param checkedExtensionFunction function that invokes the extension
     411             :    * @param exceptionClass type of the exceptions that should be thrown
     412             :    * @return the result from the plugin extension
     413             :    * @throws X expected exception from the plugin extension
     414             :    */
     415             :   static <T, R, X extends Exception> R call(
     416             :       PluginMetrics pluginMetrics,
     417             :       Extension<T> extension,
     418             :       CheckedExtensionFunction<Extension<T>, R, X> checkedExtensionFunction,
     419             :       Class<X> exceptionClass)
     420             :       throws X {
     421           0 :     try (TraceContext traceContext = newTrace(extension);
     422           0 :         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
     423             :       try {
     424           0 :         return checkedExtensionFunction.call(extension);
     425           0 :       } catch (Exception e) {
     426             :         // The only exception that can be thrown is X, but we cannot catch X since it is a generic
     427             :         // type.
     428           0 :         Throwables.throwIfInstanceOf(e, exceptionClass);
     429           0 :         Throwables.throwIfUnchecked(e);
     430           0 :         throw new IllegalStateException("unexpected exception: " + e.getMessage(), e);
     431             :       }
     432             :     }
     433             :   }
     434             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750