Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
import io.quarkiverse.githubapp.runtime.Routes;
import io.quarkiverse.githubapp.runtime.UtilsProducer;
import io.quarkiverse.githubapp.runtime.config.CheckedConfigProvider;
import io.quarkiverse.githubapp.runtime.config.GitHubAppBuildTimeConfig;
import io.quarkiverse.githubapp.runtime.error.DefaultErrorHandler;
import io.quarkiverse.githubapp.runtime.error.ErrorHandlerBridgeFunction;
import io.quarkiverse.githubapp.runtime.github.GitHubConfigFileProviderImpl;
Expand Down Expand Up @@ -143,6 +144,8 @@ class GitHubAppProcessor {

private static final MethodDescriptor EVENT_SELECT = MethodDescriptor.ofMethod(Event.class, "select", Event.class,
Annotation[].class);
private static final MethodDescriptor EVENT_FIRE = MethodDescriptor.ofMethod(Event.class, "fire",
void.class, Object.class);
private static final MethodDescriptor EVENT_FIRE_ASYNC = MethodDescriptor.ofMethod(Event.class, "fireAsync",
CompletionStage.class, Object.class);
private static final MethodDescriptor COMPLETION_STAGE_TO_COMPLETABLE_FUTURE = MethodDescriptor.ofMethod(
Expand Down Expand Up @@ -186,8 +189,7 @@ void registerForReflection(CombinedIndexBuildItem combinedIndex,
MethodParameterInfo methodParameter = configFileAnnotationInstance.target().asMethodParameter();
short parameterPosition = methodParameter.position();
Type parameterType = methodParameter.method().parameterTypes().get(parameterPosition);
reflectiveHierarchies.produce(new ReflectiveHierarchyBuildItem.Builder()
.type(parameterType)
reflectiveHierarchies.produce(ReflectiveHierarchyBuildItem.builder(parameterType)
.index(index)
.source(GitHubAppProcessor.class.getSimpleName() + " > " + methodParameter.method().declaringClass() + "#"
+ methodParameter.method())
Expand Down Expand Up @@ -277,6 +279,7 @@ void removeCompatibilityBridgeMethodsFromGitHubApi(

@BuildStep
void generateClasses(CombinedIndexBuildItem combinedIndex, LaunchModeBuildItem launchMode,
GitHubAppBuildTimeConfig gitHubAppBuildTimeConfig,
List<AdditionalEventDispatchingClassesIndexBuildItem> additionalEventDispatchingIndexes,
BuildProducer<AdditionalBeanBuildItem> additionalBeans,
BuildProducer<GeneratedBeanBuildItem> generatedBeans,
Expand All @@ -293,7 +296,7 @@ void generateClasses(CombinedIndexBuildItem combinedIndex, LaunchModeBuildItem l
// Add @Vetoed to all the user-defined event listening classes
annotationsTransformer
.produce(new AnnotationsTransformerBuildItem(new VetoUserDefinedEventListeningClassesAnnotationsTransformer(
allEventDefinitions.stream().map(d -> d.getAnnotation()).collect(Collectors.toSet()))));
allEventDefinitions.stream().map(EventDefinition::getAnnotation).collect(Collectors.toSet()))));

// Add the qualifiers as beans
String[] subscriberAnnotations = allEventDefinitions.stream().map(d -> d.getAnnotation().toString())
Expand All @@ -307,8 +310,8 @@ void generateClasses(CombinedIndexBuildItem combinedIndex, LaunchModeBuildItem l
generateAnnotationLiterals(classOutput, dispatchingConfiguration);

ClassOutput beanClassOutput = new GeneratedBeanGizmoAdaptor(generatedBeans);
generateDispatcher(beanClassOutput, launchMode, dispatchingConfiguration, reflectiveClasses);
generateMultiplexers(beanClassOutput, index, dispatchingConfiguration, reflectiveClasses);
generateDispatcher(beanClassOutput, launchMode, dispatchingConfiguration, reflectiveClasses, gitHubAppBuildTimeConfig);
generateMultiplexers(beanClassOutput, index, dispatchingConfiguration, reflectiveClasses, gitHubAppBuildTimeConfig);
}

@BuildStep
Expand Down Expand Up @@ -394,7 +397,7 @@ private static DispatchingConfiguration getDispatchingConfiguration(
.stream()
.filter(ai -> ai.target().kind() == Kind.METHOD_PARAMETER)
.filter(ai -> !Modifier.isInterface(ai.target().asMethodParameter().method().declaringClass().flags()))
.collect(Collectors.toList());
.toList();
for (AnnotationInstance eventSubscriberInstance : eventSubscriberInstances) {
String action = eventDefinition.getAction() != null ? eventDefinition.getAction()
: (eventSubscriberInstance.value() != null ? eventSubscriberInstance.value().asString() : Actions.ALL);
Expand Down Expand Up @@ -424,7 +427,7 @@ private static DispatchingConfiguration getDispatchingConfiguration(
.stream()
.filter(ai -> ai.target().kind() == Kind.METHOD_PARAMETER)
.filter(ai -> !Modifier.isInterface(ai.target().asMethodParameter().method().declaringClass().flags()))
.collect(Collectors.toList());
.toList();
for (AnnotationInstance rawEventSubscriberInstance : rawEventSubscriberInstances) {
String event = rawEventSubscriberInstance.valueWithDefault(index, "event").asString();
String action = rawEventSubscriberInstance.valueWithDefault(index, "action").asString();
Expand Down Expand Up @@ -511,7 +514,8 @@ private static void generateAnnotationLiterals(ClassOutput classOutput, Dispatch
private static void generateDispatcher(ClassOutput beanClassOutput,
LaunchModeBuildItem launchMode,
DispatchingConfiguration dispatchingConfiguration,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses) {
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses,
GitHubAppBuildTimeConfig gitHubAppBuildTimeConfig) {
String dispatcherClassName = GitHubEvent.class.getName() + "DispatcherImpl";

reflectiveClasses.produce(ReflectiveClassBuildItem.builder(dispatcherClassName).methods(true).fields(true).build());
Expand Down Expand Up @@ -653,16 +657,18 @@ private static void generateDispatcher(ClassOutput beanClassOutput,
eventMatchesCreator.writeArrayValue(annotationLiteralArrayRh, 0, annotationLiteralRh);

if (Actions.ALL.equals(action)) {
fireAsyncAction(eventMatchesCreator, launchMode.getLaunchMode(), dispatcherClassCreator.getClassName(),
gitHubEventRh, multiplexedEventRh, annotationLiteralArrayRh);
fireAction(gitHubAppBuildTimeConfig, eventMatchesCreator, launchMode.getLaunchMode(),
dispatcherClassCreator.getClassName(), gitHubEventRh, multiplexedEventRh,
annotationLiteralArrayRh);
} else {
BytecodeCreator actionMatchesCreator = eventMatchesCreator
.ifTrue(eventMatchesCreator.invokeVirtualMethod(MethodDescriptors.OBJECT_EQUALS,
eventMatchesCreator.load(action), dispatchedActionRh))
.trueBranch();

fireAsyncAction(actionMatchesCreator, launchMode.getLaunchMode(), dispatcherClassCreator.getClassName(),
gitHubEventRh, multiplexedEventRh, annotationLiteralArrayRh);
fireAction(gitHubAppBuildTimeConfig, actionMatchesCreator, launchMode.getLaunchMode(),
dispatcherClassCreator.getClassName(), gitHubEventRh, multiplexedEventRh,
annotationLiteralArrayRh);
}
}
}
Expand Down Expand Up @@ -698,7 +704,8 @@ private static void generateDispatcher(ClassOutput beanClassOutput,
private static void generateMultiplexers(ClassOutput beanClassOutput,
IndexView index,
DispatchingConfiguration dispatchingConfiguration,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses) {
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses,
GitHubAppBuildTimeConfig gitHubAppBuildTimeConfig) {
for (Entry<DotName, TreeSet<EventDispatchingMethod>> eventDispatchingMethodsEntry : dispatchingConfiguration
.getMethods().entrySet()) {
DotName declaringClassName = eventDispatchingMethodsEntry.getKey();
Expand Down Expand Up @@ -741,7 +748,7 @@ private static void generateMultiplexers(ClassOutput beanClassOutput,
originalConstructor.parameterTypes().stream().map(t -> t.name().toString()).toArray(String[]::new)));

List<AnnotationInstance> originalMethodAnnotations = originalConstructor.annotations().stream()
.filter(ai -> ai.target().kind() == Kind.METHOD).collect(Collectors.toList());
.filter(ai -> ai.target().kind() == Kind.METHOD).toList();
for (AnnotationInstance originalMethodAnnotation : originalMethodAnnotations) {
constructorCreator.addAnnotation(originalMethodAnnotation);
}
Expand Down Expand Up @@ -864,7 +871,10 @@ private static void generateMultiplexers(ClassOutput beanClassOutput,
AnnotatedElement generatedParameterAnnotations = methodCreator
.getParameterAnnotations(generatedParameterIndex);
if (parameterAnnotations.stream().anyMatch(ai -> ai.name().equals(eventSubscriberInstance.name()))) {
generatedParameterAnnotations.addAnnotation(DotNames.OBSERVES_ASYNC.toString());
switch (gitHubAppBuildTimeConfig.eventingModel()) {
case ASYNC -> generatedParameterAnnotations.addAnnotation(DotNames.OBSERVES_ASYNC.toString());
case SYNC -> generatedParameterAnnotations.addAnnotation(DotNames.OBSERVES.toString());
}
generatedParameterAnnotations.addAnnotation(eventSubscriberInstance);
} else {
for (AnnotationInstance annotationInstance : parameterAnnotations) {
Expand Down Expand Up @@ -986,6 +996,28 @@ private static void generateMultiplexers(ClassOutput beanClassOutput,
}
}

private static void fireAction(GitHubAppBuildTimeConfig gitHubAppBuildTimeConfig, BytecodeCreator bytecodeCreator,
LaunchMode launchMode, String className,
ResultHandle gitHubEventRh, ResultHandle multiplexedEventRh, ResultHandle annotationLiteralArrayRh) {
switch (gitHubAppBuildTimeConfig.eventingModel()) {
case ASYNC -> fireAsyncAction(bytecodeCreator, launchMode, className, gitHubEventRh, multiplexedEventRh,
annotationLiteralArrayRh);
case SYNC -> fireSyncAction(bytecodeCreator, launchMode, className, gitHubEventRh, multiplexedEventRh,
annotationLiteralArrayRh);
}
}

private static void fireSyncAction(BytecodeCreator bytecodeCreator, LaunchMode launchMode, String className,
ResultHandle gitHubEventRh, ResultHandle multiplexedEventRh, ResultHandle annotationLiteralArrayRh) {
ResultHandle cdiEventRh = bytecodeCreator.invokeInterfaceMethod(EVENT_SELECT,
bytecodeCreator.readInstanceField(
FieldDescriptor.of(className, EVENT_EMITTER_FIELD, Event.class),
bytecodeCreator.getThis()),
annotationLiteralArrayRh);

bytecodeCreator.invokeInterfaceMethod(EVENT_FIRE, cdiEventRh, multiplexedEventRh);
}

private static ResultHandle fireAsyncAction(BytecodeCreator bytecodeCreator, LaunchMode launchMode, String className,
ResultHandle gitHubEventRh, ResultHandle multiplexedEventRh, ResultHandle annotationLiteralArrayRh) {
ResultHandle cdiEventRh = bytecodeCreator.invokeInterfaceMethod(EVENT_SELECT,
Expand Down
25 changes: 25 additions & 0 deletions docs/modules/ROOT/pages/developer-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,31 @@ By default, the config file path is relative to the `.github/` directory.
You can reference a file outside this directory by using an `/absolute/path`.
====

== Eventing model

Quarkus GitHub App supports two eventing models via the `quarkus.github-app.eventing-model` configuration property:

`sync`::
Events are handled synchronously. The response to the original GitHub request will only be sent when the event has been handled.
+
If an error occurs when handling the event, the status code of the response is `500` and the body contains a short description of the error.
+
In the GitHub App delivery page on GitHub (`https://github.com/settings/apps/<your-github-app>/advanced`), you are able to see the failed requests.
`async` (the default, for backward compatibility reasons)::
Events are handled asynchronously. The response to the original GitHub request is sent just after the parsing of the request (with a `200` status code, except if the request couldn't be parsed properly).
+
If an error occurs when handling the event, it is logged, but it is already too late to affect the response as it has already been sent.
+
In the GitHub App delivery page on GitHub (`https://github.com/settings/apps/<your-github-app>/advanced`), you only see failures for early parsing errors, not for errors in your event handling code.
+
With `async`, you will use less resources and will keep the connections to GitHub open for a very short amount of time.
However, you can't rely on the HTTP status code of the response anymore.

[NOTE]
====
In development, when using the Smee.io forwarder, errors are not reported to GitHub.
====

== Error handler

The Quarkus GitHub App extension provides an error handler that will log errors with as many details as possible.
Expand Down
24 changes: 24 additions & 0 deletions docs/modules/ROOT/pages/includes/quarkus-github-app.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ h|[.header-title]##Configuration property##
h|Type
h|Default

a|icon:lock[title=Fixed at build time] [[quarkus-github-app_quarkus-github-app-eventing-model]] [.property-path]##link:#quarkus-github-app_quarkus-github-app-eventing-model[`quarkus.github-app.eventing-model`]##
ifdef::add-copy-button-to-config-props[]
config_property_copy_button:+++quarkus.github-app.eventing-model+++[]
endif::add-copy-button-to-config-props[]


[.description]
--
Eventing model used to determine how to handle the events received from GitHub.

- When using `sync`, events are handled synchronously and errors correctly reported to GitHub if an error occurs.
- When using `async`, events are handled asynchronously and a 200 is returned right away so you can't see the errors on the GitHub side. The advantage of using this approach is that you don't keep the HTTP connection to GitHub open while your payload is handled.


ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_GITHUB_APP_EVENTING_MODEL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_GITHUB_APP_EVENTING_MODEL+++`
endif::add-copy-button-to-env-var[]
--
a|`sync`, `async`
|`sync`

a| [[quarkus-github-app_quarkus-github-app-app-id]] [.property-path]##link:#quarkus-github-app_quarkus-github-app-app-id[`quarkus.github-app.app-id`]##
ifdef::add-copy-button-to-config-props[]
config_property_copy_button:+++quarkus.github-app.app-id+++[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ h|[.header-title]##Configuration property##
h|Type
h|Default

a|icon:lock[title=Fixed at build time] [[quarkus-github-app_quarkus-github-app-eventing-model]] [.property-path]##link:#quarkus-github-app_quarkus-github-app-eventing-model[`quarkus.github-app.eventing-model`]##
ifdef::add-copy-button-to-config-props[]
config_property_copy_button:+++quarkus.github-app.eventing-model+++[]
endif::add-copy-button-to-config-props[]


[.description]
--
Eventing model used to determine how to handle the events received from GitHub.

- When using `sync`, events are handled synchronously and errors correctly reported to GitHub if an error occurs.
- When using `async`, events are handled asynchronously and a 200 is returned right away so you can't see the errors on the GitHub side. The advantage of using this approach is that you don't keep the HTTP connection to GitHub open while your payload is handled.


ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_GITHUB_APP_EVENTING_MODEL+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_GITHUB_APP_EVENTING_MODEL+++`
endif::add-copy-button-to-env-var[]
--
a|`sync`, `async`
|`sync`

a| [[quarkus-github-app_quarkus-github-app-app-id]] [.property-path]##link:#quarkus-github-app_quarkus-github-app-app-id[`quarkus.github-app.app-id`]##
ifdef::add-copy-button-to-config-props[]
config_property_copy_button:+++quarkus.github-app.app-id+++[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkiverse.githubapp.error;

import io.quarkiverse.githubapp.GitHubEvent;

public class GitHubEventDeliveryException extends RuntimeException {

public GitHubEventDeliveryException(GitHubEvent gitHubEvent, String context, boolean serviceDown, Throwable cause) {
super(generateMessage(gitHubEvent, context, serviceDown, cause), cause);
}

private static String generateMessage(GitHubEvent gitHubEvent, String context, boolean serviceDown, Throwable cause) {
return """
Error handling a GitHub event:

> Repository: %s
> Event: %s
> Delivery ID: %s
> Context: %s
> Error: %s
""".formatted(gitHubEvent.getRepository().orElse(""), gitHubEvent.getEventAction(), gitHubEvent.getDeliveryId(),
context,
serviceDown ? "GitHub APIs are unavailable: " + cause.getMessage() : cause.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.quarkiverse.githubapp.runtime;

public enum EventingModel {
SYNC,
ASYNC;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.jboss.logging.Logger;

import io.quarkiverse.githubapp.GitHubEvent;
import io.quarkiverse.githubapp.error.GitHubEventDeliveryException;
import io.quarkiverse.githubapp.runtime.config.CheckedConfigProvider;
import io.quarkiverse.githubapp.runtime.replay.ReplayEventsRoute;
import io.quarkiverse.githubapp.runtime.signing.PayloadSignatureChecker;
Expand Down Expand Up @@ -67,7 +68,7 @@ public void init(@Observes StartupEvent startupEvent) throws IOException {

public void init(@Observes Router router) {
router.post(checkedConfigProvider.webhookUrlPath())
.handler(BodyHandler.create()) // this is required so that the body to be read by subsequent handlers
.handler(BodyHandler.create()) // this is required so that the body can be read by subsequent handlers
.blockingHandler(routingContext -> {
handleRequest(
routingContext,
Expand Down Expand Up @@ -142,9 +143,12 @@ private void handleRequest(RoutingContext routingContext,
replayRouteInstance.get().pushEvent(gitHubEvent);
}

gitHubEventEmitter.fire(gitHubEvent);

routingExchange.ok().end();
try {
gitHubEventEmitter.fire(gitHubEvent);
routingExchange.ok().end();
} catch (GitHubEventDeliveryException e) {
routingExchange.serverError().end(e.getMessage());
}
}

private static boolean isBlank(String value) {
Expand Down
Loading
Loading