From 224476426a1b55f46bc4e0ab420e5cd12e6d8cca Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Fri, 20 Dec 2024 15:08:58 +1100 Subject: [PATCH 1/2] fix: handle JVM deserialization of generic types --- .../block/ftl/deployment/ModuleBuilder.java | 9 ++-- .../xyz/block/ftl/runtime/FTLRecorder.java | 15 +++---- .../xyz/block/ftl/runtime/VerbRegistry.java | 44 ++++++++++++++++--- 3 files changed, 48 insertions(+), 20 deletions(-) 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 98f3c4d933..914c47db22 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 @@ -17,7 +17,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.regex.Pattern; @@ -36,7 +35,6 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.arc.processor.DotNames; import xyz.block.ftl.Config; @@ -73,7 +71,6 @@ import xyz.block.ftl.schema.v1.TypeAlias; import xyz.block.ftl.schema.v1.Unit; import xyz.block.ftl.schema.v1.Verb; -import xyz.block.ftl.v1.CallRequest; public class ModuleBuilder { @@ -190,7 +187,7 @@ public void registerVerbMethod(MethodInfo method, String className, boolean exported, BodyType bodyType, Consumer metadataCallback) { try { List> parameterTypes = new ArrayList<>(); - List> paramMappers = new ArrayList<>(); + List paramMappers = new ArrayList<>(); org.jboss.jandex.Type bodyParamType = null; Nullability bodyParamNullability = Nullability.MISSING; @@ -199,7 +196,9 @@ public void registerVerbMethod(MethodInfo method, String className, MetadataCalls.Builder callsMetadata = MetadataCalls.newBuilder(); MetadataConfig.Builder configMetadata = MetadataConfig.newBuilder(); MetadataSecrets.Builder secretMetadata = MetadataSecrets.newBuilder(); + var pos = -1; for (var param : method.parameters()) { + pos++; if (param.hasAnnotation(Secret.class)) { Class paramType = ModuleBuilder.loadClass(param.type()); parameterTypes.add(paramType); @@ -248,7 +247,7 @@ public void registerVerbMethod(MethodInfo method, String className, Class paramType = ModuleBuilder.loadClass(param.type()); parameterTypes.add(paramType); //TODO: map and list types - paramMappers.add(new VerbRegistry.BodySupplier(paramType)); + paramMappers.add(new VerbRegistry.BodySupplier(pos)); } else { throw new RuntimeException("Unknown parameter type " + param.type() + " on FTL method: " + method.declaringClass().name() + "." + method.name()); diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java index 3b67e8e65e..8dac193022 100644 --- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Timer; import java.util.TimerTask; -import java.util.function.BiFunction; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor; @@ -27,7 +26,7 @@ public class FTLRecorder { public static final String X_FTL_VERB = "X-ftl-verb"; public void registerVerb(String module, String verbName, String methodName, List> parameterTypes, - Class verbHandlerClass, List> paramMappers, + Class verbHandlerClass, List paramMappers, boolean allowNullReturn) { //TODO: this sucks try { @@ -67,11 +66,11 @@ public void registerEnum(Class ennum, List> variants) { } } - public BiFunction topicSupplier(String className, String callingVerb) { + public VerbRegistry.ParameterSupplier topicSupplier(String className, String callingVerb) { try { var cls = Thread.currentThread().getContextClassLoader().loadClass(className.replace("/", ".")); var topic = cls.getDeclaredConstructor(String.class).newInstance(callingVerb); - return new BiFunction() { + return new VerbRegistry.ParameterSupplier() { @Override public Object apply(ObjectMapper mapper, CallRequest callRequest) { return topic; @@ -82,11 +81,11 @@ public Object apply(ObjectMapper mapper, CallRequest callRequest) { } } - public BiFunction verbClientSupplier(String className) { + public VerbRegistry.ParameterSupplier verbClientSupplier(String className) { try { var cls = Thread.currentThread().getContextClassLoader().loadClass(className.replace("/", ".")); var client = cls.getDeclaredConstructor().newInstance(); - return new BiFunction() { + return new VerbRegistry.ParameterSupplier() { @Override public Object apply(ObjectMapper mapper, CallRequest callRequest) { return client; @@ -97,8 +96,8 @@ public Object apply(ObjectMapper mapper, CallRequest callRequest) { } } - public BiFunction leaseClientSupplier() { - return new BiFunction() { + public VerbRegistry.ParameterSupplier leaseClientSupplier() { + return new VerbRegistry.ParameterSupplier() { @Override public Object apply(ObjectMapper mapper, CallRequest callRequest) { diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java index cb9a7e9d64..c02ceb813e 100644 --- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -16,6 +17,7 @@ import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.google.protobuf.ByteString; import io.quarkus.arc.Arc; @@ -37,7 +39,7 @@ public VerbRegistry(ObjectMapper mapper) { } public void register(String module, String name, InstanceHandle verbHandlerClass, Method method, - List> paramMappers, boolean allowNullReturn) { + List paramMappers, boolean allowNullReturn) { verbs.put(new Key(module, name), new AnnotatedEndpointHandler(verbHandlerClass, method, paramMappers, allowNullReturn)); } @@ -61,15 +63,18 @@ private record Key(String module, String name) { private class AnnotatedEndpointHandler implements VerbInvoker { final InstanceHandle verbHandlerClass; final Method method; - final List> parameterSuppliers; + final List parameterSuppliers; final boolean allowNull; private AnnotatedEndpointHandler(InstanceHandle verbHandlerClass, Method method, - List> parameterSuppliers, boolean allowNull) { + List parameterSuppliers, boolean allowNull) { this.verbHandlerClass = verbHandlerClass; this.method = method; this.parameterSuppliers = parameterSuppliers; this.allowNull = allowNull; + for (ParameterSupplier parameterSupplier : parameterSuppliers) { + parameterSupplier.init(method); + } } public CallResponse handle(CallRequest in) { @@ -106,19 +111,36 @@ public CallResponse handle(CallRequest in) { } } - public record BodySupplier(Class inputClass) implements BiFunction { + public static class BodySupplier implements ParameterSupplier { + + final int parameterIndex; + volatile Type inputClass; + + public BodySupplier(int parameterIndex) { + this.parameterIndex = parameterIndex; + } + + public void init(Method method) { + inputClass = method.getGenericParameterTypes()[parameterIndex]; + } @Override public Object apply(ObjectMapper mapper, CallRequest in) { try { - return mapper.createParser(in.getBody().newInput()).readValueAs(inputClass); + ObjectReader reader = mapper.reader(); + return reader.forType(reader.getTypeFactory().constructType(inputClass)) + .readValue(in.getBody().newInput()); } catch (IOException e) { throw new RuntimeException(e); } } + + public int getParameterIndex() { + return parameterIndex; + } } - public static class SecretSupplier implements BiFunction, ParameterExtractor { + public static class SecretSupplier implements ParameterSupplier, ParameterExtractor { final String name; final Class inputClass; @@ -153,7 +175,7 @@ public Object extractParameter(ResteasyReactiveRequestContext context) { } } - public static class ConfigSupplier implements BiFunction, ParameterExtractor { + public static class ConfigSupplier implements ParameterSupplier, ParameterExtractor { final String name; final Class inputClass; @@ -187,4 +209,12 @@ public String getName() { } } + public interface ParameterSupplier extends BiFunction { + + // TODO: this is pretty yuck, but it lets us avoid a whole heap of nasty stuff to get the generic type + default void init(Method method) { + + } + + } } From 44a96d05cdd98a7e3cb37bf317e9181d0a3c8c6a Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Fri, 20 Dec 2024 15:27:48 +1100 Subject: [PATCH 2/2] tmp --- backend/runner/pubsub/integration_test.go | 2 +- jvm-runtime/jvm_integration_test.go | 6 +++ jvm-runtime/testdata/go/gomodule/server.go | 7 +++ jvm-runtime/testdata/go/gomodule/types.ftl.go | 8 ++- .../block/ftl/test/TestInvokeGoFromJava.java | 50 ++++--------------- .../block/ftl/test/TestInvokeGoFromKotlin.kt | 7 +++ 6 files changed, 37 insertions(+), 43 deletions(-) diff --git a/backend/runner/pubsub/integration_test.go b/backend/runner/pubsub/integration_test.go index af10cb4f97..62c22d7991 100644 --- a/backend/runner/pubsub/integration_test.go +++ b/backend/runner/pubsub/integration_test.go @@ -74,7 +74,7 @@ func TestRetry(t *testing.T) { checkConsumed("subscriber", "consumeButFailAndRetry", false, retriesPerCall+1, optional.Some("secondCall")), checkPublished("subscriber", "consumeButFailAndRetryFailed", 2), - in.IfLanguage("go", checkConsumed("subscriber", "consumeFromDeadLetter", true, 2, optional.None[string]())), + checkConsumed("subscriber", "consumeFromDeadLetter", true, 2, optional.None[string]()), ) } diff --git a/jvm-runtime/jvm_integration_test.go b/jvm-runtime/jvm_integration_test.go index 7fed09287b..d187f45f51 100644 --- a/jvm-runtime/jvm_integration_test.go +++ b/jvm-runtime/jvm_integration_test.go @@ -152,6 +152,7 @@ func TestJVMCoreFunctionality(t *testing.T) { }) })...) tests = append(tests, AllRuntimesVerbTest("testObjectVerb", exampleObject)...) + tests = append(tests, AllRuntimesVerbTest("testGenericType", FailedEvent[TestObject]{Event: exampleObject, Error: "failed"})...) tests = append(tests, AllRuntimesVerbTest("testObjectOptionalFieldsVerb", exampleOptionalFieldsObject)...) tests = append(tests, AllRuntimesVerbTest("objectMapVerb", map[string]TestObject{"hello": exampleObject})...) tests = append(tests, AllRuntimesVerbTest("objectArrayVerb", []TestObject{exampleObject})...) @@ -480,3 +481,8 @@ type Thing struct{} func (Word) tag() {} func (Thing) tag() {} + +type FailedEvent[Event any] struct { + Event Event `json:"event"` + Error string `json:"error"` +} diff --git a/jvm-runtime/testdata/go/gomodule/server.go b/jvm-runtime/testdata/go/gomodule/server.go index 55200a80a5..9604ad9ba3 100644 --- a/jvm-runtime/testdata/go/gomodule/server.go +++ b/jvm-runtime/testdata/go/gomodule/server.go @@ -5,6 +5,8 @@ import ( "fmt" "time" + "ftl/builtin" + "github.com/decentralized-identity/web5-go/dids/did" "github.com/block/ftl/go-runtime/ftl" @@ -219,6 +221,11 @@ func TestObjectVerb(ctx context.Context, val TestObject) (TestObject, error) { return val, nil } +//ftl:verb export +func TestGenericType(ctx context.Context, val builtin.FailedEvent[TestObject]) (builtin.FailedEvent[TestObject], error) { + return val, nil +} + //ftl:verb export func TestObjectOptionalFieldsVerb(ctx context.Context, val TestObjectOptionalFields) (TestObjectOptionalFields, error) { return val, nil diff --git a/jvm-runtime/testdata/go/gomodule/types.ftl.go b/jvm-runtime/testdata/go/gomodule/types.ftl.go index 0a90cefcb4..cb44a196a4 100644 --- a/jvm-runtime/testdata/go/gomodule/types.ftl.go +++ b/jvm-runtime/testdata/go/gomodule/types.ftl.go @@ -3,8 +3,9 @@ package gomodule import ( "context" - "github.com/block/ftl/go-runtime/ftl" + ftlbuiltin "ftl/builtin" "github.com/block/ftl/common/reflection" + "github.com/block/ftl/go-runtime/ftl" "github.com/decentralized-identity/web5-go/dids/did" stdtime "time" ) @@ -65,6 +66,8 @@ type StringMapVerbClient func(context.Context, map[string]string) (map[string]st type StringVerbClient func(context.Context, string) (string, error) +type TestGenericTypeClient func(context.Context, ftlbuiltin.FailedEvent[TestObject]) (ftlbuiltin.FailedEvent[TestObject], error) + type TestObjectOptionalFieldsVerbClient func(context.Context, TestObjectOptionalFields) (TestObjectOptionalFields, error) type TestObjectVerbClient func(context.Context, TestObject) (TestObject, error) @@ -172,6 +175,9 @@ func init() { reflection.ProvideResourcesForVerb( StringVerb, ), + reflection.ProvideResourcesForVerb( + TestGenericType, + ), reflection.ProvideResourcesForVerb( TestObjectOptionalFieldsVerb, ), diff --git a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java index b3e003255a..4e48a47475 100644 --- a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java +++ b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java @@ -7,47 +7,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import ftl.gomodule.AnimalWrapper; -import ftl.gomodule.BoolVerbClient; -import ftl.gomodule.BytesVerbClient; -import ftl.gomodule.ColorWrapper; -import ftl.gomodule.EmptyVerbClient; -import ftl.gomodule.ErrorEmptyVerbClient; -import ftl.gomodule.ExternalTypeVerbClient; -import ftl.gomodule.FloatVerbClient; -import ftl.gomodule.IntVerbClient; -import ftl.gomodule.ObjectArrayVerbClient; -import ftl.gomodule.ObjectMapVerbClient; -import ftl.gomodule.OptionalBoolVerbClient; -import ftl.gomodule.OptionalBytesVerbClient; -import ftl.gomodule.OptionalFloatVerbClient; -import ftl.gomodule.OptionalIntVerbClient; -import ftl.gomodule.OptionalStringArrayVerbClient; -import ftl.gomodule.OptionalStringMapVerbClient; -import ftl.gomodule.OptionalStringVerbClient; -import ftl.gomodule.OptionalTestObjectOptionalFieldsVerbClient; -import ftl.gomodule.OptionalTestObjectVerbClient; -import ftl.gomodule.OptionalTimeVerbClient; -import ftl.gomodule.ParameterizedObjectVerbClient; -import ftl.gomodule.ParameterizedType; -import ftl.gomodule.Scalar; -import ftl.gomodule.ShapeWrapper; -import ftl.gomodule.SinkVerbClient; -import ftl.gomodule.SourceVerbClient; -import ftl.gomodule.StringArrayVerbClient; -import ftl.gomodule.StringEnumVerbClient; -import ftl.gomodule.StringList; -import ftl.gomodule.StringMapVerbClient; -import ftl.gomodule.StringVerbClient; -import ftl.gomodule.TestObject; -import ftl.gomodule.TestObjectOptionalFields; -import ftl.gomodule.TestObjectOptionalFieldsVerbClient; -import ftl.gomodule.TestObjectVerbClient; -import ftl.gomodule.TimeVerbClient; -import ftl.gomodule.TypeEnumVerbClient; -import ftl.gomodule.TypeEnumWrapper; -import ftl.gomodule.TypeWrapperEnumVerbClient; -import ftl.gomodule.ValueEnumVerbClient; +import ftl.builtin.FailedEvent; +import ftl.gomodule.*; import web5.sdk.dids.didcore.Did; import xyz.block.ftl.Export; import xyz.block.ftl.Verb; @@ -154,6 +115,13 @@ public boolean boolVerb(boolean val, BoolVerbClient client) { return client.testObjectVerb(val); } + @Export + @Verb + public @NotNull FailedEvent testGenericType(@NotNull FailedEvent val, + TestGenericTypeClient client) { + return client.testGenericType(val); + } + @Export @Verb public @NotNull TestObjectOptionalFields testObjectOptionalFieldsVerb(@NotNull TestObjectOptionalFields val, diff --git a/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt index efb225e6d6..26ec497604 100644 --- a/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt +++ b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt @@ -1,5 +1,6 @@ package xyz.block.ftl.test +import ftl.builtin.FailedEvent import ftl.gomodule.* import web5.sdk.dids.didcore.Did import xyz.block.ftl.Export @@ -117,6 +118,12 @@ fun testObjectOptionalFieldsVerb( return client.testObjectOptionalFieldsVerb(payload) } +@Export +@Verb +fun testGenericType(payload: FailedEvent, client: TestGenericTypeClient): FailedEvent { + return client.testGenericType(payload) +} + // now the same again but with option return / input types @Export @Verb