com.google.gerrit.server.plugincontext.PluginContext Maven / Gradle / Ivy
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.plugincontext;
import static java.util.Objects.requireNonNull;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.Extension;
import com.google.gerrit.metrics.Counter3;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
import com.google.gerrit.metrics.DisabledMetricMaker;
import com.google.gerrit.metrics.Field;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer3;
import com.google.gerrit.server.cancellation.RequestCancelledException;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
/**
 * Context for invoking plugin extensions.
 *
 * Invoking a plugin extension through a PluginContext sets a logging tag with the plugin name is
 * set. This way any errors that are triggered by the plugin extension (even if they happen in
 * Gerrit code which is called by the plugin extension) can be easily attributed to the plugin.
 *
 * 
If possible plugin extensions should be invoked through:
 *
 * 
 *   - {@link PluginItemContext} for extensions from {@link DynamicItem}
 *   
 - {@link PluginSetContext} for extensions from {@link DynamicSet}
 *   
 - {@link PluginMapContext} for extensions from {@link DynamicMap}
 * 
 
 *
 * A plugin context can be manually opened by invoking the newTrace methods. This should only be
 * needed if an extension throws multiple exceptions that need to be handled:
 *
 * 
{@code
 * public interface Foo {
 *   void doFoo() throws Exception1, Exception2, Exception3;
 * }
 *
 * ...
 *
 * for (Extension fooExtension : fooDynamicMap) {
 *   try (TraceContext traceContext = PluginContext.newTrace(fooExtension)) {
 *     fooExtension.get().doFoo();
 *   }
 * }
 * } 
 *
 * This class hosts static methods with generic functionality to invoke plugin extensions with a
 * trace context that are commonly used by {@link PluginItemContext}, {@link PluginSetContext} and
 * {@link PluginMapContext}.
 *
 * 
The run* methods execute an extension but don't deliver a result back to the caller.
 * Exceptions can be caught and logged.
 *
 * 
The call* methods execute an extension and deliver a result back to the caller.
 */
