From 648efac8b38c54952738140822fddbb4f0485f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mathieu?= Date: Wed, 1 Sep 2021 11:35:54 +0200 Subject: [PATCH] GCF v2 and Cloud Event support --- bom/application/pom.xml | 2 +- .../functions/FunqyBackgroundFunction.java | 2 +- .../functions/FunqyCloudEventsFunction.java | 57 ++++++++++++++ .../FunqyCloudFunctionsBindingRecorder.java | 15 ++++ .../GoogleCloudFunctionsProcessor.java | 5 ++ .../functions/GoogleCloudFunctionInfo.java | 5 +- .../GoogleCloudFunctionRecorder.java | 1 + .../functions/QuarkusCloudEventsFunction.java | 74 +++++++++++++++++++ .../gcp/functions/test/GreetingFunctions.java | 10 +++ .../src/main/resources/application.properties | 3 +- .../function/test/CloudEventStorageTest.java | 26 +++++++ .../src/main/resources/application.properties | 3 +- 12 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyCloudEventsFunction.java create mode 100644 extensions/google-cloud-functions/runtime/src/main/java/io/quarkus/gcp/functions/QuarkusCloudEventsFunction.java create mode 100644 integration-tests/google-cloud-functions/src/main/java/io/quarkus/gcp/function/test/CloudEventStorageTest.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 6a9935ffbd505b..2e5b82b87bc54a 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -194,7 +194,7 @@ 3.17.3 ${protobuf-java.version} 4.6.1 - 1.0.1 + 1.0.4 1.21 2.8.6 0.46 diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyBackgroundFunction.java b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyBackgroundFunction.java index 8e612af8c7704c..92743edc5564e1 100644 --- a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyBackgroundFunction.java +++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyBackgroundFunction.java @@ -24,7 +24,7 @@ public class FunqyBackgroundFunction implements RawBackgroundFunction { Thread.currentThread().setContextClassLoader(FunqyBackgroundFunction.class.getClassLoader()); Class appClass = Class.forName("io.quarkus.runner.ApplicationImpl"); String[] args = {}; - Application app = (Application) appClass.newInstance(); + Application app = (Application) appClass.getConstructor().newInstance(); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyCloudEventsFunction.java b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyCloudEventsFunction.java new file mode 100644 index 00000000000000..eff2b350299242 --- /dev/null +++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyCloudEventsFunction.java @@ -0,0 +1,57 @@ +package io.quarkus.funqy.gcp.functions; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import com.google.cloud.functions.CloudEventsFunction; + +import io.cloudevents.CloudEvent; +import io.quarkus.runtime.Application; + +public class FunqyCloudEventsFunction implements CloudEventsFunction { + protected static final String deploymentStatus; + protected static boolean started = false; + + static { + StringWriter error = new StringWriter(); + PrintWriter errorWriter = new PrintWriter(error, true); + if (Application.currentApplication() == null) { // were we already bootstrapped? Needed for mock unit testing. + // For GCP functions, we need to set the TCCL to the QuarkusHttpFunction classloader then restore it. + // Without this, we have a lot of classloading issues (ClassNotFoundException on existing classes) + // during static init. + ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(FunqyCloudEventsFunction.class.getClassLoader()); + Class appClass = Class.forName("io.quarkus.runner.ApplicationImpl"); + String[] args = {}; + Application app = (Application) appClass.getConstructor().newInstance(); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + app.stop(); + } + }); + app.start(args); + errorWriter.println("Quarkus bootstrapped successfully."); + started = true; + } catch (Exception ex) { + errorWriter.println("Quarkus bootstrap failed."); + ex.printStackTrace(errorWriter); + } finally { + Thread.currentThread().setContextClassLoader(currentCl); + } + } else { + errorWriter.println("Quarkus bootstrapped successfully."); + started = true; + } + deploymentStatus = error.toString(); + } + + @Override + public void accept(CloudEvent cloudEvent) throws Exception { + if (!started) { + throw new RuntimeException(deploymentStatus); + } + FunqyCloudFunctionsBindingRecorder.handle(cloudEvent); + } +} diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyCloudFunctionsBindingRecorder.java b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyCloudFunctionsBindingRecorder.java index fa4b57b26e3372..e16a6fdd6b5ce2 100644 --- a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyCloudFunctionsBindingRecorder.java +++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyCloudFunctionsBindingRecorder.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.ObjectWriter; import com.google.cloud.functions.Context; +import io.cloudevents.CloudEvent; import io.quarkus.arc.ManagedContext; import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.funqy.runtime.FunctionConstructor; @@ -96,6 +97,20 @@ public static void handle(String event, Context context) { } } + /** + * Handle CloudEventsFunction + * + * @param cloudEvent + */ + public static void handle(CloudEvent cloudEvent) { + FunqyServerResponse response = dispatch(cloudEvent); + + Object value = response.getOutput().await().indefinitely(); + if (value != null) { + throw new RuntimeException("A background function cannot return a value"); + } + } + private static FunqyServerResponse dispatch(Object input) { ManagedContext requestContext = beanContainer.requestContext(); requestContext.activate(); diff --git a/extensions/google-cloud-functions/deployment/src/main/java/io/quarkus/gcp/functions/deployment/GoogleCloudFunctionsProcessor.java b/extensions/google-cloud-functions/deployment/src/main/java/io/quarkus/gcp/functions/deployment/GoogleCloudFunctionsProcessor.java index c9e032b62e44f2..de99ed97b30cc1 100755 --- a/extensions/google-cloud-functions/deployment/src/main/java/io/quarkus/gcp/functions/deployment/GoogleCloudFunctionsProcessor.java +++ b/extensions/google-cloud-functions/deployment/src/main/java/io/quarkus/gcp/functions/deployment/GoogleCloudFunctionsProcessor.java @@ -14,6 +14,7 @@ import org.jboss.jandex.IndexView; import com.google.cloud.functions.BackgroundFunction; +import com.google.cloud.functions.CloudEventsFunction; import com.google.cloud.functions.HttpFunction; import com.google.cloud.functions.RawBackgroundFunction; @@ -36,6 +37,7 @@ public class GoogleCloudFunctionsProcessor { public static final DotName DOTNAME_HTTP_FUNCTION = DotName.createSimple(HttpFunction.class.getName()); public static final DotName DOTNAME_BACKGROUND_FUNCTION = DotName.createSimple(BackgroundFunction.class.getName()); public static final DotName DOTNAME_RAW_BACKGROUND_FUNCTION = DotName.createSimple(RawBackgroundFunction.class.getName()); + public static final DotName DOTNAME_CLOUD_EVENT_FUNCTION = DotName.createSimple(CloudEventsFunction.class.getName()); @BuildStep public FeatureBuildItem feature() { @@ -56,6 +58,7 @@ public List discoverFunctionClass(CombinedIndexBuildItem Collection httpFunctions = index.getAllKnownImplementors(DOTNAME_HTTP_FUNCTION); Collection backgroundFunctions = index.getAllKnownImplementors(DOTNAME_BACKGROUND_FUNCTION); Collection rawBackgroundFunctions = index.getAllKnownImplementors(DOTNAME_RAW_BACKGROUND_FUNCTION); + Collection cloudEventFunctions = index.getAllKnownImplementors(DOTNAME_CLOUD_EVENT_FUNCTION); List cloudFunctions = new ArrayList<>(); cloudFunctions.addAll( @@ -65,6 +68,8 @@ public List discoverFunctionClass(CombinedIndexBuildItem cloudFunctions.addAll( registerFunctions(unremovableBeans, rawBackgroundFunctions, GoogleCloudFunctionInfo.FunctionType.RAW_BACKGROUND)); + cloudFunctions.addAll( + registerFunctions(unremovableBeans, cloudEventFunctions, GoogleCloudFunctionInfo.FunctionType.CLOUD_EVENT)); if (cloudFunctions.isEmpty()) { throw new BuildException("No Google Cloud Function found on the classpath", Collections.emptyList()); diff --git a/extensions/google-cloud-functions/runtime/src/main/java/io/quarkus/gcp/functions/GoogleCloudFunctionInfo.java b/extensions/google-cloud-functions/runtime/src/main/java/io/quarkus/gcp/functions/GoogleCloudFunctionInfo.java index 46c3aa9a0de9d9..d9f8dc5a4ef44c 100644 --- a/extensions/google-cloud-functions/runtime/src/main/java/io/quarkus/gcp/functions/GoogleCloudFunctionInfo.java +++ b/extensions/google-cloud-functions/runtime/src/main/java/io/quarkus/gcp/functions/GoogleCloudFunctionInfo.java @@ -29,9 +29,10 @@ public void setFunctionType(FunctionType functionType) { this.functionType = functionType; } - public static enum FunctionType { + public enum FunctionType { HTTP, BACKGROUND, - RAW_BACKGROUND; + RAW_BACKGROUND, + CLOUD_EVENT } } diff --git a/extensions/google-cloud-functions/runtime/src/main/java/io/quarkus/gcp/functions/GoogleCloudFunctionRecorder.java b/extensions/google-cloud-functions/runtime/src/main/java/io/quarkus/gcp/functions/GoogleCloudFunctionRecorder.java index d7acc2b2274146..8f4cb73a0d2466 100644 --- a/extensions/google-cloud-functions/runtime/src/main/java/io/quarkus/gcp/functions/GoogleCloudFunctionRecorder.java +++ b/extensions/google-cloud-functions/runtime/src/main/java/io/quarkus/gcp/functions/GoogleCloudFunctionRecorder.java @@ -37,5 +37,6 @@ public void selectDelegate(GoogleCloudFunctionsConfig config, List appClass = Class.forName("io.quarkus.runner.ApplicationImpl"); + String[] args = {}; + Application app = (Application) appClass.getConstructor().newInstance(); + app.start(args); + errorWriter.println("Quarkus bootstrapped successfully."); + started = true; + } catch (Exception ex) { + errorWriter.println("Quarkus bootstrap failed."); + ex.printStackTrace(errorWriter); + } finally { + Thread.currentThread().setContextClassLoader(currentCl); + } + } else { + errorWriter.println("Quarkus bootstrapped successfully."); + started = true; + } + deploymentStatus = error.toString(); + } + + static void setDelegate(String selectedDelegate) { + if (selectedDelegate != null) { + try { + Class clazz = Class.forName(selectedDelegate, false, Thread.currentThread().getContextClassLoader()); + delegate = (CloudEventsFunction) Arc.container().instance(clazz).get(); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void accept(CloudEvent cloudEvent) throws Exception { + if (!started) { + throw new IOException(deploymentStatus); + } + + // TODO maybe we can check this at static init + if (delegate == null) { + throw new IOException("We didn't found any CloudEventsFunction to run " + + "(or there is multiple one and none selected inside your application.properties)"); + } + + delegate.accept(cloudEvent); + } +} diff --git a/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/GreetingFunctions.java b/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/GreetingFunctions.java index 9ec3c7ee68dcf0..c7e1bb98ad909c 100644 --- a/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/GreetingFunctions.java +++ b/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/GreetingFunctions.java @@ -2,6 +2,7 @@ import javax.inject.Inject; +import io.cloudevents.CloudEvent; import io.quarkus.funqy.Funq; import io.quarkus.funqy.gcp.functions.event.PubsubMessage; import io.quarkus.funqy.gcp.functions.event.StorageEvent; @@ -23,4 +24,13 @@ public void helloGCSWorld(StorageEvent storageEvent) { System.out.println(storageEvent.name + " - " + message); } + @Funq + public void helloCloudEvent(CloudEvent cloudEvent) { + System.out.println("Receive event Id: " + cloudEvent.getId()); + System.out.println("Receive event Subject: " + cloudEvent.getSubject()); + System.out.println("Receive event Type: " + cloudEvent.getType()); + System.out.println("Receive event Data: " + new String(cloudEvent.getData().toBytes())); + System.out.println("Be polite, say " + service.hello("world")); + } + } diff --git a/integration-tests/funqy-google-cloud-functions/src/main/resources/application.properties b/integration-tests/funqy-google-cloud-functions/src/main/resources/application.properties index d35c85ba890498..59d042700da56c 100644 --- a/integration-tests/funqy-google-cloud-functions/src/main/resources/application.properties +++ b/integration-tests/funqy-google-cloud-functions/src/main/resources/application.properties @@ -1,2 +1,3 @@ -quarkus.funqy.export=helloGCSWorld +#quarkus.funqy.export=helloGCSWorld #quarkus.funqy.export=helloPubSubWorld +quarkus.funqy.export=helloCloudEvent diff --git a/integration-tests/google-cloud-functions/src/main/java/io/quarkus/gcp/function/test/CloudEventStorageTest.java b/integration-tests/google-cloud-functions/src/main/java/io/quarkus/gcp/function/test/CloudEventStorageTest.java new file mode 100644 index 00000000000000..9d2c358600ffb4 --- /dev/null +++ b/integration-tests/google-cloud-functions/src/main/java/io/quarkus/gcp/function/test/CloudEventStorageTest.java @@ -0,0 +1,26 @@ +package io.quarkus.gcp.function.test; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.cloud.functions.CloudEventsFunction; + +import io.cloudevents.CloudEvent; +import io.quarkus.gcp.function.test.service.GreetingService; + +@Named("cloudEventTest") +@ApplicationScoped +public class CloudEventStorageTest implements CloudEventsFunction { + @Inject + GreetingService greetingService; + + @Override + public void accept(CloudEvent cloudEvent) throws Exception { + System.out.println("Receive event Id: " + cloudEvent.getId()); + System.out.println("Receive event Subject: " + cloudEvent.getSubject()); + System.out.println("Receive event Type: " + cloudEvent.getType()); + System.out.println("Receive event Data: " + new String(cloudEvent.getData().toBytes())); + System.out.println("Be polite, say " + greetingService.hello()); + } +} diff --git a/integration-tests/google-cloud-functions/src/main/resources/application.properties b/integration-tests/google-cloud-functions/src/main/resources/application.properties index c5c81be1fce44f..836825d7531740 100644 --- a/integration-tests/google-cloud-functions/src/main/resources/application.properties +++ b/integration-tests/google-cloud-functions/src/main/resources/application.properties @@ -1,3 +1,4 @@ -quarkus.google-cloud-functions.function=httpTest +#quarkus.google-cloud-functions.function=httpTest +quarkus.google-cloud-functions.function=cloudEventTest #quarkus.google-cloud-functions.function=rawPubSubTest #quarkus.google-cloud-functions.function=storageTest \ No newline at end of file