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 : }
|