Skip to content

Commit

Permalink
feat: retry and catch
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartwdouglas committed Aug 12, 2024
1 parent 13c8ca7 commit d25dde4
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.quarkus.bootstrap.prebuild.CodeGenException;
import io.quarkus.deployment.CodeGenContext;
import io.quarkus.deployment.CodeGenProvider;
import xyz.block.ftl.GeneratedRef;
import xyz.block.ftl.Subscription;
import xyz.block.ftl.VerbClient;
import xyz.block.ftl.VerbClientDefinition;
Expand Down Expand Up @@ -150,6 +151,10 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
}
String thisType = className(data.getName());
TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(thisType)
.addAnnotation(
AnnotationSpec.builder(GeneratedRef.class)
.addMember("name", "\"" + data.getName() + "\"")
.addMember("module", "\"" + module.getName() + "\"").build())
.addModifiers(Modifier.PUBLIC);
MethodSpec.Builder allConstructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC);

Expand Down Expand Up @@ -210,6 +215,10 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
}
String thisType = className(data.getName());
TypeSpec.Builder dataBuilder = TypeSpec.enumBuilder(thisType)
.addAnnotation(
AnnotationSpec.builder(GeneratedRef.class)
.addMember("name", "\"" + data.getName() + "\"")
.addMember("module", "\"" + module.getName() + "\"").build())
.addModifiers(Modifier.PUBLIC);

for (var i : data.getVariantsList()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.ClassType;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.VoidType;
Expand Down Expand Up @@ -63,6 +65,8 @@
import xyz.block.ftl.Config;
import xyz.block.ftl.Cron;
import xyz.block.ftl.Export;
import xyz.block.ftl.GeneratedRef;
import xyz.block.ftl.Retry;
import xyz.block.ftl.Secret;
import xyz.block.ftl.Subscription;
import xyz.block.ftl.Verb;
Expand Down Expand Up @@ -91,6 +95,7 @@
import xyz.block.ftl.v1.schema.MetadataCalls;
import xyz.block.ftl.v1.schema.MetadataCronJob;
import xyz.block.ftl.v1.schema.MetadataIngress;
import xyz.block.ftl.v1.schema.MetadataRetry;
import xyz.block.ftl.v1.schema.MetadataSubscriber;
import xyz.block.ftl.v1.schema.Module;
import xyz.block.ftl.v1.schema.Optional;
Expand All @@ -114,6 +119,7 @@ class FtlProcessor {
public static final DotName SECRET = DotName.createSimple(Secret.class);
public static final DotName CONFIG = DotName.createSimple(Config.class);
public static final DotName OFFSET_DATE_TIME = DotName.createSimple(OffsetDateTime.class.getName());
public static final DotName GENERATED_REF = DotName.createSimple(GeneratedRef.class);

@BuildStep
ModuleNameBuildItem moduleName(ApplicationInfoBuildItem applicationInfoBuildItem) {
Expand Down Expand Up @@ -224,49 +230,10 @@ public void registerVerbs(CombinedIndexBuildItem index,
.setEvent(buildType(extractionContext, topic.eventType())).build()));
}

for (var verb : index.getIndex().getAnnotations(VERB)) {
boolean exported = verb.target().hasAnnotation(EXPORT);
var method = verb.target().asMethod();
String className = method.declaringClass().name().toString();
beans.addBeanClass(className);

handleVerbMethod(extractionContext, method, className, exported, BodyType.ALLOWED, null);
}
for (var cron : index.getIndex().getAnnotations(CRON)) {
var method = cron.target().asMethod();
String className = method.declaringClass().name().toString();
beans.addBeanClass(className);
handleVerbMethod(extractionContext, method, className, false, BodyType.DISALLOWED, (builder -> {
builder.addMetadata(Metadata.newBuilder()
.setCronJob(MetadataCronJob.newBuilder().setCron(cron.value().asString())).build());
}));
}
for (var subscription : index.getIndex().getAnnotations(SUBSCRIPTION)) {
if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) {
continue;
}
var method = subscription.target().asMethod();
String className = method.declaringClass().name().toString();
String name = subscription.value("name").asString();
String module = subscription.value("module") == null ? moduleName : subscription.value("module").asString();
String topic = subscription.value("topic").asString();
generateSubscription(moduleBuilder, extractionContext, beans, method, className, name, module, topic);
}
for (var metaSub : subscriptionMetaAnnotationsBuildItem.getAnnotations().entrySet()) {
for (var subscription : index.getIndex().getAnnotations(metaSub.getKey())) {
if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) {
log.warnf("Subscription annotation on non-method target: %s", subscription.target());
continue;
}
var method = subscription.target().asMethod();
generateSubscription(moduleBuilder, extractionContext, beans, method,
method.declaringClass().name().toString(),
metaSub.getValue().name(),
metaSub.getValue().module(),
metaSub.getValue().topic());
}

}
handleVerbAnnotations(index, beans, extractionContext);
handleCronAnnotations(index, beans, extractionContext);
handleSubscriptionAnnotations(index, subscriptionMetaAnnotationsBuildItem, moduleName, moduleBuilder, extractionContext,
beans);

