From 32707b2c611943c29b151cfe63eca55488cfb0ff Mon Sep 17 00:00:00 2001 From: Geoffrey GREBERT Date: Thu, 22 Aug 2024 01:58:40 +0200 Subject: [PATCH] add devservice --- .../ROOT/pages/includes/quarkus-temporal.adoc | 78 ++++++++- .../quarkus-temporal_quarkus.temporal.adoc | 78 ++++++++- extension/deployment/pom.xml | 20 ++- .../deployment/TemporalProcessor.java | 3 +- .../deployment/devui/TemporalContainer.java | 39 +++++ .../devui/TemporalDevUIProcessor.java | 60 ++++++- .../devui/TemporalDevserviceConfig.java | 33 ++++ .../devui/TemporalDevserviceProcessor.java | 77 +++++++++ .../deployment/devui/TemporalUiConfig.java | 18 +++ .../deployment/HealthCheckEnabledTest.java | 11 +- extension/runtime/pom.xml | 8 + .../config/ConnectionRuntimeConfig.java | 3 +- .../config/TemporalBuildtimeConfig.java | 5 +- .../temporal/devui/TemporalUiProxy.java | 74 +++++++++ integration-tests/devservice/pom.xml | 141 +++++++++++++++++ .../quarkiverse/temporal/it/cdi/CdiBean.java | 0 .../it/cdi/defaultWorker/CDIActivityImpl.java | 0 .../it/cdi/defaultWorker/CDIWorkflowImpl.java | 0 .../it/cdi/namedWorker/CDIActivityImpl.java | 0 .../it/cdi/namedWorker/CDIWorkflowImpl.java | 0 .../temporal/it/cdi/shared/CDIActivity.java | 0 .../temporal/it/cdi/shared/CDIWorkflow.java | 0 .../defaultWorker/AccountActivityImpl.java | 0 .../defaultWorker/CoreTransactionDetails.java | 0 .../MoneyTransferWorkflowImpl.java | 0 .../namedWorker/AccountActivityImpl.java | 0 .../namedWorker/CoreTransactionDetails.java | 0 .../MoneyTransferWorkflowImpl.java | 0 .../moneyTransfer/shared/AccountActivity.java | 0 .../shared/MoneyTransferWorkflow.java | 0 .../shared/TransactionDetails.java | 0 .../it/rest/MoneyTransferResource.java | 0 .../it/util/MDCContextPropagator.java | 0 .../src/main/resources/application.properties | 0 .../temporal/it/CDIActivityIT.java | 0 .../temporal/it/MoneyTransferIT.java | 0 integration-tests/mock/pom.xml | 143 +++++++++++++++++ .../quarkiverse/temporal/it/cdi/CdiBean.java | 12 ++ .../it/cdi/defaultWorker/CDIActivityImpl.java | 18 +++ .../it/cdi/defaultWorker/CDIWorkflowImpl.java | 36 +++++ .../it/cdi/namedWorker/CDIActivityImpl.java | 20 +++ .../it/cdi/namedWorker/CDIWorkflowImpl.java | 38 +++++ .../temporal/it/cdi/shared/CDIActivity.java | 12 ++ .../temporal/it/cdi/shared/CDIWorkflow.java | 11 ++ .../defaultWorker/AccountActivityImpl.java | 43 +++++ .../defaultWorker/CoreTransactionDetails.java | 43 +++++ .../MoneyTransferWorkflowImpl.java | 115 ++++++++++++++ .../namedWorker/AccountActivityImpl.java | 45 ++++++ .../namedWorker/CoreTransactionDetails.java | 43 +++++ .../MoneyTransferWorkflowImpl.java | 116 ++++++++++++++ .../moneyTransfer/shared/AccountActivity.java | 20 +++ .../shared/MoneyTransferWorkflow.java | 13 ++ .../shared/TransactionDetails.java | 16 ++ .../it/rest/MoneyTransferResource.java | 38 +++++ .../it/util/MDCContextPropagator.java | 139 ++++++++++++++++ .../src/main/resources/application.properties | 2 + .../temporal/it/CDIActivityIT.java | 43 +++++ .../temporal/it/MoneyTransferIT.java | 47 ++++++ integration-tests/pom.xml | 148 ++---------------- .../src/main/resources/application.properties | 2 +- pom.xml | 1 - 61 files changed, 1650 insertions(+), 162 deletions(-) create mode 100644 extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalContainer.java create mode 100644 extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceConfig.java create mode 100644 extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceProcessor.java create mode 100644 extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalUiConfig.java create mode 100644 extension/runtime/src/main/java/io/quarkiverse/temporal/devui/TemporalUiProxy.java create mode 100644 integration-tests/devservice/pom.xml rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/cdi/CdiBean.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIActivityImpl.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIWorkflowImpl.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIActivityImpl.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIWorkflowImpl.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIActivity.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIWorkflow.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/AccountActivityImpl.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/CoreTransactionDetails.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/MoneyTransferWorkflowImpl.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/AccountActivityImpl.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/CoreTransactionDetails.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/MoneyTransferWorkflowImpl.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/AccountActivity.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/MoneyTransferWorkflow.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/TransactionDetails.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/rest/MoneyTransferResource.java (100%) rename integration-tests/{ => devservice}/src/main/java/io/quarkiverse/temporal/it/util/MDCContextPropagator.java (100%) create mode 100644 integration-tests/devservice/src/main/resources/application.properties rename integration-tests/{ => devservice}/src/test/java/io/quarkiverse/temporal/it/CDIActivityIT.java (100%) rename integration-tests/{ => devservice}/src/test/java/io/quarkiverse/temporal/it/MoneyTransferIT.java (100%) create mode 100644 integration-tests/mock/pom.xml create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/CdiBean.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIActivityImpl.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIWorkflowImpl.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIActivityImpl.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIWorkflowImpl.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIActivity.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIWorkflow.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/AccountActivityImpl.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/CoreTransactionDetails.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/MoneyTransferWorkflowImpl.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/AccountActivityImpl.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/CoreTransactionDetails.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/MoneyTransferWorkflowImpl.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/AccountActivity.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/MoneyTransferWorkflow.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/TransactionDetails.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/rest/MoneyTransferResource.java create mode 100644 integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/util/MDCContextPropagator.java create mode 100644 integration-tests/mock/src/main/resources/application.properties create mode 100644 integration-tests/mock/src/test/java/io/quarkiverse/temporal/it/CDIActivityIT.java create mode 100644 integration-tests/mock/src/test/java/io/quarkiverse/temporal/it/MoneyTransferIT.java diff --git a/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc b/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc index cd3a8e4..288dd6e 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc @@ -8,11 +8,83 @@ h|[.header-title]##Configuration property## h|Type h|Default +a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-devservice-enabled]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-devservice-enabled[`quarkus.temporal.devservice.enabled`]## + +[.description] +-- +Enable the Temporal Devservice. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEMPORAL_DEVSERVICE_ENABLED+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEMPORAL_DEVSERVICE_ENABLED+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +|`true` + +a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-devservice-image]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-devservice-image[`quarkus.temporal.devservice.image`]## + +[.description] +-- +The image to use for the Temporal Devservice. + +Minimum supported version: `temporalio/auto-setup:1.24.3.0` + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEMPORAL_DEVSERVICE_IMAGE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEMPORAL_DEVSERVICE_IMAGE+++` +endif::add-copy-button-to-env-var[] +-- +|string +|`temporalio/auto-setup` + +a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-devservice-reuse]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-devservice-reuse[`quarkus.temporal.devservice.reuse`]## + +[.description] +-- +Whether to reuse the Temporal Devservice. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEMPORAL_DEVSERVICE_REUSE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEMPORAL_DEVSERVICE_REUSE+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +|`true` + +a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-ui-url]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-ui-url[`quarkus.temporal.ui.url`]## + +[.description] +-- +The url of the Temporal UI. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEMPORAL_UI_URL+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEMPORAL_UI_URL+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-enable-mock]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-enable-mock[`quarkus.temporal.enable-mock`]## [.description] -- -enable mock for testing +enable mock for testing. + +If enabled, the Temporal devservice will not be started. ifdef::add-copy-button-to-env-var[] @@ -701,7 +773,7 @@ a| [[quarkus-temporal_quarkus-temporal-connection-target]] [.property-path]##lin [.description] -- -Sets a target string, which can be either a valid `NameResolver`-compliant URI, or an authority string. See `ManagedChannelBuilder++#++forTarget(String)` for more information about parameter format. Default is 127.0.0.1:7233 +Sets a target string, which can be either a valid `NameResolver`-compliant URI, or an authority string. See `ManagedChannelBuilder++#++forTarget(String)` for more information about parameter format. ifdef::add-copy-button-to-env-var[] @@ -712,7 +784,7 @@ Environment variable: `+++QUARKUS_TEMPORAL_CONNECTION_TARGET+++` endif::add-copy-button-to-env-var[] -- |string -|`127.0.0.1:7233` +|required icon:exclamation-circle[title=Configuration property is required] a| [[quarkus-temporal_quarkus-temporal-connection-enable-https]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-connection-enable-https[`quarkus.temporal.connection.enable-https`]## diff --git a/docs/modules/ROOT/pages/includes/quarkus-temporal_quarkus.temporal.adoc b/docs/modules/ROOT/pages/includes/quarkus-temporal_quarkus.temporal.adoc index cd3a8e4..288dd6e 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-temporal_quarkus.temporal.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-temporal_quarkus.temporal.adoc @@ -8,11 +8,83 @@ h|[.header-title]##Configuration property## h|Type h|Default +a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-devservice-enabled]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-devservice-enabled[`quarkus.temporal.devservice.enabled`]## + +[.description] +-- +Enable the Temporal Devservice. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEMPORAL_DEVSERVICE_ENABLED+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEMPORAL_DEVSERVICE_ENABLED+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +|`true` + +a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-devservice-image]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-devservice-image[`quarkus.temporal.devservice.image`]## + +[.description] +-- +The image to use for the Temporal Devservice. + +Minimum supported version: `temporalio/auto-setup:1.24.3.0` + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEMPORAL_DEVSERVICE_IMAGE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEMPORAL_DEVSERVICE_IMAGE+++` +endif::add-copy-button-to-env-var[] +-- +|string +|`temporalio/auto-setup` + +a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-devservice-reuse]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-devservice-reuse[`quarkus.temporal.devservice.reuse`]## + +[.description] +-- +Whether to reuse the Temporal Devservice. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEMPORAL_DEVSERVICE_REUSE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEMPORAL_DEVSERVICE_REUSE+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +|`true` + +a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-ui-url]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-ui-url[`quarkus.temporal.ui.url`]## + +[.description] +-- +The url of the Temporal UI. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEMPORAL_UI_URL+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEMPORAL_UI_URL+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-enable-mock]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-enable-mock[`quarkus.temporal.enable-mock`]## [.description] -- -enable mock for testing +enable mock for testing. + +If enabled, the Temporal devservice will not be started. ifdef::add-copy-button-to-env-var[] @@ -701,7 +773,7 @@ a| [[quarkus-temporal_quarkus-temporal-connection-target]] [.property-path]##lin [.description] -- -Sets a target string, which can be either a valid `NameResolver`-compliant URI, or an authority string. See `ManagedChannelBuilder++#++forTarget(String)` for more information about parameter format. Default is 127.0.0.1:7233 +Sets a target string, which can be either a valid `NameResolver`-compliant URI, or an authority string. See `ManagedChannelBuilder++#++forTarget(String)` for more information about parameter format. ifdef::add-copy-button-to-env-var[] @@ -712,7 +784,7 @@ Environment variable: `+++QUARKUS_TEMPORAL_CONNECTION_TARGET+++` endif::add-copy-button-to-env-var[] -- |string -|`127.0.0.1:7233` +|required icon:exclamation-circle[title=Configuration property is required] a| [[quarkus-temporal_quarkus-temporal-connection-enable-https]] [.property-path]##link:#quarkus-temporal_quarkus-temporal-connection-enable-https[`quarkus.temporal.connection.enable-https`]## diff --git a/extension/deployment/pom.xml b/extension/deployment/pom.xml index d26646c..f52ae4f 100644 --- a/extension/deployment/pom.xml +++ b/extension/deployment/pom.xml @@ -19,15 +19,29 @@ io.quarkus - quarkus-grpc-common-deployment + quarkus-grpc-deployment io.quarkus - quarkus-grpc-deployment + quarkus-smallrye-health-spi io.quarkus - quarkus-smallrye-health-spi + quarkus-devservices-deployment + + + io.quarkus + quarkus-vertx-http-deployment + + + org.testcontainers + testcontainers + + + junit + junit + + io.quarkiverse.temporal diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/TemporalProcessor.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/TemporalProcessor.java index 6e585b4..5cd94d3 100644 --- a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/TemporalProcessor.java +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/TemporalProcessor.java @@ -92,7 +92,8 @@ public class TemporalProcessor { public static final DotName ACTIVITY_INTERFACE = DotName.createSimple(ActivityInterface.class); - private static final String FEATURE = "temporal"; + public static final String FEATURE = "temporal"; + public static final DotName CONTEXT_PROPAGATOR = DotName.createSimple(ContextPropagator.class); @BuildStep FeatureBuildItem feature() { diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalContainer.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalContainer.java new file mode 100644 index 0000000..04fef8d --- /dev/null +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalContainer.java @@ -0,0 +1,39 @@ +package io.quarkiverse.temporal.deployment.devui; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +public class TemporalContainer extends GenericContainer { + + private static final Integer SERVER_EXPOSED_PORT = 7233; + private static final Integer UI_EXPOSED_PORT = 8233; + private final String path; + + public TemporalContainer(DockerImageName dockerImageName, String path, Boolean reuse) { + super(dockerImageName); + this.path = path; + + withCreateContainerCmdModifier(cmd -> { + cmd.withEntrypoint("/usr/local/bin/temporal"); + cmd.withCmd( + "server", "start-dev", + "--ip", "0.0.0.0", + "--port", SERVER_EXPOSED_PORT.toString(), + "--ui-public-path", path); + }) + .withExposedPorts(SERVER_EXPOSED_PORT, UI_EXPOSED_PORT) + .withReuse(reuse); + } + + public TemporalContainer(String dockerImageName, String path, Boolean reuse) { + this(DockerImageName.parse(dockerImageName), path, reuse); + } + + public String getUiUrl() { + return "http://" + getHost() + ":" + getMappedPort(UI_EXPOSED_PORT) + path; + } + + public String getServerUrl() { + return getHost() + ":" + getMappedPort(SERVER_EXPOSED_PORT); + } +} diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevUIProcessor.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevUIProcessor.java index 7de9377..d52db10 100644 --- a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevUIProcessor.java +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevUIProcessor.java @@ -2,28 +2,44 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; +import io.quarkiverse.temporal.deployment.TemporalProcessor; import io.quarkiverse.temporal.deployment.WorkerBuildItem; import io.quarkiverse.temporal.deployment.WorkflowBuildItem; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devui.spi.page.CardPageBuildItem; import io.quarkus.devui.spi.page.ExternalPageBuilder; import io.quarkus.devui.spi.page.Page; import io.quarkus.devui.spi.page.PageBuilder; import io.quarkus.devui.spi.page.TableDataPageBuilder; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.temporal.client.WorkflowClient; /** * Dev UI card for displaying important details such Temporal version. */ +@BuildSteps(onlyIf = IsDevelopment.class) public class TemporalDevUIProcessor { - @BuildStep(onlyIf = IsDevelopment.class) - void createCard(BuildProducer cardPageBuildItemBuildProducer, List workflows, - List workers) { + @BuildStep + void createCard( + BuildProducer cardPageBuildItemBuildProducer, + List workflows, + List workers, + GlobalDevServicesConfig globalDevServicesConfig, + TemporalUiConfig uiConfig, + TemporalDevserviceConfig temporalDevserviceConfig, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, + LaunchModeBuildItem launchMode, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { final CardPageBuildItem card = new CardPageBuildItem(); final PageBuilder versionPage = Page.externalPageBuilder("Version") @@ -58,11 +74,47 @@ void createCard(BuildProducer cardPageBuildItemBuildProducer, card.addBuildTimeData("workers", workers.stream().map(WorkerBuildTimeData::new).collect(Collectors.toList())); + uiPage(uiConfig.url(), temporalDevserviceConfig, managementInterfaceBuildTimeConfig, launchMode, + nonApplicationRootPathBuildItem, card); + card.setCustomCard("qwc-temporal-card.js"); cardPageBuildItemBuildProducer.produce(card); } + private void uiPage( + Optional configPath, + TemporalDevserviceConfig temporalDevserviceConfig, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, + LaunchModeBuildItem launchMode, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + CardPageBuildItem card) { + var path = configPath; + + // check if the UI url is set in the config or if the devservice is enabled + if (!path.isPresent() && Boolean.TRUE.equals(temporalDevserviceConfig.enabled())) { + var defaultBasePath = nonApplicationRootPathBuildItem.resolveManagementPath( + TemporalProcessor.FEATURE, + managementInterfaceBuildTimeConfig, + launchMode); + + path = Optional.of(defaultBasePath); + } + + // if the path is not set, we don't have a UI to link to + if (!path.isPresent()) { + return; + } + + // add the UI page + final PageBuilder uiPage = Page.externalPageBuilder("UI") + .icon("font-awesome-solid:desktop") + .url(path.get(), path.get()) + .isHtmlContent(); + + card.addPage(uiPage); + } + static class WorkflowBuildTimeData { WorkflowBuildTimeData(WorkflowBuildItem item) { this.name = item.workflow.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1"); @@ -88,7 +140,7 @@ static class WorkerBuildTimeData { this.workflows = item.workflows.stream().map(workflow -> workflow.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1")) .collect(Collectors.toList()); this.activities = item.activities.stream() - .map(activities -> activities.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1")).collect(Collectors.toList()); + .map(activity -> activity.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1")).collect(Collectors.toList()); } private final String name; diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceConfig.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceConfig.java new file mode 100644 index 0000000..df001d0 --- /dev/null +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceConfig.java @@ -0,0 +1,33 @@ +package io.quarkiverse.temporal.deployment.devui; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "quarkus.temporal.devservice") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface TemporalDevserviceConfig { + + /** + * Enable the Temporal Devservice. + */ + @WithDefault("true") + Boolean enabled(); + + /** + * The image to use for the Temporal Devservice. + * + *

