forked from helidon-io/helidon
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Redesign of how MP metrics interceptors work; support async JAX-RS en…
…dpoints for additional metrics (helidon-io#2868)
- Loading branch information
Showing
31 changed files
with
1,275 additions
and
700 deletions.
There are no files selected for viewing
105 changes: 105 additions & 0 deletions
105
microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptionRunner.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
166 changes: 166 additions & 0 deletions
166
...profile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptionRunnerImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
84 changes: 84 additions & 0 deletions
84
...profile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptionTargetInfo.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.