//TODO: make this composable so it is not just one big method, build items should contribute to the schema
for (var endpoint : restEndpoints.getEntries()) {
Expand Down Expand Up @@ -369,14 +336,80 @@ public void registerVerbs(CombinedIndexBuildItem index,
Files.setPosixFilePermissions(output, newPerms);
}

private void handleVerbAnnotations(CombinedIndexBuildItem index, AdditionalBeanBuildItem.Builder beans,
ExtractionContext extractionContext) {
for (var verb : index.getIndex().getAnnotations(VERB)) {
boolean exported = verb.target().hasAnnotation(EXPORT);
var method = verb.target().asMethod();
String className = method.declaringClass().name().toString();
beans.addBeanClass(className);

handleVerbMethod(extractionContext, method, className, exported, BodyType.ALLOWED, null);
}
}

private void handleSubscriptionAnnotations(CombinedIndexBuildItem index,
SubscriptionMetaAnnotationsBuildItem subscriptionMetaAnnotationsBuildItem, String moduleName,
Module.Builder moduleBuilder, ExtractionContext extractionContext, AdditionalBeanBuildItem.Builder beans) {
for (var subscription : index.getIndex().getAnnotations(SUBSCRIPTION)) {
var info = SubscriptionMetaAnnotationsBuildItem.fromJandex(subscription, moduleName);
if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) {
continue;
}
var method = subscription.target().asMethod();
String className = method.declaringClass().name().toString();
generateSubscription(moduleBuilder, extractionContext, beans, method, className, info);
}
for (var metaSub : subscriptionMetaAnnotationsBuildItem.getAnnotations().entrySet()) {
for (var subscription : index.getIndex().getAnnotations(metaSub.getKey())) {
if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) {
log.warnf("Subscription annotation on non-method target: %s", subscription.target());
continue;
}
var method = subscription.target().asMethod();
generateSubscription(moduleBuilder, extractionContext, beans, method,
method.declaringClass().name().toString(),
metaSub.getValue());
}

}
}

private void handleCronAnnotations(CombinedIndexBuildItem index, AdditionalBeanBuildItem.Builder beans,
ExtractionContext extractionContext) {
for (var cron : index.getIndex().getAnnotations(CRON)) {
var method = cron.target().asMethod();
String className = method.declaringClass().name().toString();
beans.addBeanClass(className);
handleVerbMethod(extractionContext, method, className, false, BodyType.DISALLOWED, (builder -> {
builder.addMetadata(Metadata.newBuilder()
.setCronJob(MetadataCronJob.newBuilder().setCron(cron.value().asString())).build());
}));
}
}

private void generateSubscription(Module.Builder moduleBuilder, ExtractionContext extractionContext,
AdditionalBeanBuildItem.Builder beans, MethodInfo method, String className, String name, String module,
String topic) {
AdditionalBeanBuildItem.Builder beans, MethodInfo method, String className,
SubscriptionMetaAnnotationsBuildItem.SubscriptionAnnotation info) {
beans.addBeanClass(className);
moduleBuilder.addDecls(Decl.newBuilder().setSubscription(xyz.block.ftl.v1.schema.Subscription.newBuilder()
.setName(name).setTopic(Ref.newBuilder().setName(topic).setModule(module).build())).build());
.setName(info.name()).setTopic(Ref.newBuilder().setName(info.topic()).setModule(info.module()).build()))
.build());
handleVerbMethod(extractionContext, method, className, false, BodyType.REQUIRED, (builder -> {
builder.addMetadata(Metadata.newBuilder().setSubscriber(MetadataSubscriber.newBuilder().setName(name)));
builder.addMetadata(Metadata.newBuilder().setSubscriber(MetadataSubscriber.newBuilder().setName(info.name())));
if (method.hasAnnotation(Retry.class)) {
RetryRecord retry = RetryRecord.fromJandex(method.annotation(Retry.class), extractionContext.moduleName);

MetadataRetry.Builder retryBuilder = MetadataRetry.newBuilder();
if (!retry.catchVerb().isEmpty()) {
retryBuilder.setCatch(Ref.newBuilder().setModule(retry.catchModule())
.setName(retry.catchVerb()).build());
}
retryBuilder.setCount(retry.count())
.setMaxBackoff(retry.maxBackoff())
.setMinBackoff(retry.minBackoff());
builder.addMetadata(Metadata.newBuilder().setRetry(retryBuilder).build());
}
}));
}

Expand Down Expand Up @@ -438,12 +471,14 @@ private void handleVerbMethod(ExtractionContext context, MethodInfo method, Stri
}
bodyParamType = VoidType.VOID;
}
if (callsMetadata.getCallsCount() > 0) {
verbBuilder.addMetadata(Metadata.newBuilder().setCalls(callsMetadata));
}