public class PluginContext {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
  @FunctionalInterface
  public interface ExtensionImplConsumer {
    void run(T t) throws Exception;
  }
  @FunctionalInterface
  public interface ExtensionImplFunction {
    R call(T input);
  }
  @FunctionalInterface
  public interface CheckedExtensionImplFunction {
    R call(T input) throws X;
  }
  @FunctionalInterface
  public interface ExtensionConsumer> {
    void run(T extension) throws Exception;
  }
  @FunctionalInterface
  public interface ExtensionFunction, R> {
    R call(T extension);
  }
  @FunctionalInterface
  public interface CheckedExtensionFunction, R, X extends Exception> {
    R call(T extension) throws X;
  }
  @Singleton
  public static class PluginMetrics {
    public static final PluginMetrics DISABLED_INSTANCE =
        new PluginMetrics(new DisabledMetricMaker());
    final Timer3 latency;
    final Counter3 errorCount;
    @Inject
    PluginMetrics(MetricMaker metricMaker) {
      Field pluginNameField =
          Field.ofString("plugin_name", Metadata.Builder::pluginName)
              .description("The name of the plugin.")
              .build();
      Field classNameField =
          Field.ofString("class_name", Metadata.Builder::className)
              .description("The class of the plugin that was invoked.")
              .build();
      Field exportValueField =
          Field.ofString("export_value", Metadata.Builder::exportValue)
              .description("The export name under which the invoked class is registered.")
              .build();
      this.latency =
          metricMaker.newTimer(
              "plugin/latency",
              new Description("Latency for plugin invocation")
                  .setCumulative()
                  .setUnit(Units.MILLISECONDS),
              pluginNameField,
              classNameField,
              exportValueField);
      this.errorCount =
          metricMaker.newCounter(
              "plugin/error_count",
              new Description("Number of plugin errors").setCumulative().setUnit("errors"),
              pluginNameField,
              classNameField,
              exportValueField);
    }
    Timer3.Context startLatency(Extension> extension) {
      return latency.start(
          extension.getPluginName(),
          extension.get().getClass().getName(),
          Strings.nullToEmpty(extension.getExportName()));
    }
    void incrementErrorCount(Extension> extension) {
      errorCount.increment(
          extension.getPluginName(),
          extension.get().getClass().getName(),
          Strings.nullToEmpty(extension.getExportName()));
    }
  }
  /**
   * Opens a new trace context for invoking a plugin extension.
   *
   * @param dynamicItem dynamic item that holds the extension implementation that is being invoked
   *     from within the trace context
   * @return the created trace context
   */
  public static  TraceContext newTrace(DynamicItem dynamicItem) {
    Extension extension = dynamicItem.getEntry();
    if (extension == null) {
      return TraceContext.open();
    }
    return newTrace(extension);
  }
  /**
   * Opens a new trace context for invoking a plugin extension.
   *
   * @param extension extension that is being invoked from within the trace context
   * @return the created trace context
   */
  public static  TraceContext newTrace(Extension extension) {
    return TraceContext.open().addPluginTag(requireNonNull(extension).getPluginName());
  }
  /**
   * Runs a plugin extension. All exceptions from the plugin extension are caught and logged (except
   * {@link RequestCancelledException}.
   *
   * The consumer gets the extension implementation provided that should be invoked.
   *
   * @param pluginMetrics the plugin metrics
   * @param extension extension that is being invoked
   * @param extensionImplConsumer the consumer that invokes the extension
   */
  static  void runLogExceptions(
      PluginMetrics pluginMetrics,
      Extension extension,
      ExtensionImplConsumer extensionImplConsumer) {
    T extensionImpl = extension.get();
    if (extensionImpl == null) {
      return;
    }
    try (TraceContext traceContext = newTrace(extension);
        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
      extensionImplConsumer.run(extensionImpl);
    } catch (Exception e) {
      Throwables.throwIfInstanceOf(e, RequestCancelledException.class);
      pluginMetrics.incrementErrorCount(extension);
      logger.atWarning().withCause(e).log(
          "Failure in %s of plugin %s", extensionImpl.getClass(), extension.getPluginName());
    }
  }
  /**
   * Runs a plugin extension. All exceptions from the plugin extension are caught and logged (except
   * {@link RequestCancelledException}.
   *
   * The consumer get the {@link Extension} provided that should be invoked. The extension
   * provides access to the plugin name and the export name.
   *
   * @param pluginMetrics the plugin metrics
   * @param extension extension that is being invoked
   * @param extensionConsumer the consumer that invokes the extension
   */
  static  void runLogExceptions(
      PluginMetrics pluginMetrics,
      Extension extension,
      ExtensionConsumer> extensionConsumer) {
    T extensionImpl = extension.get();
    if (extensionImpl == null) {
      return;
    }
    try (TraceContext traceContext = newTrace(extension);
        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
      extensionConsumer.run(extension);
    } catch (Exception e) {
      Throwables.throwIfInstanceOf(e, RequestCancelledException.class);
      pluginMetrics.incrementErrorCount(extension);
      logger.atWarning().withCause(e).log(
          "Failure in %s of plugin %s", extensionImpl.getClass(), extension.getPluginName());
    }
  }
  /**
   * Runs a plugin extension. All exceptions from the plugin extension except exceptions of the
   * specified type are caught and logged. Exceptions of the specified type are thrown and must be
   * handled by the caller.
   *
   * The consumer gets the extension implementation provided that should be invoked.
   *
   * @param pluginMetrics the plugin metrics
   * @param extension extension that is being invoked
   * @param extensionImplConsumer the consumer that invokes the extension
   * @param exceptionClass type of the exceptions that should be thrown
   * @throws X expected exception from the plugin extension
   */
  static  void runLogExceptions(
      PluginMetrics pluginMetrics,
      Extension extension,
      ExtensionImplConsumer extensionImplConsumer,
      Class exceptionClass)
      throws X {
    T extensionImpl = extension.get();
    if (extensionImpl == null) {
      return;
    }
    try (TraceContext traceContext = newTrace(extension);
        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
      extensionImplConsumer.run(extensionImpl);
    } catch (Exception e) {
      Throwables.throwIfInstanceOf(e, exceptionClass);
      Throwables.throwIfUnchecked(e);
      pluginMetrics.incrementErrorCount(extension);
      logger.atWarning().withCause(e).log(
          "Failure in %s of plugin %s", extensionImpl.getClass(), extension.getPluginName());
    }
  }
  /**
   * Runs a plugin extension. All exceptions from the plugin extension except exceptions of the
   * specified type are caught and logged. Exceptions of the specified type are thrown and must be
   * handled by the caller.
   *
   * The consumer get the {@link Extension} provided that should be invoked. The extension
   * provides access to the plugin name and the export name.
   *
   * @param pluginMetrics the plugin metrics
   * @param extension extension that is being invoked
   * @param extensionConsumer the consumer that invokes the extension
   * @param exceptionClass type of the exceptions that should be thrown
   * @throws X expected exception from the plugin extension
   */
  static  void runLogExceptions(
      PluginMetrics pluginMetrics,
      Extension extension,
      ExtensionConsumer> extensionConsumer,
      Class exceptionClass)
      throws X {
    T extensionImpl = extension.get();
    if (extensionImpl == null) {
      return;
    }
    try (TraceContext traceContext = newTrace(extension);
        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
      extensionConsumer.run(extension);
    } catch (Exception e) {
      Throwables.throwIfInstanceOf(e, exceptionClass);
      Throwables.throwIfUnchecked(e);
      pluginMetrics.incrementErrorCount(extension);
      logger.atWarning().withCause(e).log(
          "Failure in %s of plugin %s", extensionImpl.getClass(), extension.getPluginName());
    }
  }
  /**
   * Calls a plugin extension and returns the result from the plugin extension call.
   *
   * The function gets the extension implementation provided that should be invoked.
   *
   * @param pluginMetrics the plugin metrics
   * @param extension extension that is being invoked
   * @param extensionImplFunction function that invokes the extension
   * @return the result from the plugin extension
   */
  static  R call(
      PluginMetrics pluginMetrics,
      Extension extension,
      ExtensionImplFunction extensionImplFunction) {
    try (TraceContext traceContext = newTrace(extension);
        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
      return extensionImplFunction.call(extension.get());
    }
  }
  /**
   * Calls a plugin extension and returns the result from the plugin extension call. Exceptions of
   * the specified type are thrown and must be handled by the caller.
   *
   * The function gets the extension implementation provided that should be invoked.
   *
   * @param pluginMetrics the plugin metrics
   * @param extension extension that is being invoked
   * @param checkedExtensionImplFunction function that invokes the extension
   * @param exceptionClass type of the exceptions that should be thrown
   * @return the result from the plugin extension
   * @throws X expected exception from the plugin extension
   */
  static  R call(
      PluginMetrics pluginMetrics,
      Extension extension,
      CheckedExtensionImplFunction checkedExtensionImplFunction,
      Class exceptionClass)
      throws X {
    try (TraceContext traceContext = newTrace(extension);
        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
      try {
        return checkedExtensionImplFunction.call(extension.get());
      } catch (Exception e) {
        // The only exception that can be thrown is X, but we cannot catch X since it is a generic
        // type.
        Throwables.throwIfInstanceOf(e, exceptionClass);
        Throwables.throwIfUnchecked(e);
        throw new IllegalStateException("unexpected exception: " + e.getMessage(), e);
      }
    }
  }
  /**
   * Calls a plugin extension and returns the result from the plugin extension call.
   *
   * The function get the {@link Extension} provided that should be invoked. The extension
   * provides access to the plugin name and the export name.
   *
   * @param pluginMetrics the plugin metrics
   * @param extension extension that is being invoked
   * @param extensionFunction function that invokes the extension
   * @return the result from the plugin extension
   */
  static  R call(
      PluginMetrics pluginMetrics,
      Extension extension,
      ExtensionFunction, R> extensionFunction) {
    try (TraceContext traceContext = newTrace(extension);
        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
      return extensionFunction.call(extension);
    }
  }
  /**
   * Calls a plugin extension and returns the result from the plugin extension call. Exceptions of
   * the specified type are thrown and must be handled by the caller.
   *
   * The function get the {@link Extension} provided that should be invoked. The extension
   * provides access to the plugin name and the export name.
   *
   * @param pluginMetrics the plugin metrics
   * @param extension extension that is being invoked
   * @param checkedExtensionFunction function that invokes the extension
   * @param exceptionClass type of the exceptions that should be thrown
   * @return the result from the plugin extension
   * @throws X expected exception from the plugin extension
   */
  static  R call(
      PluginMetrics pluginMetrics,
      Extension extension,
      CheckedExtensionFunction, R, X> checkedExtensionFunction,
      Class exceptionClass)
      throws X {
    try (TraceContext traceContext = newTrace(extension);
        Timer3.Context ctx = pluginMetrics.startLatency(extension)) {
      try {
        return checkedExtensionFunction.call(extension);
      } catch (Exception e) {
        // The only exception that can be thrown is X, but we cannot catch X since it is a generic
        // type.
        Throwables.throwIfInstanceOf(e, exceptionClass);
        Throwables.throwIfUnchecked(e);
        throw new IllegalStateException("unexpected exception: " + e.getMessage(), e);
      }
    }
  }
}