From 4b0d88ca761e11557101e9fe2cbbbdb311d44d8c Mon Sep 17 00:00:00 2001 From: Loic Hermann Date: Sun, 11 Aug 2024 17:34:32 -0400 Subject: [PATCH] add first integration test --- .../ROOT/pages/includes/quarkus-temporal.adoc | 17 --- .../deployment/TemporalProcessor.java | 21 +++- integration-tests/pom.xml | 5 + .../temporal/it/client/TransferApp.java | 77 ------------ .../AccountActivityImpl.java | 2 +- .../defaultWorker/CoreTransactionDetails.java | 43 +++++++ .../MoneyTransferWorkflowImpl.java | 2 +- .../it/namedWorker/AccountActivityImpl.java | 45 +++++++ .../CoreTransactionDetails.java | 2 +- .../MoneyTransferWorkflowImpl.java | 116 ++++++++++++++++++ .../it/shared/TransactionDetails.java | 2 +- .../src/main/resources/application.properties | 2 +- .../quarkiverse/temporal/it/TemporalIT.java | 38 ++++++ 13 files changed, 267 insertions(+), 105 deletions(-) delete mode 100644 integration-tests/src/main/java/io/quarkiverse/temporal/it/client/TransferApp.java rename integration-tests/src/main/java/io/quarkiverse/temporal/it/{worker => defaultWorker}/AccountActivityImpl.java (97%) create mode 100644 integration-tests/src/main/java/io/quarkiverse/temporal/it/defaultWorker/CoreTransactionDetails.java rename integration-tests/src/main/java/io/quarkiverse/temporal/it/{worker => defaultWorker}/MoneyTransferWorkflowImpl.java (99%) create mode 100644 integration-tests/src/main/java/io/quarkiverse/temporal/it/namedWorker/AccountActivityImpl.java rename integration-tests/src/main/java/io/quarkiverse/temporal/it/{worker => namedWorker}/CoreTransactionDetails.java (96%) create mode 100644 integration-tests/src/main/java/io/quarkiverse/temporal/it/namedWorker/MoneyTransferWorkflowImpl.java diff --git a/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc b/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc index e26c5c3..4c31ba3 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc @@ -13,23 +13,6 @@ h|Default a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-enable-mock]]`link:#quarkus-temporal_quarkus-temporal-enable-mock[quarkus.temporal.enable-mock]` -[.description] --- -enable mock for testing - -ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_TEMPORAL_ENABLE_MOCK+++[] -endif::add-copy-button-to-env-var[] -ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_TEMPORAL_ENABLE_MOCK+++` -endif::add-copy-button-to-env-var[] ---|boolean -|`false` - - -a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-enable-mock]]`link:#quarkus-temporal_quarkus-temporal-enable-mock[quarkus.temporal.enable-mock]` - - [.description] -- enable mock for testing 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 29325c2..af64d4e 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 @@ -1,7 +1,9 @@ package io.quarkiverse.temporal.deployment; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -76,13 +78,17 @@ void produceWorkflows( AnnotationTarget target = instance.target(); Collection allKnownImplementors = beanArchiveBuildItem.getIndex() .getAllKnownImplementors(target.asClass().name()); - if (allKnownImplementors.size() != 1) { - throw new IllegalStateException("Workflow " + target.asClass().name() + " must have exactly one implementor"); - } + Set seenWorkers = new HashSet<>(); allKnownImplementors.forEach(implementor -> { AnnotationInstance annotation = implementor.annotation(WORKFLOW_IMPL); String[] workers = annotation == null ? new String[] { "" } : annotation.value("workers").asStringArray(); + + if (!Collections.disjoint(seenWorkers, Arrays.asList(workers))) { + throw new IllegalStateException( + "Workflow " + target.asClass().name() + " has more than one implementor on worker"); + } + Collections.addAll(seenWorkers, workers); producer.produce(new WorkflowImplBuildItem(loadClass(implementor), workers)); }); } @@ -97,13 +103,16 @@ void produceActivities( AnnotationTarget target = instance.target(); Collection allKnownImplementors = beanArchiveBuildItem.getIndex() .getAllKnownImplementors(target.asClass().name()); - if (allKnownImplementors.size() != 1) { - throw new IllegalStateException("Activity " + target.asClass().name() + " must have exactly one implementor"); - } + Set seenWorkers = new HashSet<>(); allKnownImplementors.forEach(implementor -> { AnnotationInstance annotation = implementor.annotation(ACTIVITY_IMPL); String[] workers = annotation == null ? new String[] { "" } : annotation.value("workers").asStringArray(); + if (!Collections.disjoint(seenWorkers, Arrays.asList(workers))) { + throw new IllegalStateException( + "Activity " + target.asClass().name() + " has more than one implementor on worker"); + } + Collections.addAll(seenWorkers, workers); producer.produce(new ActivityImplBuildItem(loadClass(implementor), workers)); }); } diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 596ffe8..b63c191 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -29,6 +29,11 @@ quarkus-temporal ${project.version} + + io.quarkiverse.temporal + quarkus-temporal-test + ${project.version} + io.quarkus quarkus-junit5 diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/client/TransferApp.java b/integration-tests/src/main/java/io/quarkiverse/temporal/it/client/TransferApp.java deleted file mode 100644 index 63835be..0000000 --- a/integration-tests/src/main/java/io/quarkiverse/temporal/it/client/TransferApp.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.quarkiverse.temporal.it.client; - -import static io.quarkiverse.temporal.it.shared.Shared.MONEY_TRANSFER_TASK_QUEUE; - -import java.security.SecureRandom; -import java.time.Instant; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; - -import io.quarkiverse.temporal.it.shared.MoneyTransferWorkflow; -import io.quarkiverse.temporal.it.shared.TransactionDetails; -import io.quarkiverse.temporal.it.worker.CoreTransactionDetails; -import io.temporal.api.common.v1.WorkflowExecution; -import io.temporal.client.WorkflowClient; -import io.temporal.client.WorkflowOptions; - -@Path("/hello") -@ApplicationScoped -public class TransferApp { - - @Inject - WorkflowClient client; - - private static final SecureRandom random; - - static { - // Seed the random number generator with nano date - random = new SecureRandom(); - random.setSeed(Instant.now().getNano()); - } - - public static String randomAccountIdentifier() { - return IntStream.range(0, 9) - .mapToObj(i -> String.valueOf(random.nextInt(10))) - .collect(Collectors.joining()); - } - - @GET - public String hello() { - // Workflow options configure Workflow stubs. - // A WorkflowId prevents duplicate instances, which are removed. - WorkflowOptions options = WorkflowOptions.newBuilder() - .setTaskQueue(MONEY_TRANSFER_TASK_QUEUE) - .setWorkflowId("money-transfer-workflow") - .build(); - - // WorkflowStubs enable calls to methods as if the Workflow object is local - // but actually perform a gRPC call to the Temporal Service. - MoneyTransferWorkflow workflow = client.newWorkflowStub(MoneyTransferWorkflow.class, options); - - // Configure the details for this money transfer request - String referenceId = UUID.randomUUID().toString().substring(0, 18); - String fromAccount = randomAccountIdentifier(); - String toAccount = randomAccountIdentifier(); - int amountToTransfer = ThreadLocalRandom.current().nextInt(15, 75); - TransactionDetails transaction = new CoreTransactionDetails(fromAccount, toAccount, referenceId, amountToTransfer); - - // Perform asynchronous execution. - // This process exits after making this call and printing details. - WorkflowExecution we = WorkflowClient.start(workflow::transfer, transaction); - - System.out.printf("\nMONEY TRANSFER PROJECT\n\n"); - System.out.printf("Initiating transfer of $%d from [Account %s] to [Account %s].\n\n", - amountToTransfer, fromAccount, toAccount); - System.out.printf("[WorkflowID: %s]\n[RunID: %s]\n[Transaction Reference: %s]\n\n", we.getWorkflowId(), we.getRunId(), - referenceId); - - return "Hello"; - } -} diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/worker/AccountActivityImpl.java b/integration-tests/src/main/java/io/quarkiverse/temporal/it/defaultWorker/AccountActivityImpl.java similarity index 97% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/worker/AccountActivityImpl.java rename to integration-tests/src/main/java/io/quarkiverse/temporal/it/defaultWorker/AccountActivityImpl.java index b1f39e0..37f55a7 100644 --- a/integration-tests/src/main/java/io/quarkiverse/temporal/it/worker/AccountActivityImpl.java +++ b/integration-tests/src/main/java/io/quarkiverse/temporal/it/defaultWorker/AccountActivityImpl.java @@ -1,4 +1,4 @@ -package io.quarkiverse.temporal.it.worker; +package io.quarkiverse.temporal.it.defaultWorker; import io.quarkiverse.temporal.it.shared.AccountActivity; import io.temporal.activity.Activity; diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/defaultWorker/CoreTransactionDetails.java b/integration-tests/src/main/java/io/quarkiverse/temporal/it/defaultWorker/CoreTransactionDetails.java new file mode 100644 index 0000000..8d82d41 --- /dev/null +++ b/integration-tests/src/main/java/io/quarkiverse/temporal/it/defaultWorker/CoreTransactionDetails.java @@ -0,0 +1,43 @@ +package io.quarkiverse.temporal.it.defaultWorker; + +import io.quarkiverse.temporal.it.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/src/main/java/io/quarkiverse/temporal/it/worker/MoneyTransferWorkflowImpl.java b/integration-tests/src/main/java/io/quarkiverse/temporal/it/defaultWorker/MoneyTransferWorkflowImpl.java similarity index 99% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/worker/MoneyTransferWorkflowImpl.java rename to integration-tests/src/main/java/io/quarkiverse/temporal/it/defaultWorker/MoneyTransferWorkflowImpl.java index 331b57a..d1e3657 100644 --- a/integration-tests/src/main/java/io/quarkiverse/temporal/it/worker/MoneyTransferWorkflowImpl.java +++ b/integration-tests/src/main/java/io/quarkiverse/temporal/it/defaultWorker/MoneyTransferWorkflowImpl.java @@ -1,4 +1,4 @@ -package io.quarkiverse.temporal.it.worker; +package io.quarkiverse.temporal.it.defaultWorker; import java.time.Duration; import java.util.HashMap; diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/namedWorker/AccountActivityImpl.java b/integration-tests/src/main/java/io/quarkiverse/temporal/it/namedWorker/AccountActivityImpl.java new file mode 100644 index 0000000..b4b42a8 --- /dev/null +++ b/integration-tests/src/main/java/io/quarkiverse/temporal/it/namedWorker/AccountActivityImpl.java @@ -0,0 +1,45 @@ +package io.quarkiverse.temporal.it.namedWorker; + +import io.quarkiverse.temporal.ActivityImpl; +import io.quarkiverse.temporal.it.shared.AccountActivity; +import io.temporal.activity.Activity; + +@ActivityImpl(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/src/main/java/io/quarkiverse/temporal/it/worker/CoreTransactionDetails.java b/integration-tests/src/main/java/io/quarkiverse/temporal/it/namedWorker/CoreTransactionDetails.java similarity index 96% rename from integration-tests/src/main/java/io/quarkiverse/temporal/it/worker/CoreTransactionDetails.java rename to integration-tests/src/main/java/io/quarkiverse/temporal/it/namedWorker/CoreTransactionDetails.java index fcd11de..55287df 100644 --- a/integration-tests/src/main/java/io/quarkiverse/temporal/it/worker/CoreTransactionDetails.java +++ b/integration-tests/src/main/java/io/quarkiverse/temporal/it/namedWorker/CoreTransactionDetails.java @@ -1,4 +1,4 @@ -package io.quarkiverse.temporal.it.worker; +package io.quarkiverse.temporal.it.namedWorker; import io.quarkiverse.temporal.it.shared.TransactionDetails; diff --git a/integration-tests/src/main/java/io/quarkiverse/temporal/it/namedWorker/MoneyTransferWorkflowImpl.java b/integration-tests/src/main/java/io/quarkiverse/temporal/it/namedWorker/MoneyTransferWorkflowImpl.java new file mode 100644 index 0000000..c979e4a --- /dev/null +++ b/integration-tests/src/main/java/io/quarkiverse/temporal/it/namedWorker/MoneyTransferWorkflowImpl.java @@ -0,0 +1,116 @@ +package io.quarkiverse.temporal.it.namedWorker; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import io.quarkiverse.temporal.WorkflowImpl; +import io.quarkiverse.temporal.it.shared.AccountActivity; +import io.quarkiverse.temporal.it.shared.MoneyTransferWorkflow; +import io.quarkiverse.temporal.it.shared.TransactionDetails; +import io.temporal.activity.ActivityOptions; +import io.temporal.common.RetryOptions; +import io.temporal.workflow.Workflow; + +@WorkflowImpl(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/src/main/java/io/quarkiverse/temporal/it/shared/TransactionDetails.java b/integration-tests/src/main/java/io/quarkiverse/temporal/it/shared/TransactionDetails.java index 84e3a2e..39f1d09 100644 --- a/integration-tests/src/main/java/io/quarkiverse/temporal/it/shared/TransactionDetails.java +++ b/integration-tests/src/main/java/io/quarkiverse/temporal/it/shared/TransactionDetails.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.quarkiverse.temporal.it.worker.CoreTransactionDetails; +import io.quarkiverse.temporal.it.defaultWorker.CoreTransactionDetails; @JsonDeserialize(as = CoreTransactionDetails.class) public interface TransactionDetails { diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties index 7e3691c..bd1af6d 100644 --- a/integration-tests/src/main/resources/application.properties +++ b/integration-tests/src/main/resources/application.properties @@ -1,2 +1,2 @@ quarkus.http.port=8081 -quarkus.native.additional-build-args=--initialize-at-run-time=io.quarkiverse.temporal.it.client.TransferApp \ No newline at end of file +quarkus.temporal.enable-mock=true \ No newline at end of file diff --git a/integration-tests/src/test/java/io/quarkiverse/temporal/it/TemporalIT.java b/integration-tests/src/test/java/io/quarkiverse/temporal/it/TemporalIT.java index 86d009d..f8ef658 100644 --- a/integration-tests/src/test/java/io/quarkiverse/temporal/it/TemporalIT.java +++ b/integration-tests/src/test/java/io/quarkiverse/temporal/it/TemporalIT.java @@ -1,7 +1,45 @@ package io.quarkiverse.temporal.it; +import java.util.concurrent.TimeoutException; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; + +import io.quarkiverse.temporal.it.defaultWorker.CoreTransactionDetails; +import io.quarkiverse.temporal.it.shared.MoneyTransferWorkflow; +import io.quarkiverse.temporal.it.shared.TransactionDetails; import io.quarkus.test.junit.QuarkusTest; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; @QuarkusTest public class TemporalIT { + + @Inject + WorkflowClient client; + + @Test + public void testRunWorkflowOnDefaultWorker() throws TimeoutException { + WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue("") + .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); + } }