diff options
Diffstat (limited to 'java/com/google/gerrit/server/plugincontext/PluginContext.java')
-rw-r--r-- | java/com/google/gerrit/server/plugincontext/PluginContext.java | 415 |
1 files changed, 415 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/plugincontext/PluginContext.java b/java/com/google/gerrit/server/plugincontext/PluginContext.java new file mode 100644 index 0000000000..70b23e3b8b --- /dev/null +++ b/java/com/google/gerrit/server/plugincontext/PluginContext.java @@ -0,0 +1,415 @@ +// 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.logging.TraceContext; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +/** + * Context for invoking plugin extensions. + * + * <p>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. + * + * <p>If possible plugin extensions should be invoked through: + * + * <ul> + * <li>{@link PluginItemContext} for extensions from {@link DynamicItem} + * <li>{@link PluginSetContext} for extensions from {@link DynamicSet} + * <li>{@link PluginMapContext} for extensions from {@link DynamicMap} + * </ul> + * + * <p>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: + * + * <pre> + * public interface Foo { + * void doFoo() throws Exception1, Exception2, Exception3; + * } + * + * ... + * + * for (Extension<Foo> fooExtension : fooDynamicMap) { + * try (TraceContext traceContext = PluginContext.newTrace(fooExtension)) { + * fooExtension.get().doFoo(); + * } + * } + * </pre> + * + * <p>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}. + * + * <p>The run* methods execute an extension but don't deliver a result back to the caller. + * Exceptions can be caught and logged. + * + * <p>The call* methods execute an extension and deliver a result back to the caller. + */ +public class PluginContext<T> { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + @FunctionalInterface + public interface ExtensionImplConsumer<T> { + void run(T t) throws Exception; + } + + @FunctionalInterface + public interface ExtensionImplFunction<T, R> { + R call(T input); + } + + @FunctionalInterface + public interface CheckedExtensionImplFunction<T, R, X extends Exception> { + R call(T input) throws X; + } + + @FunctionalInterface + public interface ExtensionConsumer<T extends Extension<?>> { + void run(T extension) throws Exception; + } + + @FunctionalInterface + public interface ExtensionFunction<T extends Extension<?>, R> { + R call(T extension); + } + + @FunctionalInterface + public interface CheckedExtensionFunction<T extends Extension<?>, 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<String, String, String> latency; + final Counter3<String, String, String> errorCount; + + @Inject + PluginMetrics(MetricMaker metricMaker) { + this.latency = + metricMaker.newTimer( + "plugin/latency", + new Description("Latency for plugin invocation") + .setCumulative() + .setUnit(Units.MILLISECONDS), + Field.ofString("plugin_name"), + Field.ofString("class_name"), + Field.ofString("export_name")); + this.errorCount = + metricMaker.newCounter( + "plugin/error_count", + new Description("Number of plugin errors").setCumulative().setUnit("errors"), + Field.ofString("plugin_name"), + Field.ofString("class_name"), + Field.ofString("export_name")); + } + + 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 <T> TraceContext newTrace(DynamicItem<T> dynamicItem) { + Extension<T> 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 <T> TraceContext newTrace(Extension<T> extension) { + return TraceContext.open().addPluginTag(requireNonNull(extension).getPluginName()); + } + + /** + * Runs a plugin extension. All exceptions from the plugin extension are caught and logged. + * + * <p>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 <T> void runLogExceptions( + PluginMetrics pluginMetrics, + Extension<T> extension, + ExtensionImplConsumer<T> extensionImplConsumer) { + T extensionImpl = extension.get(); + if (extensionImpl == null) { + return; + } + try (TraceContext traceContext = newTrace(extension); + Timer3.Context ctx = pluginMetrics.startLatency(extension)) { + extensionImplConsumer.run(extensionImpl); + } catch (Throwable 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 are caught and logged. + * + * <p>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 <T> void runLogExceptions( + PluginMetrics pluginMetrics, + Extension<T> extension, + ExtensionConsumer<Extension<T>> extensionConsumer) { + T extensionImpl = extension.get(); + if (extensionImpl == null) { + return; + } + + try (TraceContext traceContext = newTrace(extension); + Timer3.Context ctx = pluginMetrics.startLatency(extension)) { + extensionConsumer.run(extension); + } catch (Throwable 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. + * + * <p>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 <T, X extends Exception> void runLogExceptions( + PluginMetrics pluginMetrics, + Extension<T> extension, + ExtensionImplConsumer<T> extensionImplConsumer, + Class<X> 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 (Throwable e) { + Throwables.throwIfInstanceOf(e, exceptionClass); + Throwables.throwIfUnchecked(e); + pluginMetrics.incrementErrorCount(extension); + logger.atWarning().withCause(e).log( + "Failure in %s of plugin invoke%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. + * + * <p>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 <T, X extends Exception> void runLogExceptions( + PluginMetrics pluginMetrics, + Extension<T> extension, + ExtensionConsumer<Extension<T>> extensionConsumer, + Class<X> 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 (Throwable 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. + * + * <p>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 <T, R> R call( + PluginMetrics pluginMetrics, + Extension<T> extension, + ExtensionImplFunction<T, R> 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. + * + * <p>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 <T, R, X extends Exception> R call( + PluginMetrics pluginMetrics, + Extension<T> extension, + CheckedExtensionImplFunction<T, R, X> checkedExtensionImplFunction, + Class<X> 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. + * + * <p>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 <T, R> R call( + PluginMetrics pluginMetrics, + Extension<T> extension, + ExtensionFunction<Extension<T>, 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. + * + * <p>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 <T, R, X extends Exception> R call( + PluginMetrics pluginMetrics, + Extension<T> extension, + CheckedExtensionFunction<Extension<T>, R, X> checkedExtensionFunction, + Class<X> 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); + } + } + } +} |