From 80348ea818e59e9365416f1a529431d3f42cf039 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Tue, 8 Oct 2024 12:10:12 +1100 Subject: [PATCH] feat: Improve local subscriptions --- .../ftl/java/test/publisher/Publisher.java | 13 ++++++++- docs/content/docs/reference/pubsub.md | 5 ++-- .../xyz/block/ftl/deployment/FTLDotNames.java | 2 ++ .../block/ftl/deployment/ModuleBuilder.java | 4 +-- .../SubscriptionMetaAnnotationsBuildItem.java | 28 +++++++++++++++++-- .../ftl/deployment/SubscriptionProcessor.java | 5 ++-- .../block/ftl/deployment/TopicsProcessor.java | 3 +- .../main/java/xyz/block/ftl/Subscription.java | 8 ++++-- 8 files changed, 53 insertions(+), 15 deletions(-) diff --git a/backend/controller/pubsub/testdata/java/publisher/src/main/java/xyz/block/ftl/java/test/publisher/Publisher.java b/backend/controller/pubsub/testdata/java/publisher/src/main/java/xyz/block/ftl/java/test/publisher/Publisher.java index f8c4056b82..d1e1f52eaa 100644 --- a/backend/controller/pubsub/testdata/java/publisher/src/main/java/xyz/block/ftl/java/test/publisher/Publisher.java +++ b/backend/controller/pubsub/testdata/java/publisher/src/main/java/xyz/block/ftl/java/test/publisher/Publisher.java @@ -2,6 +2,7 @@ import io.quarkus.logging.Log; import xyz.block.ftl.Export; +import xyz.block.ftl.Subscription; import xyz.block.ftl.Topic; import xyz.block.ftl.TopicDefinition; import xyz.block.ftl.Verb; @@ -14,6 +15,11 @@ interface TestTopic extends Topic { } + @TopicDefinition("localTopic") + interface LocalTopic extends Topic { + + } + @Export @TopicDefinition("topic2") interface Topic2 extends Topic { @@ -21,7 +27,7 @@ interface Topic2 extends Topic { } @Verb - void publishTen(TestTopic testTopic) throws Exception { + void publishTen(LocalTopic testTopic) throws Exception { for (var i = 0; i < 10; ++i) { var t = java.time.ZonedDateTime.now(); Log.infof("Publishing %s", t); @@ -42,4 +48,9 @@ void publishOneToTopic2(Topic2 topic2) throws Exception { Log.infof("Publishing %s", t); topic2.publish(new PubSubEvent().setTime(t)); } + + @Subscription(topicClass = LocalTopic.class, name = "localSubscription") + public void local(TestTopic testTopic, PubSubEvent event) { + testTopic.publish(event); + } } diff --git a/docs/content/docs/reference/pubsub.md b/docs/content/docs/reference/pubsub.md index c1e5b464e1..77a6b02080 100644 --- a/docs/content/docs/reference/pubsub.md +++ b/docs/content/docs/reference/pubsub.md @@ -116,14 +116,13 @@ There are two ways to subscribe to a topic. The first is to declare a method wit subscribing to a topic inside the same module: ```java -@Subscription(topic = "invoices", name = "invoicesSubscription") +@Subscription(topicClass = InvoiceTopic.class, name = "invoicesSubscription") public void consumeInvoice(Invoice event) { // ... } ``` -This is ok, but it requires the use of string constants for the topic name, which can be error-prone. If you are subscribing to a topic from -another module, FTL will generate a type-safe subscription meta annotation you can use to subscribe to the topic: +If you are subscribing to a topic from another module, FTL will generate a type-safe subscription meta annotation you can use to subscribe to the topic: ```java @Retention(java.lang.annotation.RetentionPolicy.RUNTIME) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java index 2bc6d65a46..07a9a9255d 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java @@ -11,6 +11,7 @@ import xyz.block.ftl.LeaseClient; import xyz.block.ftl.Secret; import xyz.block.ftl.Subscription; +import xyz.block.ftl.TopicDefinition; import xyz.block.ftl.TypeAlias; import xyz.block.ftl.TypeAliasMapper; import xyz.block.ftl.Verb; @@ -33,4 +34,5 @@ private FTLDotNames() { public static final DotName SUBSCRIPTION = DotName.createSimple(Subscription.class); public static final DotName LEASE_CLIENT = DotName.createSimple(LeaseClient.class); public static final DotName GENERATED_REF = DotName.createSimple(GeneratedRef.class); + public static final DotName TOPIC_DEFINITION = DotName.createSimple(TopicDefinition.class); } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java index 63639ece62..3f70b3a8b9 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java @@ -620,10 +620,10 @@ private boolean setDeclExport(String name, boolean export) { var existing = decls.get(name); if (existing != null) { if (existing.hasData()) { - var merged = existing.getData().toBuilder().setExport(export).build(); + var merged = existing.getData().toBuilder().setExport(export || existing.getData().getExport()).build(); decls.put(name, Decl.newBuilder().setData(merged).build()); } else if (existing.hasTypeAlias()) { - var merged = existing.getTypeAlias().toBuilder().setExport(export).build(); + var merged = existing.getTypeAlias().toBuilder().setExport(export || existing.getData().getExport()).build(); decls.put(name, Decl.newBuilder().setTypeAlias(merged).build()); } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionMetaAnnotationsBuildItem.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionMetaAnnotationsBuildItem.java index 83e2d35cf5..5cc950d072 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionMetaAnnotationsBuildItem.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionMetaAnnotationsBuildItem.java @@ -5,8 +5,10 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; import io.quarkus.builder.item.SimpleBuildItem; +import xyz.block.ftl.Topic; public final class SubscriptionMetaAnnotationsBuildItem extends SimpleBuildItem { @@ -23,13 +25,33 @@ public Map getAnnotations() { public record SubscriptionAnnotation(String module, String topic, String name) { } - public static SubscriptionAnnotation fromJandex(AnnotationInstance subscriptions, String currentModuleName) { - AnnotationValue moduleValue = subscriptions.value("module"); + public static SubscriptionAnnotation fromJandex(IndexView indexView, AnnotationInstance subscriptions, + String currentModuleName) { + AnnotationValue moduleValue = subscriptions.value("module"); + AnnotationValue topicValue = subscriptions.value("topic"); + AnnotationValue topicClassValue = subscriptions.value("topicClass"); + String topicName; + if (topicValue != null && !topicValue.asString().isEmpty()) { + if (topicClassValue != null && !topicClassValue.asClass().name().toString().equals(Topic.class.getName())) { + throw new IllegalArgumentException("Cannot specify both topic and topicClass"); + } + topicName = topicValue.asString(); + } else if (topicClassValue != null && !topicClassValue.asClass().name().toString().equals(Topic.class.getName())) { + var topicClass = indexView.getClassByName(topicClassValue.asClass().name()); + AnnotationInstance annotation = topicClass.annotation(FTLDotNames.TOPIC_DEFINITION); + if (annotation == null) { + throw new IllegalArgumentException( + "topicClass must be annotated with @TopicDefinition for subscription " + subscriptions); + } + topicName = annotation.value().asString(); + } else { + throw new IllegalArgumentException("Either topic or topicClass must be specified on " + subscriptions); + } return new SubscriptionMetaAnnotationsBuildItem.SubscriptionAnnotation( moduleValue == null || moduleValue.asString().isEmpty() ? currentModuleName : moduleValue.asString(), - subscriptions.value("topic").asString(), + topicName, subscriptions.value("name").asString()); } } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java index bfd17e2121..3d55cc15eb 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java @@ -38,7 +38,8 @@ SubscriptionMetaAnnotationsBuildItem subscriptionAnnotations(CombinedIndexBuildI continue; } annotations.put(subscriptions.target().asClass().name(), - SubscriptionMetaAnnotationsBuildItem.fromJandex(subscriptions, moduleNameBuildItem.getModuleName())); + SubscriptionMetaAnnotationsBuildItem.fromJandex(combinedIndexBuildItem.getComputingIndex(), subscriptions, + moduleNameBuildItem.getModuleName())); } return new SubscriptionMetaAnnotationsBuildItem(annotations); } @@ -52,7 +53,7 @@ public void registerSubscriptions(CombinedIndexBuildItem index, AdditionalBeanBuildItem.Builder beans = AdditionalBeanBuildItem.builder().setUnremovable(); var moduleName = moduleNameBuildItem.getModuleName(); for (var subscription : index.getIndex().getAnnotations(FTLDotNames.SUBSCRIPTION)) { - var info = SubscriptionMetaAnnotationsBuildItem.fromJandex(subscription, moduleName); + var info = SubscriptionMetaAnnotationsBuildItem.fromJandex(index.getComputingIndex(), subscription, moduleName); if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) { continue; } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java index dda47644dd..aff19617e8 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java @@ -19,7 +19,6 @@ import io.quarkus.gizmo.MethodDescriptor; import xyz.block.ftl.Export; import xyz.block.ftl.Topic; -import xyz.block.ftl.TopicDefinition; import xyz.block.ftl.runtime.TopicHelper; import xyz.block.ftl.v1.schema.Decl; @@ -30,7 +29,7 @@ public class TopicsProcessor { @BuildStep TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer generatedTopicProducer) { - var topicDefinitions = index.getComputingIndex().getAnnotations(TopicDefinition.class); + var topicDefinitions = index.getComputingIndex().getAnnotations(FTLDotNames.TOPIC_DEFINITION); log.infof("Processing %d topic definition annotations into decls", topicDefinitions.size()); Map topics = new HashMap<>(); Set names = new HashSet<>(); diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Subscription.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Subscription.java index 9b2283f5e0..7ecd4c5d84 100644 --- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Subscription.java +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Subscription.java @@ -15,9 +15,9 @@ /** * - * @return The name of the topic to subscribe to. + * @return The name of the topic to subscribe to. Cannot be used in conjunction with {@link #topicClass()}. */ - String topic(); + String topic() default ""; /** * @@ -25,4 +25,8 @@ */ String name(); + /** + * The class of the topic to subscribe to, which can be used in place of directly specifying the topic name and module. + */ + Class topicClass() default Topic.class; }