context.recorder.registerVerb(context.moduleName(), verbName, method.name(), parameterTypes,
Class.forName(className, false, Thread.currentThread().getContextClassLoader()), paramMappers);
verbBuilder
.setName(verbName)
.addMetadata(Metadata.newBuilder().setCalls(callsMetadata))
.setExport(exported)
.setRequest(buildType(context, bodyParamType))
.setResponse(buildType(context, method.returnType()));
Expand Down Expand Up @@ -557,6 +592,14 @@ private Type buildType(ExtractionContext context, org.jboss.jandex.Type type) {
}
case CLASS -> {
var clazz = type.asClassType();
var info = context.index().getComputingIndex().getClassByName(clazz.name());
if (info != null && info.hasDeclaredAnnotation(GENERATED_REF)) {
var ref = info.declaredAnnotation(GENERATED_REF);
return Type.newBuilder()
.setRef(Ref.newBuilder().setName(ref.value("name").asString())
.setModule(ref.value("module").asString()))
.build();
}
if (clazz.name().equals(DotName.STRING_NAME)) {
return Type.newBuilder().setString(xyz.block.ftl.v1.schema.String.newBuilder().build()).build();
}
Expand Down Expand Up @@ -601,6 +644,18 @@ private Type buildType(ExtractionContext context, org.jboss.jandex.Type type) {
.addTypeParameters(buildType(context, paramType.arguments().get(0)))
.addTypeParameters(Type.newBuilder().setUnit(Unit.newBuilder().build())))
.build();
} else {
ClassInfo classByName = context.index().getComputingIndex().getClassByName(paramType.name());
var cb = ClassType.builder(classByName.name());
var main = buildType(context, cb.build());
var builder = main.toBuilder();
var refBuilder = builder.getRef().toBuilder();

for (var arg : paramType.arguments()) {
refBuilder.addTypeParameters(buildType(context, arg));
}
builder.setRef(refBuilder);
return builder.build();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package xyz.block.ftl.deployment;

import org.jboss.jandex.AnnotationInstance;

public record RetryRecord(int count, String minBackoff, String maxBackoff, String catchModule, String catchVerb) {

public static RetryRecord fromJandex(AnnotationInstance nested, String currentModuleName) {
return new RetryRecord(
nested.value("count") != null ? nested.value("count").asInt() : 0,
nested.value("minBackoff") != null ? nested.value("minBackoff").asString() : "",
nested.value("maxBackoff") != null ? nested.value("maxBackoff").asString() : "",
nested.value("catchModule") != null ? nested.value("catchModule").asString() : currentModuleName,
nested.value("catchVerb") != null ? nested.value("catchVerb").asString() : "");
}

public boolean isEmpty() {
return count == 0 && minBackoff.isEmpty() && maxBackoff.isEmpty() && catchVerb.isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.util.Map;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.DotName;

import io.quarkus.builder.item.SimpleBuildItem;
Expand All @@ -20,4 +22,14 @@ public Map<DotName, SubscriptionAnnotation> getAnnotations() {

public record SubscriptionAnnotation(String module, String topic, String name) {
}

public static SubscriptionAnnotation fromJandex(AnnotationInstance subscriptions, String currentModuleName) {
AnnotationValue moduleValue = subscriptions.value("module");

return new SubscriptionMetaAnnotationsBuildItem.SubscriptionAnnotation(
moduleValue == null || moduleValue.asString().isEmpty() ? currentModuleName
: moduleValue.asString(),
subscriptions.value("topic").asString(),
subscriptions.value("name").asString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import java.util.Set;

import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.DotName;
import org.jboss.jandex.Type;

Expand Down Expand Up @@ -84,18 +83,14 @@ TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer<Generat
@BuildStep
SubscriptionMetaAnnotationsBuildItem subscriptionAnnotations(CombinedIndexBuildItem combinedIndexBuildItem,
ModuleNameBuildItem moduleNameBuildItem) {

Map<DotName, SubscriptionMetaAnnotationsBuildItem.SubscriptionAnnotation> annotations = new HashMap<>();
for (var subscriptions : combinedIndexBuildItem.getComputingIndex().getAnnotations(Subscription.class)) {
if (subscriptions.target().kind() != AnnotationTarget.Kind.TYPE) {
if (subscriptions.target().kind() != AnnotationTarget.Kind.CLASS) {
continue;
}
AnnotationValue moduleValue = subscriptions.value("module");
annotations.put(subscriptions.target().asClass().name(),
new SubscriptionMetaAnnotationsBuildItem.SubscriptionAnnotation(
moduleValue == null || moduleValue.asString().isEmpty() ? moduleNameBuildItem.getModuleName()
: moduleValue.asString(),
subscriptions.value("topic").asString(),
subscriptions.value("name").asString()));
SubscriptionMetaAnnotationsBuildItem.fromJandex(subscriptions, moduleNameBuildItem.getModuleName()));
}
return new SubscriptionMetaAnnotationsBuildItem(annotations);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package xyz.block.ftl;

/**
* Indicates that the class was generated from an external module.
*/
public @interface GeneratedRef {

String name();

String module();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package xyz.block.ftl;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface Retry {
int count() default 0;

String minBackoff() default "";

String maxBackoff() default "";

String catchModule() default "";

String catchVerb() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.lang.annotation.RetentionPolicy;

/**
* Used to override the name of a verb
* Used to override the name of a verb. Without this annotation it defaults to the method name.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface VerbName {
Expand Down

0 comments on commit d25dde4

Please sign in to comment.