Skip to content

Commit

Permalink
Redesign of how MP metrics interceptors work; support async JAX-RS en…
Browse files Browse the repository at this point in the history
…dpoints for additional metrics (helidon-io#2868)
  • Loading branch information
tjquinno authored and aseovic committed Apr 26, 2021
1 parent af4a75b commit 9cd9f56
Show file tree
Hide file tree
Showing 31 changed files with 1,275 additions and 700 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright (c) 2021 Oracle and/or its affiliates.
*
* 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 io.helidon.microprofile.metrics;

import java.util.function.BiConsumer;

import javax.interceptor.InvocationContext;

/**
* Abstraction of processing around an interception point, independent from the details of any
* particular interceptor or the specific type of work done (e.g., updating metrics) before the intercepted invocation runs and
* after it completes.
* <p>
* To use {@code InterceptionRunner}, clients:
* <ul>
* <li>Create an instance of a class which implements {@code InterceptionRunner}.</li>
* <li>From the interceptor's {@code @AroundConstruct} and {@code @AroundInvoke} methods, invoke one of the variants of
* the runner's {@link #run(InvocationContext, Iterable, BiConsumer) run} method. Which variant depends on whether the
* specific interceptor needs to operate on the work items
* <ul>
* <li>only before (e.g., to increment a counter metric), or</li>
* <li>both
* before and after (e.g., to update a metric that measures time spent in the intercepted method)</li>
* </ul>
* the intercepted
* method
* runs.
* <p>
* The interceptor passes the {@code run} method:
* <ul>
* <li>a {@code Supplier<Iterable<>>} of the work items,</li>
* <li>a pre-invocation {@code Consumer} of work item which performs an action on each work item before the
* intercepted invocation runs, and</li>
* <li>an post-completion {@code Consumer} of work item which performs an action on each work item after the
* intercepted invocation has finished, only for the "before-and-after"
* {@link #run(InvocationContext, Iterable, BiConsumer, BiConsumer) run} variant.</li>
* </ul>
* </p>
* </li>
* </ul>
* </p>
* <p>
* The runner
* <ol>
* <li>invokes the pre-invocation consumer for all work items,</li>
* <li>invokes the intercepted executable, then</li>
* <li>(if provided) invokes the post-completion consumer for all work
* items.</li>
* </ol>
* </p>
* <p>
* The interface requires a {@code Iterable<>} for work items because, in the before-and-after case, the runner
* might need to process the work items twice. In those cases, the {@code Iterable} can furnish two {@code Iterators}.
* </p>
*/
interface InterceptionRunner {

/**
* Invokes the intercepted executable represented by the {@code InvocationContext}, performing the pre-invocation
* operation on each work item.
*
* @param context {@code InvocationContext} for the intercepted invocation
* @param workItems the work items the interceptor will operate on
* @param preInvocationHandler the pre-invoke operation to perform on each work item
* @param <T> type of the work items
* @return the return value from the invoked executable
* @throws Exception for any error thrown by the {@code Iterable} of work items or the invoked executable itself
*/
<T> Object run(
InvocationContext context,
Iterable<T> workItems,
BiConsumer<InvocationContext, T> preInvocationHandler) throws Exception;

/**
* Invokes the intercepted executable represented by the {@code InvocationContext}, performing the pre-invocation
* and post-completion operation on each work item.
*
* @param context {@code InvocationContext} for the intercepted invocation
* @param workItems the work items the interceptor will operate on
* @param preInvocationHandler the pre-invoke operation to perform on each work item
* @param postCompletionHandler the post-completion operation to perform on each work item
* @param <T> type of the work items
* @return the return value from the invoked executable
* @throws Exception for any error thrown by the {@code Iterable} of work items or the invoked executable itself
*/
<T> Object run(
InvocationContext context,
Iterable<T> workItems,
BiConsumer<InvocationContext, T> preInvocationHandler,
BiConsumer<InvocationContext, T> postCompletionHandler) throws Exception;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright (c) 2021 Oracle and/or its affiliates.
*
* 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 io.helidon.microprofile.metrics;

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.function.BiConsumer;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.interceptor.InvocationContext;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.CompletionCallback;
import javax.ws.rs.container.Suspended;

/**
* A general-purpose implementation of {@link InterceptionRunner}, supporting asynchronous JAX-RS endpoints as indicated by the
* presence of a {@code @Suspended AsyncResponse} parameter.
*/
class InterceptionRunnerImpl implements InterceptionRunner {

/*
* In this impl, constructor runners and synchronous method runners are identical and have no saved context at all, so we
* can use the same instance for all except async method runners.
*/
private static final InterceptionRunner INSTANCE = new InterceptionRunnerImpl();

/**
* Returns the appropriate {@code InterceptionRunner} for the executable.
*
* @param executable the {@code Constructor} or {@code Method} requiring interceptor support
* @return the {@code InterceptionRunner}
*/
static InterceptionRunner create(Executable executable) {
if (executable instanceof Constructor<?>) {
return INSTANCE;
}
if (executable instanceof Method) {
final int asyncResponseSlot = InterceptionRunnerImpl.asyncResponseSlot((Method) executable);
return asyncResponseSlot >= 0
? AsyncMethodRunnerImpl.create(asyncResponseSlot)
: INSTANCE;
}
throw new IllegalArgumentException("Executable " + executable.getName() + " is not a constructor or method");
}

@Override
public <T> Object run(
InvocationContext context,
Iterable<T> workItems,
BiConsumer<InvocationContext, T> preInvocationHandler) throws Exception {
workItems.forEach(workItem -> preInvocationHandler.accept(context, workItem));
return context.proceed();
}

@Override
public <T> Object run(
InvocationContext context,
Iterable<T> workItems,
BiConsumer<InvocationContext, T> preInvocationHandler,
BiConsumer<InvocationContext, T> postCompletionHandler) throws Exception {
workItems.forEach(workItem -> preInvocationHandler.accept(context, workItem));
try {
return context.proceed();
} finally {
workItems.forEach(workItem -> postCompletionHandler.accept(context, workItem));
}
}

/**
* An {@code InterceptionRunner} which supports JAX-RS asynchronous methods.
*/
private static class AsyncMethodRunnerImpl extends InterceptionRunnerImpl {
private final int asyncResponseSlot;

static InterceptionRunner create(int asyncResponseSlot) {
return new AsyncMethodRunnerImpl(asyncResponseSlot);
}

private AsyncMethodRunnerImpl(int asyncResponseSlot) {
this.asyncResponseSlot = asyncResponseSlot;
}

@Override
public <T> Object run(
InvocationContext context,
Iterable<T> workItems,
BiConsumer<InvocationContext, T> preInvocationHandler,
BiConsumer<InvocationContext, T> postCompletionHandler) throws Exception {

// Check the post-completion handler now because we don't want an NPE thrown from some other call stack when we try to
// use it in the completion callback. Any other null argument would trigger an NPE from the current call stack.
Objects.requireNonNull(postCompletionHandler, "postCompletionHandler");

workItems.forEach(workItem -> preInvocationHandler.accept(context, workItem));
AsyncResponse asyncResponse = AsyncResponse.class.cast(context.getParameters()[asyncResponseSlot]);
asyncResponse.register(FinishCallback.create(context, postCompletionHandler, workItems));
return context.proceed();
}

@Override
public String toString() {
return new StringJoiner(", ", AsyncMethodRunnerImpl.class.getSimpleName() + "[", "]")
.add("asyncResponseSlot=" + asyncResponseSlot)
.toString();
}
}

private static class FinishCallback<T> implements CompletionCallback {

private static final Logger LOGGER = Logger.getLogger(FinishCallback.class.getName());

private final InvocationContext context;
private final BiConsumer<InvocationContext, T> postCompletionHandler;
private final Iterable<T> workItems;

static <T> FinishCallback<T> create(InvocationContext context, BiConsumer<InvocationContext, T> postCompletionHandler,
Iterable<T> workItems) {
return new FinishCallback<>(context, postCompletionHandler, workItems);
}
private FinishCallback(InvocationContext context, BiConsumer<InvocationContext, T> postCompletionHandler,
Iterable<T> workItems) {
this.context = context;
this.postCompletionHandler = postCompletionHandler;
this.workItems = workItems;
}

@Override
public void onComplete(Throwable throwable) {
workItems.forEach(workItem -> postCompletionHandler.accept(context, workItem));
if (throwable != null) {
LOGGER.log(Level.FINE, "Throwable detected by interceptor async callback", throwable);
}
}
}

private static int asyncResponseSlot(Method interceptedMethod) {
int result = 0;

for (Parameter p : interceptedMethod.getParameters()) {
if (AsyncResponse.class.isAssignableFrom(p.getType()) && p.getAnnotation(Suspended.class) != null) {
return result;
}
result++;
}
return -1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2021 Oracle and/or its affiliates.
*
* 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 io.helidon.microprofile.metrics;

import java.lang.annotation.Annotation;
import java.lang.reflect.Executable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

/**
* Records information about an intercepted executable.
* <p>
* Specifically:
* <ul>
* <li>the work items to be updated when the corresponding executable is intercepted, organized by the annotation class that
* gave rise to each work item; and</li>
* <li>the {@code InterceptionRunner} to use in updating the work items and invoking the method or constructor.</li>
* </ul>
* </p>
*
* @param <T> base type of the work items handled by the interceptor represented by this instance
*/
class InterceptionTargetInfo<T> {

private final InterceptionRunner runner;

private final Map<Class<?>, Collection<T>> workItemsByAnnotationType = new HashMap<>();

/**
* Creates a new instance based on the provided {@code Executable}.
*
* @param executable the constructor or method subject to interception
* @return the new instance
*/
static <T> InterceptionTargetInfo<T> create(Executable executable) {
return new InterceptionTargetInfo<>(InterceptionRunnerImpl.create(executable));
}

private InterceptionTargetInfo(InterceptionRunner runner) {
this.runner = runner;
}

InterceptionRunner runner() {
return runner;
}

Iterable<T> workItems(Class<? extends Annotation> annotationType) {
/*
* Build a supplier of the iterable, because before-and-after runners will need to process the work items twice so we need
* to give the runner a supplier.
*/
return workItemsByAnnotationType.get(annotationType);
}

/**
* Adds a work item to this info, identifying the annotation type that led to this work item.
* @param annotationType type of the interceptor
* @param workItem the newly-created workItem
*/
void addWorkItem(Class<? extends Annotation> annotationType, T workItem) {

// Using a set for the actual collection subtly handles the case where a class-level and a method- or constructor-level
// annotation both indicate the same workItem. We do not want to update the same workItem twice in that case.

Collection<T> workItems = workItemsByAnnotationType.computeIfAbsent(annotationType, c -> new HashSet<>());
workItems.add(workItem);
}
}
Loading

0 comments on commit 9cd9f56

Please sign in to comment.