+ * Minimum supported version: temporalio/auto-setup:1.24.3.0 + */ + @WithDefault("temporalio/auto-setup") + String image(); + + /** + * Whether to reuse the Temporal Devservice. + */ + @WithDefault("true") + boolean reuse(); + +} diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceProcessor.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceProcessor.java new file mode 100644 index 0000000..d1d0c98 --- /dev/null +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceProcessor.java @@ -0,0 +1,77 @@ +package io.quarkiverse.temporal.deployment.devui; + +import java.util.Map; + +import io.quarkiverse.temporal.deployment.TemporalProcessor; +import io.quarkiverse.temporal.devui.TemporalUiProxy; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.DevServicesResultBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; + +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) +public class TemporalDevserviceProcessor { + + @BuildStep + DevServicesResultBuildItem start(TemporalDevserviceConfig config, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + LaunchModeBuildItem launchMode, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { + if (Boolean.FALSE.equals(config.enabled())) { + return null; + } + + String path = nonApplicationRootPathBuildItem.resolveManagementPath( + TemporalProcessor.FEATURE, + managementInterfaceBuildTimeConfig, + launchMode); + + TemporalContainer container = new TemporalContainer(config.image(), path, config.reuse()); + container.start(); + + Map configOverrides = Map.of( + "quarkus.temporal.connection.target", container.getServerUrl(), + "quarkus.temporal.ui.url", container.getUiUrl(), + "quarkus.otel.instrument.grpc", "false", + "quarkus.temporal.telemetry.enabled", "false", + "quarkus.grpc.server.use-separate-server", "false", + "quarkus.otel.instrument.vertx-http", "false"); + + return new DevServicesResultBuildItem.RunningDevService( + TemporalProcessor.FEATURE, + container.getContainerId(), + container::close, + configOverrides) + .toBuildItem(); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void registerWebProxy( + TemporalDevserviceConfig config, + TemporalUiProxy proxy, + BuildProducer routes, + NonApplicationRootPathBuildItem frameworkRoot, + CoreVertxBuildItem coreVertxBuildItem) { + if (Boolean.FALSE.equals(config.enabled())) { + return; + } + + routes.produce(frameworkRoot.routeBuilder() + .management() + .route(TemporalProcessor.FEATURE + "/*") + .displayOnNotFoundPage("Portal UI not found") + .handler(proxy.handler(coreVertxBuildItem.getVertx())) + .build()); + } + +} diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalUiConfig.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalUiConfig.java new file mode 100644 index 0000000..4d37941 --- /dev/null +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalUiConfig.java @@ -0,0 +1,18 @@ +package io.quarkiverse.temporal.deployment.devui; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; + +@ConfigMapping(prefix = "quarkus.temporal.ui") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface TemporalUiConfig { + + /** + * The url of the Temporal UI. + */ + Optional url(); + +} diff --git a/extension/deployment/src/test/java/io/quarkiverse/temporal/deployment/HealthCheckEnabledTest.java b/extension/deployment/src/test/java/io/quarkiverse/temporal/deployment/HealthCheckEnabledTest.java index d936a63..c55246a 100644 --- a/extension/deployment/src/test/java/io/quarkiverse/temporal/deployment/HealthCheckEnabledTest.java +++ b/extension/deployment/src/test/java/io/quarkiverse/temporal/deployment/HealthCheckEnabledTest.java @@ -19,17 +19,18 @@ public class HealthCheckEnabledTest { .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addAsResource( new StringAsset( - "quarkus.temporal.start-workers: false\n" + "quarkus.temporal.health.enabled: true\n"), + "quarkus.temporal.start-workers: false\n" + + "quarkus.temporal.health.enabled: true\n"), "application.properties")); @Test - public void testDataSourceHealthCheckExclusion() { + public void testDataSourceHealthCheck() { RestAssured.when().get("/q/health/ready") .then() - .body("status", equalTo("DOWN")) // Verifies that the status at the root level is "DOWN" + .body("status", equalTo("UP")) // Verifies that the status at the root level is "DOWN" .body("checks", hasSize(1)) // Verifies that there is exactly one check in the "checks" array .body("checks[0].name", equalTo("Temporal")) // Verifies that the name of the first check is "Temporal" - .body("checks[0].status", equalTo("DOWN")) // Verifies that the status of the first check is "DOWN" - .body("checks[0].data.'127.0.0.1:7233'", equalTo("DOWN")); + .body("checks[0].status", equalTo("UP")) // Verifies that the status of the first check is "DOWN" + ; } } \ No newline at end of file diff --git a/extension/runtime/pom.xml b/extension/runtime/pom.xml index 1101b4c..1a8269f 100644 --- a/extension/runtime/pom.xml +++ b/extension/runtime/pom.xml @@ -63,6 +63,14 @@ io.opentelemetry opentelemetry-opentracing-shim + + io.quarkus + quarkus-vertx-http + + + io.vertx + vertx-web-client + diff --git a/extension/runtime/src/main/java/io/quarkiverse/temporal/config/ConnectionRuntimeConfig.java b/extension/runtime/src/main/java/io/quarkiverse/temporal/config/ConnectionRuntimeConfig.java index 07282aa..eb93cb9 100644 --- a/extension/runtime/src/main/java/io/quarkiverse/temporal/config/ConnectionRuntimeConfig.java +++ b/extension/runtime/src/main/java/io/quarkiverse/temporal/config/ConnectionRuntimeConfig.java @@ -11,9 +11,8 @@ public interface ConnectionRuntimeConfig { /** * Sets a target string, which can be either a valid {@link NameResolver}-compliant URI, or an * authority string. See {@link ManagedChannelBuilder#forTarget(String)} for more information - * about parameter format. Default is 127.0.0.1:7233 + * about parameter format. */ - @WithDefault("127.0.0.1:7233") String target(); /** diff --git a/extension/runtime/src/main/java/io/quarkiverse/temporal/config/TemporalBuildtimeConfig.java b/extension/runtime/src/main/java/io/quarkiverse/temporal/config/TemporalBuildtimeConfig.java index 7b01a40..b538059 100644 --- a/extension/runtime/src/main/java/io/quarkiverse/temporal/config/TemporalBuildtimeConfig.java +++ b/extension/runtime/src/main/java/io/quarkiverse/temporal/config/TemporalBuildtimeConfig.java @@ -21,7 +21,10 @@ enum ChannelType { } /** - * enable mock for testing + * enable mock for testing. + * + *

+ * If enabled, the Temporal devservice will not be started. */ @WithDefault("false") Boolean enableMock(); diff --git a/extension/runtime/src/main/java/io/quarkiverse/temporal/devui/TemporalUiProxy.java b/extension/runtime/src/main/java/io/quarkiverse/temporal/devui/TemporalUiProxy.java new file mode 100644 index 0000000..3b49ed1 --- /dev/null +++ b/extension/runtime/src/main/java/io/quarkiverse/temporal/devui/TemporalUiProxy.java @@ -0,0 +1,74 @@ +package io.quarkiverse.temporal.devui; + +import java.net.URL; +import java.util.Optional; +import java.util.function.Supplier; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; + +import io.quarkus.runtime.annotations.Recorder; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.client.HttpRequest; +import io.vertx.ext.web.client.WebClient; + +@Recorder +public class TemporalUiProxy { + + private static final Logger log = Logger.getLogger(TemporalUiProxy.class); + + public Handler handler(Supplier vertx) { + final Optional urlOptional = ConfigProvider.getConfig().getOptionalValue("quarkus.temporal.ui.url", + String.class); + final var client = WebClient.create(vertx.get()); + + return new Handler() { + @Override + public void handle(RoutingContext event) { + if (!urlOptional.isPresent()) { + event.response().setStatusCode(404).end(); + return; + } + + URL url; + try { + url = new URL(urlOptional.get()); + } catch (Exception e) { + log.error("Invalid URL: " + urlOptional.get(), e); + event.response().setStatusCode(500).end(); + return; + } + + final HttpRequest r = client.request(event.request().method(), url.getPort(), url.getHost(), + event.request().uri()); + + // copy all headers + event.request().headers().forEach(h -> r.putHeader(h.getKey(), h.getValue())); + + if ("websocket".equals(event.request().getHeader("upgrade"))) { + // handle WebSocket request + event.request().toWebSocket().onComplete(ws -> { + if (ws.succeeded()) { + event.request().resume(); + ws.result().handler(buff -> { + event.response().write(buff); + }); + } else { + log.error("WebSocket failed", ws.cause()); + } + }); + } else { + // handle normal request + r.sendBuffer(event.body().buffer()).andThen(resp -> { + event.response().setStatusCode(resp.result().statusCode()); + resp.result().headers().forEach(h -> event.response().putHeader(h.getKey(), h.getValue())); + event.response().end(resp.result().bodyAsBuffer()); + }); + } + } + }; + } +} diff --git a/integration-tests/devservice/pom.xml b/integration-tests/devservice/pom.xml new file mode 100644 index 0000000..ab3328f --- /dev/null +++ b/integration-tests/devservice/pom.xml @@ -0,0 +1,141 @@ + + + 4.0.0 + + io.quarkiverse.temporal + quarkus-temporal-parent + 999-SNAPSHOT + ../../pom.xml + + quarkus-temporal-integration-tests-devservice + Temporal - Integration Tests - Devservice + + true + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-grpc + + + io.quarkus + quarkus-info + + + io.quarkus + quarkus-resteasy-jackson + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-smallrye-health + + + io.quarkus + quarkus-opentelemetry + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + io.quarkiverse.temporal + quarkus-temporal + ${project.version} + + + org.apache.commons + commons-lang3 + + + io.quarkiverse.temporal + quarkus-temporal-deployment + ${project.version} + + + * + * + + + + + io.quarkus + quarkus-devservices-deployment + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + + + false + true + + + + \ No newline at end of file diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/CdiBean.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/CdiBean.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/CdiBean.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/CdiBean.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIActivityImpl.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIActivityImpl.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIActivityImpl.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIActivityImpl.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIWorkflowImpl.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIWorkflowImpl.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIWorkflowImpl.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIWorkflowImpl.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIActivityImpl.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIActivityImpl.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIActivityImpl.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIActivityImpl.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIWorkflowImpl.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIWorkflowImpl.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIWorkflowImpl.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIWorkflowImpl.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIActivity.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIActivity.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIActivity.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIActivity.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIWorkflow.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIWorkflow.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIWorkflow.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIWorkflow.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/AccountActivityImpl.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/AccountActivityImpl.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/AccountActivityImpl.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/AccountActivityImpl.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/CoreTransactionDetails.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/CoreTransactionDetails.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/CoreTransactionDetails.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/CoreTransactionDetails.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/MoneyTransferWorkflowImpl.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/MoneyTransferWorkflowImpl.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/MoneyTransferWorkflowImpl.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/MoneyTransferWorkflowImpl.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/AccountActivityImpl.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/AccountActivityImpl.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/AccountActivityImpl.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/AccountActivityImpl.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/CoreTransactionDetails.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/CoreTransactionDetails.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/CoreTransactionDetails.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/CoreTransactionDetails.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/MoneyTransferWorkflowImpl.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/MoneyTransferWorkflowImpl.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/MoneyTransferWorkflowImpl.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/MoneyTransferWorkflowImpl.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/AccountActivity.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/AccountActivity.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/AccountActivity.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/AccountActivity.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/MoneyTransferWorkflow.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/MoneyTransferWorkflow.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/MoneyTransferWorkflow.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/MoneyTransferWorkflow.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/TransactionDetails.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/TransactionDetails.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/TransactionDetails.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/TransactionDetails.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/rest/MoneyTransferResource.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/rest/MoneyTransferResource.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/rest/MoneyTransferResource.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/rest/MoneyTransferResource.java diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/util/MDCContextPropagator.java b/integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/util/MDCContextPropagator.java similarity index 100% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/util/MDCContextPropagator.java rename to integration-tests/devservice/src/main/java/io/quarkiverse/temporal/it/util/MDCContextPropagator.java diff --git a/integration-tests/devservice/src/main/resources/application.properties b/integration-tests/devservice/src/main/resources/application.properties new file mode 100644 index 0000000..e69de29 diff --git a/integration-tests/src/test/java/io/quarkiverse/temporal/it/CDIActivityIT.java b/integration-tests/devservice/src/test/java/io/quarkiverse/temporal/it/CDIActivityIT.java similarity index 100% rename from integration-tests/src/test/java/io/quarkiverse/temporal/it/CDIActivityIT.java rename to integration-tests/devservice/src/test/java/io/quarkiverse/temporal/it/CDIActivityIT.java diff --git a/integration-tests/src/test/java/io/quarkiverse/temporal/it/MoneyTransferIT.java b/integration-tests/devservice/src/test/java/io/quarkiverse/temporal/it/MoneyTransferIT.java similarity index 100% rename from integration-tests/src/test/java/io/quarkiverse/temporal/it/MoneyTransferIT.java rename to integration-tests/devservice/src/test/java/io/quarkiverse/temporal/it/MoneyTransferIT.java diff --git a/integration-tests/mock/pom.xml b/integration-tests/mock/pom.xml new file mode 100644 index 0000000..3bfa0a3 --- /dev/null +++ b/integration-tests/mock/pom.xml @@ -0,0 +1,143 @@ + + + 4.0.0 + + io.quarkiverse.temporal + quarkus-temporal-parent + 999-SNAPSHOT + ../../pom.xml + + quarkus-temporal-integration-tests-mock + Temporal - Integration Tests - Mock + + true + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-grpc + + + io.quarkus + quarkus-info + + + io.quarkus + quarkus-resteasy-jackson + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-smallrye-health + + + io.quarkus + quarkus-opentelemetry + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + io.quarkiverse.temporal + quarkus-temporal + ${project.version} + + + io.quarkiverse.temporal + quarkus-temporal-test + ${project.version} + test + + + io.quarkiverse.temporal + quarkus-temporal-test-deployment + ${project.version} + + + io.quarkiverse.temporal + quarkus-temporal-deployment + ${project.version} + + + * + * + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + + + false + true + + + + \ No newline at end of file diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/CdiBean.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/CdiBean.java new file mode 100644 index 0000000..1740109 --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/CdiBean.java @@ -0,0 +1,12 @@ +package io.quarkiverse.temporal.it.cdi; + +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class CdiBean { + + public void someMethod() { + System.out.println("Calling CDI Bean from Activity"); + System.out.flush(); + } +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIActivityImpl.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIActivityImpl.java new file mode 100644 index 0000000..6e454a6 --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIActivityImpl.java @@ -0,0 +1,18 @@ +package io.quarkiverse.temporal.it.cdi.defaultWorker; + +import jakarta.inject.Inject; + +import io.quarkiverse.temporal.it.cdi.CdiBean; +import io.quarkiverse.temporal.it.cdi.shared.CDIActivity; + +public class CDIActivityImpl implements CDIActivity { + + @Inject + CdiBean cdiBean; + + @Override + public void cdi() { + cdiBean.someMethod(); + + } +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIWorkflowImpl.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIWorkflowImpl.java new file mode 100644 index 0000000..e8b8e68 --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/defaultWorker/CDIWorkflowImpl.java @@ -0,0 +1,36 @@ +package io.quarkiverse.temporal.it.cdi.defaultWorker; + +import java.time.Duration; + +import io.quarkiverse.temporal.it.cdi.shared.CDIActivity; +import io.quarkiverse.temporal.it.cdi.shared.CDIWorkflow; +import io.temporal.activity.ActivityOptions; +import io.temporal.common.RetryOptions; +import io.temporal.workflow.Workflow; + +public class CDIWorkflowImpl implements CDIWorkflow { + + // RetryOptions specify how to automatically handle retries when Activities fail + private final RetryOptions retryoptions = RetryOptions.newBuilder() + .setInitialInterval(Duration.ofSeconds(1)) // Wait 1 second before first retry + .setMaximumInterval(Duration.ofSeconds(20)) // Do not exceed 20 seconds between retries + .setBackoffCoefficient(2) // Wait 1 second, then 2, then 4, etc + .setMaximumAttempts(5000) // Fail after 5000 attempts + .build(); + + // ActivityOptions specify the limits on how long an Activity can execute before + // being interrupted by the Orchestration service + private final ActivityOptions defaultActivityOptions = ActivityOptions.newBuilder() + .setRetryOptions(retryoptions) // Apply the RetryOptions defined above + .setStartToCloseTimeout(Duration.ofSeconds(2)) // Max execution time for single Activity + .setScheduleToCloseTimeout(Duration.ofSeconds(5000)) // Entire duration from scheduling to completion including queue time + .build(); + + // ActivityStubs enable calls to methods as if the Activity object is local but actually perform an RPC invocation + private final CDIActivity cdiActivity = Workflow.newActivityStub(CDIActivity.class, defaultActivityOptions); + + @Override + public void cdi() { + cdiActivity.cdi(); + } +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIActivityImpl.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIActivityImpl.java new file mode 100644 index 0000000..99fc763 --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIActivityImpl.java @@ -0,0 +1,20 @@ +package io.quarkiverse.temporal.it.cdi.namedWorker; + +import jakarta.inject.Inject; + +import io.quarkiverse.temporal.TemporalActivity; +import io.quarkiverse.temporal.it.cdi.CdiBean; +import io.quarkiverse.temporal.it.cdi.shared.CDIActivity; + +@TemporalActivity(workers = "namedWorker") +public class CDIActivityImpl implements CDIActivity { + + @Inject + CdiBean cdiBean; + + @Override + public void cdi() { + cdiBean.someMethod(); + + } +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIWorkflowImpl.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIWorkflowImpl.java new file mode 100644 index 0000000..8a9d85e --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/namedWorker/CDIWorkflowImpl.java @@ -0,0 +1,38 @@ +package io.quarkiverse.temporal.it.cdi.namedWorker; + +import java.time.Duration; + +import io.quarkiverse.temporal.TemporalWorkflow; +import io.quarkiverse.temporal.it.cdi.shared.CDIActivity; +import io.quarkiverse.temporal.it.cdi.shared.CDIWorkflow; +import io.temporal.activity.ActivityOptions; +import io.temporal.common.RetryOptions; +import io.temporal.workflow.Workflow; + +@TemporalWorkflow(workers = "namedWorker") +public class CDIWorkflowImpl implements CDIWorkflow { + + // RetryOptions specify how to automatically handle retries when Activities fail + private final RetryOptions retryoptions = RetryOptions.newBuilder() + .setInitialInterval(Duration.ofSeconds(1)) // Wait 1 second before first retry + .setMaximumInterval(Duration.ofSeconds(20)) // Do not exceed 20 seconds between retries + .setBackoffCoefficient(2) // Wait 1 second, then 2, then 4, etc + .setMaximumAttempts(5000) // Fail after 5000 attempts + .build(); + + // ActivityOptions specify the limits on how long an Activity can execute before + // being interrupted by the Orchestration service + private final ActivityOptions defaultActivityOptions = ActivityOptions.newBuilder() + .setRetryOptions(retryoptions) // Apply the RetryOptions defined above + .setStartToCloseTimeout(Duration.ofSeconds(2)) // Max execution time for single Activity + .setScheduleToCloseTimeout(Duration.ofSeconds(5000)) // Entire duration from scheduling to completion including queue time + .build(); + + // ActivityStubs enable calls to methods as if the Activity object is local but actually perform an RPC invocation + private final CDIActivity cdiActivity = Workflow.newActivityStub(CDIActivity.class, defaultActivityOptions); + + @Override + public void cdi() { + cdiActivity.cdi(); + } +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIActivity.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIActivity.java new file mode 100644 index 0000000..5e7fbba --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIActivity.java @@ -0,0 +1,12 @@ +package io.quarkiverse.temporal.it.cdi.shared; + +import io.temporal.activity.ActivityInterface; +import io.temporal.activity.ActivityMethod; + +@ActivityInterface +public interface CDIActivity { + + @ActivityMethod + void cdi(); + +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIWorkflow.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIWorkflow.java new file mode 100644 index 0000000..e8cf8bd --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/cdi/shared/CDIWorkflow.java @@ -0,0 +1,11 @@ +package io.quarkiverse.temporal.it.cdi.shared; + +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; + +@WorkflowInterface +public interface CDIWorkflow { + + @WorkflowMethod + void cdi(); +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/AccountActivityImpl.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/AccountActivityImpl.java new file mode 100644 index 0000000..4bf40e3 --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/AccountActivityImpl.java @@ -0,0 +1,43 @@ +package io.quarkiverse.temporal.it.moneyTransfer.defaultWorker; + +import io.quarkiverse.temporal.it.moneyTransfer.shared.AccountActivity; +import io.temporal.activity.Activity; + +public class AccountActivityImpl implements AccountActivity { + + // Mock up the withdrawal of an amount of money from the source account + @Override + public void withdraw(String accountId, String referenceId, int amount) { + System.out.printf("\nWithdrawing $%d from account %s.\n[ReferenceId: %s]\n", amount, accountId, referenceId); + System.out.flush(); + } + + // Mock up the deposit of an amount of money from the destination account + @Override + public void deposit(String accountId, String referenceId, int amount) { + boolean activityShouldSucceed = true; + if (!activityShouldSucceed) { + System.out.println("Deposit failed"); + System.out.flush(); + throw Activity.wrap(new RuntimeException("Simulated Activity error during deposit of funds")); + } + + System.out.printf("\nDepositing $%d into account %s.\n[ReferenceId: %s]\n", amount, accountId, referenceId); + System.out.flush(); + } + + // Mock up a compensation refund to the source account + @Override + public void refund(String accountId, String referenceId, int amount) { + boolean activityShouldSucceed = true; + + if (!activityShouldSucceed) { + System.out.println("Refund failed"); + System.out.flush(); + throw Activity.wrap(new RuntimeException("Simulated Activity error during refund to source account")); + } + + System.out.printf("\nRefunding $%d to account %s.\n[ReferenceId: %s]\n", amount, accountId, referenceId); + System.out.flush(); + } +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/CoreTransactionDetails.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/CoreTransactionDetails.java new file mode 100644 index 0000000..2fbacc4 --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/CoreTransactionDetails.java @@ -0,0 +1,43 @@ +package io.quarkiverse.temporal.it.moneyTransfer.defaultWorker; + +import io.quarkiverse.temporal.it.moneyTransfer.shared.TransactionDetails; + +public class CoreTransactionDetails implements TransactionDetails { + + private String sourceAccountId; + private String destinationAccountId; + private String transactionReferenceId; + private int amountToTransfer; + + public CoreTransactionDetails() { + // Default constructor is needed for Jackson deserialization + } + + public CoreTransactionDetails(String sourceAccountId, + String destinationAccountId, + String transactionReferenceId, + int amountToTransfer) { + this.sourceAccountId = sourceAccountId; + this.destinationAccountId = destinationAccountId; + this.transactionReferenceId = transactionReferenceId; + this.amountToTransfer = amountToTransfer; + } + + // MARK: Getter methods + + public String getSourceAccountId() { + return sourceAccountId; + } + + public String getDestinationAccountId() { + return destinationAccountId; + } + + public String getTransactionReferenceId() { + return transactionReferenceId; + } + + public int getAmountToTransfer() { + return amountToTransfer; + } +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/MoneyTransferWorkflowImpl.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/MoneyTransferWorkflowImpl.java new file mode 100644 index 0000000..c849795 --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/defaultWorker/MoneyTransferWorkflowImpl.java @@ -0,0 +1,115 @@ +package io.quarkiverse.temporal.it.moneyTransfer.defaultWorker; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import io.quarkiverse.temporal.it.moneyTransfer.shared.AccountActivity; +import io.quarkiverse.temporal.it.moneyTransfer.shared.MoneyTransferWorkflow; +import io.quarkiverse.temporal.it.moneyTransfer.shared.TransactionDetails; +import io.temporal.activity.ActivityOptions; +import io.temporal.common.RetryOptions; +import io.temporal.workflow.Workflow; + +public class MoneyTransferWorkflowImpl implements MoneyTransferWorkflow { + private static final String WITHDRAW = "Withdraw"; + + // RetryOptions specify how to automatically handle retries when Activities fail + private final RetryOptions retryoptions = RetryOptions.newBuilder() + .setInitialInterval(Duration.ofSeconds(1)) // Wait 1 second before first retry + .setMaximumInterval(Duration.ofSeconds(20)) // Do not exceed 20 seconds between retries + .setBackoffCoefficient(2) // Wait 1 second, then 2, then 4, etc + .setMaximumAttempts(5000) // Fail after 5000 attempts + .build(); + + // ActivityOptions specify the limits on how long an Activity can execute before + // being interrupted by the Orchestration service + private final ActivityOptions defaultActivityOptions = ActivityOptions.newBuilder() + .setRetryOptions(retryoptions) // Apply the RetryOptions defined above + .setStartToCloseTimeout(Duration.ofSeconds(2)) // Max execution time for single Activity + .setScheduleToCloseTimeout(Duration.ofSeconds(5000)) // Entire duration from scheduling to completion including queue time + .build(); + + private final Map perActivityMethodOptions = new HashMap() { + { + // A heartbeat time-out is a proof-of life indicator that an activity is still working. + // The 5 second duration used here waits for up to 5 seconds to hear a heartbeat. + // If one is not heard, the Activity fails. + // The `withdraw` method is hard-coded to succeed, so this never happens. + // Use heartbeats for long-lived event-driven applications. + put(WITHDRAW, ActivityOptions.newBuilder().setHeartbeatTimeout(Duration.ofSeconds(5)).build()); + } + }; + + // ActivityStubs enable calls to methods as if the Activity object is local but actually perform an RPC invocation + private final AccountActivity accountActivityStub = Workflow.newActivityStub(AccountActivity.class, defaultActivityOptions, + perActivityMethodOptions); + + // The transfer method is the entry point to the Workflow + // Activity method executions can be orchestrated here or from within other Activity methods + @Override + public void transfer(TransactionDetails transaction) { + // Retrieve transaction information from the `transaction` instance + String sourceAccountId = transaction.getSourceAccountId(); + String destinationAccountId = transaction.getDestinationAccountId(); + String transactionReferenceId = transaction.getTransactionReferenceId(); + int amountToTransfer = transaction.getAmountToTransfer(); + + // Stage 1: Withdraw funds from source + try { + // Launch `withdrawal` Activity + accountActivityStub.withdraw(sourceAccountId, transactionReferenceId, amountToTransfer); + } catch (Exception e) { + // If the withdrawal fails, for any exception, it's caught here + System.out.printf("[%s] Withdrawal of $%d from account %s failed", transactionReferenceId, amountToTransfer, + sourceAccountId); + System.out.flush(); + + // Transaction ends here + return; + } + + // Stage 2: Deposit funds to destination + try { + // Perform `deposit` Activity + accountActivityStub.deposit(destinationAccountId, transactionReferenceId, amountToTransfer); + + // The `deposit` was successful + System.out.printf("[%s] Transaction succeeded.\n", transactionReferenceId); + System.out.flush(); + + // Transaction ends here + return; + } catch (Exception e) { + // If the deposit fails, for any exception, it's caught here + System.out.printf("[%s] Deposit of $%d to account %s failed.\n", transactionReferenceId, amountToTransfer, + destinationAccountId); + System.out.flush(); + } + + // Continue by compensating with a refund + + try { + // Perform `refund` Activity + System.out.printf("[%s] Refunding $%d to account %s.\n", transactionReferenceId, amountToTransfer, sourceAccountId); + System.out.flush(); + + accountActivityStub.refund(sourceAccountId, transactionReferenceId, amountToTransfer); + + // Recovery successful. Transaction ends here + System.out.printf("[%s] Refund to originating account was successful.\n", transactionReferenceId); + System.out.printf("[%s] Transaction is complete. No transfer made.\n", transactionReferenceId); + return; + } catch (Exception e) { + // A recovery mechanism can fail too. Handle any exception here + System.out.printf("[%s] Deposit of $%d to account %s failed. Did not compensate withdrawal.\n", + transactionReferenceId, amountToTransfer, destinationAccountId); + System.out.printf("[%s] Workflow failed.", transactionReferenceId); + System.out.flush(); + + // Rethrowing the exception causes a Workflow Task failure + throw (e); + } + } + +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/AccountActivityImpl.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/AccountActivityImpl.java new file mode 100644 index 0000000..de7b201 --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/AccountActivityImpl.java @@ -0,0 +1,45 @@ +package io.quarkiverse.temporal.it.moneyTransfer.namedWorker; + +import io.quarkiverse.temporal.TemporalActivity; +import io.quarkiverse.temporal.it.moneyTransfer.shared.AccountActivity; +import io.temporal.activity.Activity; + +@TemporalActivity(workers = "namedWorker") +public class AccountActivityImpl implements AccountActivity { + + // Mock up the withdrawal of an amount of money from the source account + @Override + public void withdraw(String accountId, String referenceId, int amount) { + System.out.printf("\nWithdrawing $%d from account %s.\n[ReferenceId: %s]\n", amount, accountId, referenceId); + System.out.flush(); + } + + // Mock up the deposit of an amount of money from the destination account + @Override + public void deposit(String accountId, String referenceId, int amount) { + boolean activityShouldSucceed = true; + if (!activityShouldSucceed) { + System.out.println("Deposit failed"); + System.out.flush(); + throw Activity.wrap(new RuntimeException("Simulated Activity error during deposit of funds")); + } + + System.out.printf("\nDepositing $%d into account %s.\n[ReferenceId: %s]\n", amount, accountId, referenceId); + System.out.flush(); + } + + // Mock up a compensation refund to the source account + @Override + public void refund(String accountId, String referenceId, int amount) { + boolean activityShouldSucceed = true; + + if (!activityShouldSucceed) { + System.out.println("Refund failed"); + System.out.flush(); + throw Activity.wrap(new RuntimeException("Simulated Activity error during refund to source account")); + } + + System.out.printf("\nRefunding $%d to account %s.\n[ReferenceId: %s]\n", amount, accountId, referenceId); + System.out.flush(); + } +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/CoreTransactionDetails.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/CoreTransactionDetails.java new file mode 100644 index 0000000..e3787aa --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/CoreTransactionDetails.java @@ -0,0 +1,43 @@ +package io.quarkiverse.temporal.it.moneyTransfer.namedWorker; + +import io.quarkiverse.temporal.it.moneyTransfer.shared.TransactionDetails; + +public class CoreTransactionDetails implements TransactionDetails { + + private String sourceAccountId; + private String destinationAccountId; + private String transactionReferenceId; + private int amountToTransfer; + + public CoreTransactionDetails() { + // Default constructor is needed for Jackson deserialization + } + + public CoreTransactionDetails(String sourceAccountId, + String destinationAccountId, + String transactionReferenceId, + int amountToTransfer) { + this.sourceAccountId = sourceAccountId; + this.destinationAccountId = destinationAccountId; + this.transactionReferenceId = transactionReferenceId; + this.amountToTransfer = amountToTransfer; + } + + // MARK: Getter methods + + public String getSourceAccountId() { + return sourceAccountId; + } + + public String getDestinationAccountId() { + return destinationAccountId; + } + + public String getTransactionReferenceId() { + return transactionReferenceId; + } + + public int getAmountToTransfer() { + return amountToTransfer; + } +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/MoneyTransferWorkflowImpl.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/MoneyTransferWorkflowImpl.java new file mode 100644 index 0000000..e77f0cc --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/namedWorker/MoneyTransferWorkflowImpl.java @@ -0,0 +1,116 @@ +package io.quarkiverse.temporal.it.moneyTransfer.namedWorker; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import io.quarkiverse.temporal.TemporalWorkflow; +import io.quarkiverse.temporal.it.moneyTransfer.shared.AccountActivity; +import io.quarkiverse.temporal.it.moneyTransfer.shared.MoneyTransferWorkflow; +import io.quarkiverse.temporal.it.moneyTransfer.shared.TransactionDetails; +import io.temporal.activity.ActivityOptions; +import io.temporal.common.RetryOptions; +import io.temporal.workflow.Workflow; + +@TemporalWorkflow(workers = "namedWorker") +public class MoneyTransferWorkflowImpl implements MoneyTransferWorkflow { + private static final String WITHDRAW = "Withdraw"; + + // RetryOptions specify how to automatically handle retries when Activities fail + private final RetryOptions retryoptions = RetryOptions.newBuilder() + .setInitialInterval(Duration.ofSeconds(1)) // Wait 1 second before first retry + .setMaximumInterval(Duration.ofSeconds(20)) // Do not exceed 20 seconds between retries + .setBackoffCoefficient(2) // Wait 1 second, then 2, then 4, etc + .setMaximumAttempts(5000) // Fail after 5000 attempts + .build(); + + // ActivityOptions specify the limits on how long an Activity can execute before + // being interrupted by the Orchestration service + private final ActivityOptions defaultActivityOptions = ActivityOptions.newBuilder() + .setRetryOptions(retryoptions) // Apply the RetryOptions defined above + .setStartToCloseTimeout(Duration.ofSeconds(2)) // Max execution time for single Activity + .setScheduleToCloseTimeout(Duration.ofSeconds(5000)) // Entire duration from scheduling to completion including queue time + .build(); + + private final Map perActivityMethodOptions = new HashMap() { + { + // A heartbeat time-out is a proof-of life indicator that an activity is still working. + // The 5 second duration used here waits for up to 5 seconds to hear a heartbeat. + // If one is not heard, the Activity fails. + // The `withdraw` method is hard-coded to succeed, so this never happens. + // Use heartbeats for long-lived event-driven applications. + put(WITHDRAW, ActivityOptions.newBuilder().setHeartbeatTimeout(Duration.ofSeconds(5)).build()); + } + }; + + // ActivityStubs enable calls to methods as if the Activity object is local but actually perform an RPC invocation + private final AccountActivity accountActivityStub = Workflow.newActivityStub(AccountActivity.class, defaultActivityOptions, + perActivityMethodOptions); + + // The transfer method is the entry point to the Workflow + // Activity method executions can be orchestrated here or from within other Activity methods + @Override + public void transfer(TransactionDetails transaction) { + // Retrieve transaction information from the `transaction` instance + String sourceAccountId = transaction.getSourceAccountId(); + String destinationAccountId = transaction.getDestinationAccountId(); + String transactionReferenceId = transaction.getTransactionReferenceId(); + int amountToTransfer = transaction.getAmountToTransfer(); + + // Stage 1: Withdraw funds from source + try { + // Launch `withdrawal` Activity + accountActivityStub.withdraw(sourceAccountId, transactionReferenceId, amountToTransfer); + } catch (Exception e) { + // If the withdrawal fails, for any exception, it's caught here + System.out.printf("[%s] Withdrawal of $%d from account %s failed", transactionReferenceId, amountToTransfer, + sourceAccountId); + System.out.flush(); + + // Transaction ends here + return; + } + + // Stage 2: Deposit funds to destination + try { + // Perform `deposit` Activity + accountActivityStub.deposit(destinationAccountId, transactionReferenceId, amountToTransfer); + + // The `deposit` was successful + System.out.printf("[%s] Transaction succeeded.\n", transactionReferenceId); + System.out.flush(); + + // Transaction ends here + return; + } catch (Exception e) { + // If the deposit fails, for any exception, it's caught here + System.out.printf("[%s] Deposit of $%d to account %s failed.\n", transactionReferenceId, amountToTransfer, + destinationAccountId); + System.out.flush(); + } + + // Continue by compensating with a refund + + try { + // Perform `refund` Activity + System.out.printf("[%s] Refunding $%d to account %s.\n", transactionReferenceId, amountToTransfer, sourceAccountId); + System.out.flush(); + + accountActivityStub.refund(sourceAccountId, transactionReferenceId, amountToTransfer); + + // Recovery successful. Transaction ends here + System.out.printf("[%s] Refund to originating account was successful.\n", transactionReferenceId); + System.out.printf("[%s] Transaction is complete. No transfer made.\n", transactionReferenceId); + return; + } catch (Exception e) { + // A recovery mechanism can fail too. Handle any exception here + System.out.printf("[%s] Deposit of $%d to account %s failed. Did not compensate withdrawal.\n", + transactionReferenceId, amountToTransfer, destinationAccountId); + System.out.printf("[%s] Workflow failed.", transactionReferenceId); + System.out.flush(); + + // Rethrowing the exception causes a Workflow Task failure + throw (e); + } + } +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/AccountActivity.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/AccountActivity.java new file mode 100644 index 0000000..aa9892e --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/AccountActivity.java @@ -0,0 +1,20 @@ +package io.quarkiverse.temporal.it.moneyTransfer.shared; + +import io.temporal.activity.ActivityInterface; +import io.temporal.activity.ActivityMethod; + +@ActivityInterface +public interface AccountActivity { + // Withdraw an amount of money from the source account + @ActivityMethod + void withdraw(String accountId, String referenceId, int amount); + + // Deposit an amount of money into the destination account + @ActivityMethod + void deposit(String accountId, String referenceId, int amount); + + // Compensate a failed deposit by refunding to the original account + @ActivityMethod + void refund(String accountId, String referenceId, int amount); + +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/MoneyTransferWorkflow.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/MoneyTransferWorkflow.java new file mode 100644 index 0000000..fbb6a87 --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/MoneyTransferWorkflow.java @@ -0,0 +1,13 @@ +package io.quarkiverse.temporal.it.moneyTransfer.shared; + +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; + +@WorkflowInterface +public interface MoneyTransferWorkflow { + // The Workflow Execution that starts this method can be initiated from code or + // from the 'temporal' CLI utility. + @WorkflowMethod + void transfer(TransactionDetails transaction); + +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/TransactionDetails.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/TransactionDetails.java new file mode 100644 index 0000000..8e53048 --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/moneyTransfer/shared/TransactionDetails.java @@ -0,0 +1,16 @@ +package io.quarkiverse.temporal.it.moneyTransfer.shared; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import io.quarkiverse.temporal.it.moneyTransfer.defaultWorker.CoreTransactionDetails; + +@JsonDeserialize(as = CoreTransactionDetails.class) +public interface TransactionDetails { + String getSourceAccountId(); + + String getDestinationAccountId(); + + String getTransactionReferenceId(); + + int getAmountToTransfer(); +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/rest/MoneyTransferResource.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/rest/MoneyTransferResource.java new file mode 100644 index 0000000..43d5a13 --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/rest/MoneyTransferResource.java @@ -0,0 +1,38 @@ +package io.quarkiverse.temporal.it.rest; + +import static io.quarkiverse.temporal.Constants.DEFAULT_WORKER_NAME; + +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import io.quarkiverse.temporal.it.moneyTransfer.defaultWorker.CoreTransactionDetails; +import io.quarkiverse.temporal.it.moneyTransfer.shared.MoneyTransferWorkflow; +import io.quarkiverse.temporal.it.moneyTransfer.shared.TransactionDetails; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; + +@Path("/money-transfer") +public class MoneyTransferResource { + + @Inject + WorkflowClient client; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public TransactionDetails transferMoney(@QueryParam("sourceAccountId") String sourceAccountId, + @QueryParam("destinationAccountId") String destinationAccountId, + @QueryParam("transactionReferenceId") String transactionReferenceId, + @QueryParam("amountToTransfer") int amountToTransfer) { + WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue(DEFAULT_WORKER_NAME) + .setWorkflowId("money-transfer-workflow") + .build(); + + MoneyTransferWorkflow workflow = client.newWorkflowStub(MoneyTransferWorkflow.class, options); + TransactionDetails transaction = new CoreTransactionDetails(sourceAccountId, destinationAccountId, + transactionReferenceId, amountToTransfer); + workflow.transfer(transaction); + return transaction; + } +} diff --git a/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/util/MDCContextPropagator.java b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/util/MDCContextPropagator.java new file mode 100644 index 0000000..3ed717f --- /dev/null +++ b/integration-tests/mock/src/main/java/io/quarkiverse/temporal/it/util/MDCContextPropagator.java @@ -0,0 +1,139 @@ +package io.quarkiverse.temporal.it.util; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.inject.Singleton; + +import org.apache.commons.lang3.StringUtils; +import org.jboss.logging.Logger; +import org.slf4j.MDC; + +import com.google.protobuf.ByteString; + +import io.quarkus.arc.Unremovable; +import io.temporal.api.common.v1.Payload; +import io.temporal.common.context.ContextPropagator; +import io.temporal.common.converter.GlobalDataConverter; + +/** + * A {@link ContextPropagator} implementation that propagates the SLF4J MDC + * (Mapped Diagnostic Context) across Temporal workflow and activity boundaries. + * This class ensures that MDC entries with keys starting with "X-" are + * propagated. + */ +@Singleton +@Unremovable +public class MDCContextPropagator implements ContextPropagator { + + private static final Logger LOG = Logger.getLogger(MDCContextPropagator.class); + + /** + * Gets the name of the context propagator. + * + * @return the name of the context propagator, which is the fully qualified + * class name. + */ + @Override + public String getName() { + return this.getClass().getName(); + } + + /** + * Retrieves the current MDC context to be propagated. + * + * @return a map containing the current MDC context, filtered to include only + * entries with keys starting with "X-". + */ + @Override + public Object getCurrentContext() { + Map context = new HashMap<>(); + Map mdcContext = MDC.getCopyOfContextMap(); + if (mdcContext != null) { + mdcContext.entrySet().stream() + .filter(entry -> entry.getKey().startsWith("X-")) + .forEach(entry -> context.put(entry.getKey(), entry.getValue())); + } + return context; + } + + /** + * Sets the current MDC context from the given context map. + * + * @param context the context map containing MDC entries to be set. + */ + @Override + public void setCurrentContext(Object context) { + if (context instanceof Map) { + @SuppressWarnings("unchecked") + Map contextMap = (Map) context; + contextMap.forEach(MDC::put); + } + } + + /** + * Serializes the given context map to a map of Payloads. + * + * @param context the context map containing MDC entries to be serialized. + * @return a map of Payloads representing the serialized context. + */ + @Override + public Map serializeContext(Object context) { + if (!(context instanceof Map)) { + return new HashMap<>(); + } + @SuppressWarnings("unchecked") + Map contextMap = (Map) context; + Map serializedContext = new HashMap<>(); + contextMap.forEach((key, value) -> GlobalDataConverter.get().toPayload(value) + .ifPresent(payload -> serializedContext.put(key, payload))); + return serializedContext; + } + + /** + * Deserializes the given map of Payloads to a context map. + * + * @param context the map of Payloads to be deserialized. + * @return a context map containing the deserialized MDC entries. + */ + @Override + public Object deserializeContext(Map context) { + Map contextMap = new HashMap<>(); + context.forEach((key, payload) -> { + + // Handle empty {} when the data value is empty + // Adding opentracing seems to add a new value with empty data + // and the dataconverter throws an error + // This actually might be a configuration error from earlier + // but leaving in right now + // + // {_tracer-data=metadata { + // key: "encoding" + // value: "json/plain" + // } + // data: "{}" + // } + try { + String payloadValue = StringUtils.EMPTY; // default value + + // Convert data to string to compare + ByteString data = payload.getData(); + + // Check the value to see if it "empty" + if (data != null && !data.isEmpty()) { + + // Check if the value isn't {}'s + if (!StringUtils.equals("{}", data.toStringUtf8())) { + payloadValue = GlobalDataConverter.get().fromPayload(payload, String.class, String.class); + } + } + + // Add the value into the map + contextMap.put(key, payloadValue); + } catch (Exception e) { + LOG.warnf("Couldn't parse MDC Context Data Key %s", key); + } + }); + return contextMap; + } +} \ No newline at end of file diff --git a/integration-tests/mock/src/main/resources/application.properties b/integration-tests/mock/src/main/resources/application.properties new file mode 100644 index 0000000..23c5d45 --- /dev/null +++ b/integration-tests/mock/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.temporal.enable-mock=true +quarkus.temporal.devservice.enabled=false diff --git a/integration-tests/mock/src/test/java/io/quarkiverse/temporal/it/CDIActivityIT.java b/integration-tests/mock/src/test/java/io/quarkiverse/temporal/it/CDIActivityIT.java new file mode 100644 index 0000000..dd533f3 --- /dev/null +++ b/integration-tests/mock/src/test/java/io/quarkiverse/temporal/it/CDIActivityIT.java @@ -0,0 +1,43 @@ +package io.quarkiverse.temporal.it; + +import static io.quarkiverse.temporal.Constants.DEFAULT_WORKER_NAME; + +import java.util.concurrent.TimeoutException; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; + +import io.quarkiverse.temporal.it.cdi.shared.CDIWorkflow; +import io.quarkus.test.junit.QuarkusTest; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; + +@QuarkusTest +public class CDIActivityIT { + + @Inject + WorkflowClient client; + + @Test + public void testCDIInActivityOnDefaultWorker() throws TimeoutException { + WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue(DEFAULT_WORKER_NAME) + .setWorkflowId("cdi-workflow") + .build(); + + CDIWorkflow workflow = client.newWorkflowStub(CDIWorkflow.class, options); + workflow.cdi(); + } + + @Test + public void testCDIInActivityOnNamedWorker() throws TimeoutException { + WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue("namedWorker") + .setWorkflowId("cdi-workflow") + .build(); + + CDIWorkflow workflow = client.newWorkflowStub(CDIWorkflow.class, options); + workflow.cdi(); + } +} diff --git a/integration-tests/mock/src/test/java/io/quarkiverse/temporal/it/MoneyTransferIT.java b/integration-tests/mock/src/test/java/io/quarkiverse/temporal/it/MoneyTransferIT.java new file mode 100644 index 0000000..7c24444 --- /dev/null +++ b/integration-tests/mock/src/test/java/io/quarkiverse/temporal/it/MoneyTransferIT.java @@ -0,0 +1,47 @@ +package io.quarkiverse.temporal.it; + +import static io.quarkiverse.temporal.Constants.DEFAULT_WORKER_NAME; + +import java.util.concurrent.TimeoutException; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; + +import io.quarkiverse.temporal.it.moneyTransfer.defaultWorker.CoreTransactionDetails; +import io.quarkiverse.temporal.it.moneyTransfer.shared.MoneyTransferWorkflow; +import io.quarkiverse.temporal.it.moneyTransfer.shared.TransactionDetails; +import io.quarkus.test.junit.QuarkusTest; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; + +@QuarkusTest +public class MoneyTransferIT { + + @Inject + WorkflowClient client; + + @Test + public void testRunWorkflowOnDefaultWorker() throws TimeoutException { + WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue(DEFAULT_WORKER_NAME) + .setWorkflowId("money-transfer-workflow") + .build(); + + MoneyTransferWorkflow workflow = client.newWorkflowStub(MoneyTransferWorkflow.class, options); + TransactionDetails transaction = new CoreTransactionDetails("249020073", "152354872", "57c65dea-e57e-4a0a", 68); + workflow.transfer(transaction); + } + + @Test + public void testRunWorkflowOnNamedWorker() throws TimeoutException { + WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue("namedWorker") + .setWorkflowId("money-transfer-workflow") + .build(); + + MoneyTransferWorkflow workflow = client.newWorkflowStub(MoneyTransferWorkflow.class, options); + TransactionDetails transaction = new CoreTransactionDetails("249020073", "152354872", "57c65dea-e57e-4a0a", 68); + workflow.transfer(transaction); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index b64186e..4ccd2c5 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -1,146 +1,18 @@ - + 4.0.0 io.quarkiverse.temporal quarkus-temporal-parent 999-SNAPSHOT - integration-tests - Quarkus Temporal - Integration Tests - - true - - - - io.quarkus - quarkus-arc - - - io.quarkus - quarkus-grpc - - - io.quarkus - quarkus-rest - - - io.quarkus - quarkus-rest-jackson - - - io.quarkus - quarkus-smallrye-openapi - - - io.quarkus - quarkus-smallrye-health - - - io.quarkus - quarkus-info - - - io.quarkus - quarkus-opentelemetry - - - io.quarkus - quarkus-micrometer-registry-prometheus - - - io.quarkiverse.temporal - quarkus-temporal - ${project.version} - - - io.quarkiverse.temporal - quarkus-temporal-test - ${project.version} - test - - - io.quarkiverse.temporal - quarkus-temporal-test-deployment - ${project.version} - - - io.quarkiverse.temporal - quarkus-temporal-deployment - ${project.version} - - - * - * - - - - - io.quarkus - quarkus-junit5 - test - - - io.rest-assured - rest-assured - test - - - - - - io.quarkus - quarkus-maven-plugin - - - - build - - - - - - maven-failsafe-plugin - - - - integration-test - verify - - - - - - ${project.build.directory}/${project.build.finalName}-runner - org.jboss.logmanager.LogManager - ${maven.home} - - - - - - - - native-image - - - native - - - - - - maven-surefire-plugin - - ${native.surefire.skip} - - - - - - false - true - - - + quarkus-temporal-extension-integration-tests + Temporal - Integration Tests + pom + + + mock + devservice + + \ No newline at end of file diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties index cdd6159..ba37f2f 100644 --- a/integration-tests/src/main/resources/application.properties +++ b/integration-tests/src/main/resources/application.properties @@ -1,3 +1,3 @@ quarkus.http.port=8081 %test.quarkus.temporal.enable-mock=true -quarkus.grpc.server.use-separate-server=false \ No newline at end of file +quarkus.grpc.server.use-separate-server=false diff --git a/pom.xml b/pom.xml index 8c2183d..d796539 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,6 @@ UTF-8 3.15.1 1.26.0 - 1.32.0