From 284f4fa75949d487a06815aaf1a81d93ee49c87c Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Fri, 18 Oct 2024 12:21:08 -0700 Subject: [PATCH 01/15] EarlyReturn sample (Update-With-Start) --- .../samples/earlyreturn/EarlyReturn.java | 256 ++++++++++++++++++ .../io/temporal/samples/earlyreturn/README.md | 19 ++ 2 files changed, 275 insertions(+) create mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturn.java create mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/README.md diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturn.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturn.java new file mode 100644 index 00000000..a89f27d8 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturn.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package io.temporal.samples.earlyreturn; + +import io.temporal.activity.ActivityInterface; +import io.temporal.activity.ActivityMethod; +import io.temporal.activity.ActivityOptions; +import io.temporal.client.*; +import io.temporal.failure.ApplicationFailure; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.Worker; +import io.temporal.worker.WorkerFactory; +import io.temporal.workflow.*; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EarlyReturn { + private static final String TASK_QUEUE = "EarlyReturnTaskQueue"; + private static final String UPDATE_NAME = "early-return"; + + public static void main(String[] args) { + WorkflowClient client = setupWorkflowClient(); + startWorker(client); + runWorkflowWithUpdateWithStart(client); + } + + private static WorkflowClient setupWorkflowClient() { + WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); + return WorkflowClient.newInstance(service); + } + + private static void startWorker(WorkflowClient client) { + WorkerFactory factory = WorkerFactory.newInstance(client); + Worker worker = factory.newWorker(TASK_QUEUE); + + worker.registerWorkflowImplementationTypes(TransactionWorkflowImpl.class); + worker.registerActivitiesImplementations(new TransactionActivitiesImpl()); + + factory.start(); + System.out.println("Worker started"); + } + + private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { + Transaction tx = + new Transaction( + "Bob", + "Alice", + // Change this amount to a negative number to have initTransaction fail + 10000); + WorkflowOptions options = + WorkflowOptions.newBuilder() + .setTaskQueue(TASK_QUEUE) + .setWorkflowId("early-return-workflow-" + System.currentTimeMillis()) + .build(); + + WorkflowStub workflowStub = client.newUntypedWorkflowStub("TransactionWorkflow", options); + + try { + System.out.println("Starting workflow with UpdateWithStart"); + + UpdateWithStartWorkflowOperation update = + UpdateWithStartWorkflowOperation.newBuilder(UPDATE_NAME, String.class, new Object[] {}) + .setWaitForStage(WorkflowUpdateStage.COMPLETED) + .build(); + + WorkflowUpdateHandle updateHandle = workflowStub.updateWithStart(update, tx); + String transactionId = updateHandle.getResultAsync().get(); + System.out.println("Transaction initialized successfully: " + transactionId); + + // The workflow will continue running, completing the transaction. + String result = workflowStub.getResult(String.class); + System.out.println("Workflow completed with result: " + result); + } catch (Exception e) { + System.out.println("Error during workflow execution: " + e.getCause()); + // The workflow will continue running, cancelling the transaction. + } + } + + @WorkflowInterface + public interface TransactionWorkflow { + @WorkflowMethod + String processTransaction(Transaction tx); + + @UpdateMethod(name = UPDATE_NAME) + String returnInitResult(); + } + + public static class TransactionWorkflowImpl implements TransactionWorkflow { + private static final Logger log = LoggerFactory.getLogger(TransactionWorkflowImpl.class); + private final TransactionActivities activities = + Workflow.newActivityStub( + TransactionActivities.class, + ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(30)).build()); + + private boolean initDone = false; + private Transaction tx; + private Exception initError = null; + + @Override + public String processTransaction(Transaction txInput) { + this.tx = txInput; + // Phase 1: Initialize the transaction + try { + this.tx = activities.initTransaction(this.tx); + } catch (Exception e) { + initError = e; + } finally { + initDone = true; + } + + // Phase 2: Complete or cancel the transaction + if (initError != null) { + activities.cancelTransaction(this.tx); + return "Transaction cancelled"; + } else { + activities.completeTransaction(this.tx); + return "Transaction completed successfully: " + this.tx.id; + } + } + + @Override + public String returnInitResult() { + Workflow.await(() -> initDone); + if (initError != null) { + log.info("Initialization failed."); + throw Workflow.wrap(initError); + } + return tx.getId(); + } + } + + @ActivityInterface + public interface TransactionActivities { + @ActivityMethod + Transaction initTransaction(Transaction tx); + + @ActivityMethod + void cancelTransaction(Transaction tx); + + @ActivityMethod + void completeTransaction(Transaction tx); + } + + public static class TransactionActivitiesImpl implements TransactionActivities { + @Override + public Transaction initTransaction(Transaction tx) { + System.out.println("Initializing transaction"); + sleep(500); + if (tx.getAmount() <= 0) { + System.out.println("Invalid amount: " + tx.getAmount()); + throw ApplicationFailure.newNonRetryableFailure( + "Non-retryable Activity Failure: Invalid Amount", "InvalidAmount"); + } + // mint a transaction ID + String transactionId = + "TXID" + String.format("%010d", (long) (Math.random() * 1_000_000_0000L)); + tx.setId(transactionId); + sleep(500); + return tx; + } + + @Override + public void cancelTransaction(Transaction tx) { + System.out.println("Cancelling transaction"); + sleep(2000); + System.out.println("Transaction cancelled"); + } + + @Override + public void completeTransaction(Transaction tx) { + System.out.println( + "Sending $" + + tx.getAmount() + + " from " + + tx.getSourceAccount() + + " to " + + tx.getTargetAccount()); + sleep(2000); + System.out.println("Transaction completed successfully"); + } + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + public static class Transaction { + private String id; + private String sourceAccount; + private String targetAccount; + private int amount; + + // No-arg constructor for Jackson deserialization + public Transaction() {} + + public Transaction(String sourceAccount, String targetAccount, int amount) { + this.sourceAccount = sourceAccount; + this.targetAccount = targetAccount; + this.amount = amount; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSourceAccount() { + return sourceAccount; + } + + public void setSourceAccount(String sourceAccount) { + this.sourceAccount = sourceAccount; + } + + public String getTargetAccount() { + return targetAccount; + } + + public void setTargetAccount(String targetAccount) { + this.targetAccount = targetAccount; + } + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } + } +} diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/README.md b/core/src/main/java/io/temporal/samples/earlyreturn/README.md new file mode 100644 index 00000000..1e0b2c7e --- /dev/null +++ b/core/src/main/java/io/temporal/samples/earlyreturn/README.md @@ -0,0 +1,19 @@ +### Early-Return Sample + +This sample demonstrates an early-return from a workflow. + +By utilizing Update-with-Start, a client can start a new workflow and synchronously receive +a response mid-workflow, while the workflow continues to run to completion. + +Run the following command to start the sample: + +```bash +./gradlew -q execute -PmainClass=io.temporal.samples.earlyreturn.EarlyReturn +``` + +* The client will start a workflow using Update-With-Start. +* Update-With-Start will trigger an initialization step. +* If the initialization step succeeds (default), intialization will return to the client with a transaction ID and the workflow will continue. The workflow will then complete and return the final result. +* If the intitialization step fails (amount <= 0), the workflow will return to the client with an error message and the workflow will run an activity to cancel the transaction. + +To trigger a failed initialization, set the amount to <= 0 in the EarlyReturn class's `runWorkflowWithUpdateWithStart` method. \ No newline at end of file From 5082bb6192ee83ab1f13f37d921653a911ee8a92 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Mon, 21 Oct 2024 16:22:56 -0700 Subject: [PATCH 02/15] multifile but typed stub doesn't work --- .../samples/earlyreturn/EarlyReturn.java | 256 ------------------ .../earlyreturn/EarlyReturnClient.java | 60 ++++ .../earlyreturn/EarlyReturnWorker.java | 25 ++ .../io/temporal/samples/earlyreturn/README.md | 11 +- .../samples/earlyreturn/Transaction.java | 84 ++++++ .../earlyreturn/TransactionActivities.java | 16 ++ .../TransactionActivitiesImpl.java | 48 ++++ .../earlyreturn/TransactionWorkflow.java | 14 + .../earlyreturn/TransactionWorkflowImpl.java | 49 ++++ 9 files changed, 304 insertions(+), 259 deletions(-) delete mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturn.java create mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java create mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnWorker.java create mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java create mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java create mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java create mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java create mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturn.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturn.java deleted file mode 100644 index a89f27d8..00000000 --- a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturn.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved - * - * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Modifications copyright (C) 2017 Uber Technologies, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not - * use this file except in compliance with the License. A copy of the License is - * located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package io.temporal.samples.earlyreturn; - -import io.temporal.activity.ActivityInterface; -import io.temporal.activity.ActivityMethod; -import io.temporal.activity.ActivityOptions; -import io.temporal.client.*; -import io.temporal.failure.ApplicationFailure; -import io.temporal.serviceclient.WorkflowServiceStubs; -import io.temporal.worker.Worker; -import io.temporal.worker.WorkerFactory; -import io.temporal.workflow.*; -import java.time.Duration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class EarlyReturn { - private static final String TASK_QUEUE = "EarlyReturnTaskQueue"; - private static final String UPDATE_NAME = "early-return"; - - public static void main(String[] args) { - WorkflowClient client = setupWorkflowClient(); - startWorker(client); - runWorkflowWithUpdateWithStart(client); - } - - private static WorkflowClient setupWorkflowClient() { - WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); - return WorkflowClient.newInstance(service); - } - - private static void startWorker(WorkflowClient client) { - WorkerFactory factory = WorkerFactory.newInstance(client); - Worker worker = factory.newWorker(TASK_QUEUE); - - worker.registerWorkflowImplementationTypes(TransactionWorkflowImpl.class); - worker.registerActivitiesImplementations(new TransactionActivitiesImpl()); - - factory.start(); - System.out.println("Worker started"); - } - - private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { - Transaction tx = - new Transaction( - "Bob", - "Alice", - // Change this amount to a negative number to have initTransaction fail - 10000); - WorkflowOptions options = - WorkflowOptions.newBuilder() - .setTaskQueue(TASK_QUEUE) - .setWorkflowId("early-return-workflow-" + System.currentTimeMillis()) - .build(); - - WorkflowStub workflowStub = client.newUntypedWorkflowStub("TransactionWorkflow", options); - - try { - System.out.println("Starting workflow with UpdateWithStart"); - - UpdateWithStartWorkflowOperation update = - UpdateWithStartWorkflowOperation.newBuilder(UPDATE_NAME, String.class, new Object[] {}) - .setWaitForStage(WorkflowUpdateStage.COMPLETED) - .build(); - - WorkflowUpdateHandle updateHandle = workflowStub.updateWithStart(update, tx); - String transactionId = updateHandle.getResultAsync().get(); - System.out.println("Transaction initialized successfully: " + transactionId); - - // The workflow will continue running, completing the transaction. - String result = workflowStub.getResult(String.class); - System.out.println("Workflow completed with result: " + result); - } catch (Exception e) { - System.out.println("Error during workflow execution: " + e.getCause()); - // The workflow will continue running, cancelling the transaction. - } - } - - @WorkflowInterface - public interface TransactionWorkflow { - @WorkflowMethod - String processTransaction(Transaction tx); - - @UpdateMethod(name = UPDATE_NAME) - String returnInitResult(); - } - - public static class TransactionWorkflowImpl implements TransactionWorkflow { - private static final Logger log = LoggerFactory.getLogger(TransactionWorkflowImpl.class); - private final TransactionActivities activities = - Workflow.newActivityStub( - TransactionActivities.class, - ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(30)).build()); - - private boolean initDone = false; - private Transaction tx; - private Exception initError = null; - - @Override - public String processTransaction(Transaction txInput) { - this.tx = txInput; - // Phase 1: Initialize the transaction - try { - this.tx = activities.initTransaction(this.tx); - } catch (Exception e) { - initError = e; - } finally { - initDone = true; - } - - // Phase 2: Complete or cancel the transaction - if (initError != null) { - activities.cancelTransaction(this.tx); - return "Transaction cancelled"; - } else { - activities.completeTransaction(this.tx); - return "Transaction completed successfully: " + this.tx.id; - } - } - - @Override - public String returnInitResult() { - Workflow.await(() -> initDone); - if (initError != null) { - log.info("Initialization failed."); - throw Workflow.wrap(initError); - } - return tx.getId(); - } - } - - @ActivityInterface - public interface TransactionActivities { - @ActivityMethod - Transaction initTransaction(Transaction tx); - - @ActivityMethod - void cancelTransaction(Transaction tx); - - @ActivityMethod - void completeTransaction(Transaction tx); - } - - public static class TransactionActivitiesImpl implements TransactionActivities { - @Override - public Transaction initTransaction(Transaction tx) { - System.out.println("Initializing transaction"); - sleep(500); - if (tx.getAmount() <= 0) { - System.out.println("Invalid amount: " + tx.getAmount()); - throw ApplicationFailure.newNonRetryableFailure( - "Non-retryable Activity Failure: Invalid Amount", "InvalidAmount"); - } - // mint a transaction ID - String transactionId = - "TXID" + String.format("%010d", (long) (Math.random() * 1_000_000_0000L)); - tx.setId(transactionId); - sleep(500); - return tx; - } - - @Override - public void cancelTransaction(Transaction tx) { - System.out.println("Cancelling transaction"); - sleep(2000); - System.out.println("Transaction cancelled"); - } - - @Override - public void completeTransaction(Transaction tx) { - System.out.println( - "Sending $" - + tx.getAmount() - + " from " - + tx.getSourceAccount() - + " to " - + tx.getTargetAccount()); - sleep(2000); - System.out.println("Transaction completed successfully"); - } - - private void sleep(long millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } - - public static class Transaction { - private String id; - private String sourceAccount; - private String targetAccount; - private int amount; - - // No-arg constructor for Jackson deserialization - public Transaction() {} - - public Transaction(String sourceAccount, String targetAccount, int amount) { - this.sourceAccount = sourceAccount; - this.targetAccount = targetAccount; - this.amount = amount; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getSourceAccount() { - return sourceAccount; - } - - public void setSourceAccount(String sourceAccount) { - this.sourceAccount = sourceAccount; - } - - public String getTargetAccount() { - return targetAccount; - } - - public void setTargetAccount(String targetAccount) { - this.targetAccount = targetAccount; - } - - public int getAmount() { - return amount; - } - - public void setAmount(int amount) { - this.amount = amount; - } - } -} diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java new file mode 100644 index 00000000..b0ab1416 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java @@ -0,0 +1,60 @@ +package io.temporal.samples.earlyreturn; + +import io.temporal.client.*; +import io.temporal.serviceclient.WorkflowServiceStubs; + +public class EarlyReturnClient { + private static final String TASK_QUEUE = "EarlyReturnTaskQueue"; + private static final String WORKFLOW_ID_PREFIX = "early-return-workflow-"; + + public static void main(String[] args) { + WorkflowClient client = setupWorkflowClient(); + runWorkflowWithUpdateWithStart(client); + } + + // Setup the WorkflowClient + public static WorkflowClient setupWorkflowClient() { + WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); + return WorkflowClient.newInstance(service); + } + + // Run workflow using 'updateWithStart' + private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { + Transaction tx = new Transaction("", "Bob", "Alice", -1000); + + WorkflowOptions options = buildWorkflowOptions(); + TransactionWorkflow workflow = client.newWorkflowStub(TransactionWorkflow.class, options); + + try { + System.out.println("Starting workflow with UpdateWithStart"); + + UpdateWithStartWorkflowOperation updateOp = + UpdateWithStartWorkflowOperation.newBuilder(workflow::returnInitResult) + .setWaitForStage(WorkflowUpdateStage.COMPLETED) + .build(); + + // Ensure tx is passed directly into the correct start method. + WorkflowUpdateHandle updateHandle = + WorkflowClient.updateWithStart(() -> workflow.processTransaction(tx), updateOp); + + String transactionId = updateHandle.getResultAsync().get(); + + System.out.println("Transaction initialized successfully: " + transactionId); + + // // Get the final result of the workflow + // String result = updateHandle.getId() + // System.out.println("Workflow completed with result: " + result); + + } catch (Exception e) { + System.err.println("Transaction initialization failed: " + e.getMessage()); + } + } + + // Build WorkflowOptions with task queue and unique ID + private static WorkflowOptions buildWorkflowOptions() { + return WorkflowOptions.newBuilder() + .setTaskQueue(TASK_QUEUE) + .setWorkflowId(WORKFLOW_ID_PREFIX + System.currentTimeMillis()) + .build(); + } +} diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnWorker.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnWorker.java new file mode 100644 index 00000000..97ef9320 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnWorker.java @@ -0,0 +1,25 @@ +package io.temporal.samples.earlyreturn; + +import io.temporal.client.WorkflowClient; +import io.temporal.worker.Worker; +import io.temporal.worker.WorkerFactory; + +public class EarlyReturnWorker { + private static final String TASK_QUEUE = "EarlyReturnTaskQueue"; + + public static void main(String[] args) { + WorkflowClient client = EarlyReturnClient.setupWorkflowClient(); + startWorker(client); + } + + private static void startWorker(WorkflowClient client) { + WorkerFactory factory = WorkerFactory.newInstance(client); + Worker worker = factory.newWorker(TASK_QUEUE); + + worker.registerWorkflowImplementationTypes(TransactionWorkflowImpl.class); + worker.registerActivitiesImplementations(new TransactionActivitiesImpl()); + + factory.start(); + System.out.println("Worker started"); + } +} diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/README.md b/core/src/main/java/io/temporal/samples/earlyreturn/README.md index 1e0b2c7e..f0c48660 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/README.md +++ b/core/src/main/java/io/temporal/samples/earlyreturn/README.md @@ -5,10 +5,15 @@ This sample demonstrates an early-return from a workflow. By utilizing Update-with-Start, a client can start a new workflow and synchronously receive a response mid-workflow, while the workflow continues to run to completion. -Run the following command to start the sample: +To run the sample, start the worker: +```bash +./gradlew -q execute -PmainClass=io.temporal.samples.earlyreturn.EarlyReturnWorker +``` + +Then, start the client: ```bash -./gradlew -q execute -PmainClass=io.temporal.samples.earlyreturn.EarlyReturn +./gradlew -q execute -PmainClass=io.temporal.samples.earlyreturn.EarlyReturnClient ``` * The client will start a workflow using Update-With-Start. @@ -16,4 +21,4 @@ Run the following command to start the sample: * If the initialization step succeeds (default), intialization will return to the client with a transaction ID and the workflow will continue. The workflow will then complete and return the final result. * If the intitialization step fails (amount <= 0), the workflow will return to the client with an error message and the workflow will run an activity to cancel the transaction. -To trigger a failed initialization, set the amount to <= 0 in the EarlyReturn class's `runWorkflowWithUpdateWithStart` method. \ No newline at end of file +To trigger a failed initialization, set the amount to <= 0 in the `EarlyReturnClient` class's `runWorkflowWithUpdateWithStart` method and re-run the client. \ No newline at end of file diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java b/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java new file mode 100644 index 00000000..adb87012 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 Temporal Technologies, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +package io.temporal.samples.earlyreturn; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Transaction { + private String id; // Mutable field + private String sourceAccount; + private String targetAccount; + private int amount; + + // No-arg constructor for serialization frameworks + public Transaction() {} + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Transaction( + @JsonProperty("id") String id, + @JsonProperty("sourceAccount") String sourceAccount, + @JsonProperty("targetAccount") String targetAccount, + @JsonProperty("amount") int amount) { + this.id = id; + this.sourceAccount = sourceAccount; + this.targetAccount = targetAccount; + this.amount = amount; + } + + // Getters and Setters for each field + @JsonProperty("id") + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @JsonProperty("sourceAccount") + public String getSourceAccount() { + return sourceAccount; + } + + public void setSourceAccount(String sourceAccount) { + this.sourceAccount = sourceAccount; + } + + @JsonProperty("targetAccount") + public String getTargetAccount() { + return targetAccount; + } + + public void setTargetAccount(String targetAccount) { + this.targetAccount = targetAccount; + } + + @JsonProperty("amount") + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } + + @Override + public String toString() { + return String.format( + "Transaction{id='%s', sourceAccount='%s', targetAccount='%s', amount=%d}", + id, sourceAccount, targetAccount, amount); + } +} diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java new file mode 100644 index 00000000..1c018759 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java @@ -0,0 +1,16 @@ +package io.temporal.samples.earlyreturn; + +import io.temporal.activity.ActivityInterface; +import io.temporal.activity.ActivityMethod; + +@ActivityInterface +public interface TransactionActivities { + @ActivityMethod + Transaction initTransaction(Transaction tx); + + @ActivityMethod + void cancelTransaction(Transaction tx); + + @ActivityMethod + void completeTransaction(Transaction tx); +} diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java new file mode 100644 index 00000000..39cbda12 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java @@ -0,0 +1,48 @@ +package io.temporal.samples.earlyreturn; + +import io.temporal.failure.ApplicationFailure; + +public class TransactionActivitiesImpl implements TransactionActivities { + + @Override + public Transaction initTransaction(Transaction tx) { + System.out.println("Initializing transaction"); + sleep(500); + if (tx.getAmount() <= 0) { + System.out.println("Invalid amount: " + tx.getAmount()); + throw ApplicationFailure.newNonRetryableFailure( + "Non-retryable Activity Failure: Invalid Amount", "InvalidAmount"); + } + tx.setId("TXID" + String.format("%010d", (long) (Math.random() * 1_000_000_0000L))); + sleep(500); + return tx; + } + + @Override + public void cancelTransaction(Transaction tx) { + System.out.println("Cancelling transaction"); + sleep(2000); + System.out.println("Transaction cancelled"); + } + + @Override + public void completeTransaction(Transaction tx) { + System.out.println( + "Sending $" + + tx.getAmount() + + " from " + + tx.getSourceAccount() + + " to " + + tx.getTargetAccount()); + sleep(2000); + System.out.println("Transaction completed successfully"); + } + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java new file mode 100644 index 00000000..272a5343 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java @@ -0,0 +1,14 @@ +package io.temporal.samples.earlyreturn; + +import io.temporal.workflow.UpdateMethod; +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; + +@WorkflowInterface +public interface TransactionWorkflow { + @WorkflowMethod + String processTransaction(Transaction tx); + + @UpdateMethod(name = "early-return") + String returnInitResult(); +} diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java new file mode 100644 index 00000000..b3ecf3c6 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java @@ -0,0 +1,49 @@ +package io.temporal.samples.earlyreturn; + +import io.temporal.activity.ActivityOptions; +import io.temporal.workflow.Workflow; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TransactionWorkflowImpl implements TransactionWorkflow { + private static final Logger log = LoggerFactory.getLogger(TransactionWorkflowImpl.class); + private final TransactionActivities activities = + Workflow.newActivityStub( + TransactionActivities.class, + ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(30)).build()); + + private boolean initDone = false; + private Transaction tx; + private Exception initError = null; + + @Override + public String processTransaction(Transaction txInput) { + this.tx = txInput; + try { + this.tx = activities.initTransaction(this.tx); + } catch (Exception e) { + initError = e; + } finally { + initDone = true; + } + + if (initError != null) { + activities.cancelTransaction(this.tx); + return "Transaction cancelled"; + } else { + activities.completeTransaction(this.tx); + return "Transaction completed successfully: " + this.tx.getId(); + } + } + + @Override + public String returnInitResult() { + Workflow.await(() -> initDone); + if (initError != null) { + log.info("Initialization failed."); + throw Workflow.wrap(initError); + } + return tx.getId(); + } +} From d38125b2010c17e63b651d5bdc898ca1945c27f3 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Mon, 21 Oct 2024 16:24:08 -0700 Subject: [PATCH 03/15] multifile but typed stub doesn't work --- .../samples/earlyreturn/EarlyReturnClient.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java index b0ab1416..ed11f2b6 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java @@ -29,21 +29,18 @@ private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { System.out.println("Starting workflow with UpdateWithStart"); UpdateWithStartWorkflowOperation updateOp = - UpdateWithStartWorkflowOperation.newBuilder(workflow::returnInitResult) - .setWaitForStage(WorkflowUpdateStage.COMPLETED) - .build(); + UpdateWithStartWorkflowOperation.newBuilder(workflow::returnInitResult) + .setWaitForStage(WorkflowUpdateStage.COMPLETED) // Wait for update to complete + .build(); - // Ensure tx is passed directly into the correct start method. WorkflowUpdateHandle updateHandle = - WorkflowClient.updateWithStart(() -> workflow.processTransaction(tx), updateOp); + WorkflowClient.updateWithStart(workflow::processTransaction, tx, updateOp); String transactionId = updateHandle.getResultAsync().get(); System.out.println("Transaction initialized successfully: " + transactionId); - // // Get the final result of the workflow - // String result = updateHandle.getId() - // System.out.println("Workflow completed with result: " + result); + // TODO get the result of the workflow } catch (Exception e) { System.err.println("Transaction initialization failed: " + e.getMessage()); From 86c48835f60a2d55963e4eff82d07ff12efe2486 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Mon, 21 Oct 2024 22:07:34 -0700 Subject: [PATCH 04/15] split files, using jackson for transaction object --- .../earlyreturn/EarlyReturnClient.java | 33 +++++++++++++++---- .../earlyreturn/EarlyReturnWorker.java | 19 +++++++++++ .../samples/earlyreturn/Transaction.java | 21 +++++++----- .../earlyreturn/TransactionActivities.java | 19 +++++++++++ .../TransactionActivitiesImpl.java | 19 +++++++++++ .../earlyreturn/TransactionWorkflow.java | 19 +++++++++++ .../earlyreturn/TransactionWorkflowImpl.java | 19 +++++++++++ 7 files changed, 135 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java index ed11f2b6..a43f55b3 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java @@ -1,3 +1,22 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package io.temporal.samples.earlyreturn; import io.temporal.client.*; @@ -20,7 +39,8 @@ public static WorkflowClient setupWorkflowClient() { // Run workflow using 'updateWithStart' private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { - Transaction tx = new Transaction("", "Bob", "Alice", -1000); + Transaction tx = new Transaction("", "Bob", "Alice", + 1000); // Change this amount to a negative number to have initTransaction fail WorkflowOptions options = buildWorkflowOptions(); TransactionWorkflow workflow = client.newWorkflowStub(TransactionWorkflow.class, options); @@ -29,18 +49,19 @@ private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { System.out.println("Starting workflow with UpdateWithStart"); UpdateWithStartWorkflowOperation updateOp = - UpdateWithStartWorkflowOperation.newBuilder(workflow::returnInitResult) - .setWaitForStage(WorkflowUpdateStage.COMPLETED) // Wait for update to complete - .build(); + UpdateWithStartWorkflowOperation.newBuilder(workflow::returnInitResult) + .setWaitForStage(WorkflowUpdateStage.COMPLETED) // Wait for update to complete + .build(); WorkflowUpdateHandle updateHandle = - WorkflowClient.updateWithStart(workflow::processTransaction, tx, updateOp); + WorkflowClient.updateWithStart(workflow::processTransaction, tx, updateOp); String transactionId = updateHandle.getResultAsync().get(); System.out.println("Transaction initialized successfully: " + transactionId); - // TODO get the result of the workflow + String result = WorkflowStub.fromTyped(workflow).getResult(String.class); + System.out.println("Workflow completed with result: " + result); } catch (Exception e) { System.err.println("Transaction initialization failed: " + e.getMessage()); diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnWorker.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnWorker.java index 97ef9320..222368ec 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnWorker.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnWorker.java @@ -1,3 +1,22 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package io.temporal.samples.earlyreturn; import io.temporal.client.WorkflowClient; diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java b/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java index adb87012..54e1adbf 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java @@ -1,15 +1,20 @@ /* - * Copyright (c) 2024 Temporal Technologies, Inc. All Rights Reserved. + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not - * use this file except in compliance with the License. A copy of the License is - * located at + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. * - * http://www.apache.org/licenses/LICENSE-2.0 + * Modifications copyright (C) 2017 Uber Technologies, Inc. * - * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. */ package io.temporal.samples.earlyreturn; diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java index 1c018759..156dece5 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java @@ -1,3 +1,22 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package io.temporal.samples.earlyreturn; import io.temporal.activity.ActivityInterface; diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java index 39cbda12..54830c56 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java @@ -1,3 +1,22 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package io.temporal.samples.earlyreturn; import io.temporal.failure.ApplicationFailure; diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java index 272a5343..47ad69cc 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java @@ -1,3 +1,22 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package io.temporal.samples.earlyreturn; import io.temporal.workflow.UpdateMethod; diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java index b3ecf3c6..e33457c7 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java @@ -1,3 +1,22 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package io.temporal.samples.earlyreturn; import io.temporal.activity.ActivityOptions; From 497f2dc9f8512875f43343448a2b7679b023dede Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Mon, 21 Oct 2024 22:20:05 -0700 Subject: [PATCH 05/15] spotlessApply --- .../io/temporal/samples/earlyreturn/EarlyReturnClient.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java index a43f55b3..c74d548d 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java @@ -39,7 +39,9 @@ public static WorkflowClient setupWorkflowClient() { // Run workflow using 'updateWithStart' private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { - Transaction tx = new Transaction("", "Bob", "Alice", + Transaction tx = + new Transaction( + "", "Bob", "Alice", 1000); // Change this amount to a negative number to have initTransaction fail WorkflowOptions options = buildWorkflowOptions(); From d161cbd549791913348d1552d50b4fe4b53ca685 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Wed, 23 Oct 2024 09:40:39 -0700 Subject: [PATCH 06/15] workflow and update returns objects now --- .../earlyreturn/EarlyReturnClient.java | 14 +++-- .../earlyreturn/TransactionWorkflow.java | 4 +- .../earlyreturn/TransactionWorkflowImpl.java | 12 +++-- .../samples/earlyreturn/TxResult.java | 51 +++++++++++++++++++ 4 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/TxResult.java diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java index c74d548d..a21136d4 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java @@ -50,17 +50,21 @@ private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { try { System.out.println("Starting workflow with UpdateWithStart"); - UpdateWithStartWorkflowOperation updateOp = + UpdateWithStartWorkflowOperation updateOp = UpdateWithStartWorkflowOperation.newBuilder(workflow::returnInitResult) .setWaitForStage(WorkflowUpdateStage.COMPLETED) // Wait for update to complete .build(); - WorkflowUpdateHandle updateHandle = + WorkflowUpdateHandle updateHandle = WorkflowClient.updateWithStart(workflow::processTransaction, tx, updateOp); - String transactionId = updateHandle.getResultAsync().get(); - - System.out.println("Transaction initialized successfully: " + transactionId); + TxResult updateResult = updateHandle.getResultAsync().get(); + System.out.println( + "Workflow initialized with result: " + + updateResult.getStatus() + + " (transactionId: " + + updateResult.getTransactionId() + + ")"); String result = WorkflowStub.fromTyped(workflow).getResult(String.class); System.out.println("Workflow completed with result: " + result); diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java index 47ad69cc..3e2d76ee 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java @@ -26,8 +26,8 @@ @WorkflowInterface public interface TransactionWorkflow { @WorkflowMethod - String processTransaction(Transaction tx); + TxResult processTransaction(Transaction tx); @UpdateMethod(name = "early-return") - String returnInitResult(); + TxResult returnInitResult(); } diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java index e33457c7..750f6882 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java @@ -37,7 +37,7 @@ public class TransactionWorkflowImpl implements TransactionWorkflow { private Exception initError = null; @Override - public String processTransaction(Transaction txInput) { + public TxResult processTransaction(Transaction txInput) { this.tx = txInput; try { this.tx = activities.initTransaction(this.tx); @@ -49,20 +49,22 @@ public String processTransaction(Transaction txInput) { if (initError != null) { activities.cancelTransaction(this.tx); - return "Transaction cancelled"; + return new TxResult(this.tx.getId(), "Transaction cancelled."); } else { activities.completeTransaction(this.tx); - return "Transaction completed successfully: " + this.tx.getId(); + return new TxResult(this.tx.getId(), "Transaction completed successfully."); } } @Override - public String returnInitResult() { + public TxResult returnInitResult() { Workflow.await(() -> initDone); + if (initError != null) { log.info("Initialization failed."); throw Workflow.wrap(initError); } - return tx.getId(); + + return new TxResult(tx.getId(), "Initialization successful"); } } diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TxResult.java b/core/src/main/java/io/temporal/samples/earlyreturn/TxResult.java new file mode 100644 index 00000000..04c9a986 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TxResult.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package io.temporal.samples.earlyreturn; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TxResult { + private final String transactionId; + private final String status; + + // Jackson-compatible constructor with @JsonCreator and @JsonProperty annotations + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public TxResult( + @JsonProperty("transactionId") String transactionId, @JsonProperty("status") String status) { + this.transactionId = transactionId; + this.status = status; + } + + @JsonProperty("transactionId") + public String getTransactionId() { + return transactionId; + } + + @JsonProperty("status") + public String getStatus() { + return status; + } + + @Override + public String toString() { + return String.format("InitResult{transactionId='%s', status='%s'}", transactionId, status); + } +} From 7700fa1e6b51a57e0620cedca47e550abd91c1ce Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Wed, 23 Oct 2024 10:03:24 -0700 Subject: [PATCH 07/15] workflow and update returns objects --- .../earlyreturn/EarlyReturnClient.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java index a21136d4..f2656922 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java @@ -47,18 +47,20 @@ private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { WorkflowOptions options = buildWorkflowOptions(); TransactionWorkflow workflow = client.newWorkflowStub(TransactionWorkflow.class, options); - try { - System.out.println("Starting workflow with UpdateWithStart"); + System.out.println("Starting workflow with UpdateWithStart"); - UpdateWithStartWorkflowOperation updateOp = - UpdateWithStartWorkflowOperation.newBuilder(workflow::returnInitResult) - .setWaitForStage(WorkflowUpdateStage.COMPLETED) // Wait for update to complete - .build(); + UpdateWithStartWorkflowOperation updateOp = + UpdateWithStartWorkflowOperation.newBuilder(workflow::returnInitResult) + .setWaitForStage(WorkflowUpdateStage.COMPLETED) // Wait for update to complete + .build(); + TxResult updateResult = null; + try { WorkflowUpdateHandle updateHandle = WorkflowClient.updateWithStart(workflow::processTransaction, tx, updateOp); - TxResult updateResult = updateHandle.getResultAsync().get(); + updateResult = updateHandle.getResultAsync().get(); + System.out.println( "Workflow initialized with result: " + updateResult.getStatus() @@ -66,9 +68,13 @@ private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { + updateResult.getTransactionId() + ")"); - String result = WorkflowStub.fromTyped(workflow).getResult(String.class); - System.out.println("Workflow completed with result: " + result); - + TxResult result = WorkflowStub.fromTyped(workflow).getResult(TxResult.class); + System.out.println( + "Workflow completed with result: " + + result.getStatus() + + " (transactionId: " + + result.getTransactionId() + + ")"); } catch (Exception e) { System.err.println("Transaction initialization failed: " + e.getMessage()); } From 463c2c7d36c427c5e0dba81ec267f48d842d0458 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Fri, 25 Oct 2024 07:46:20 -0700 Subject: [PATCH 08/15] immutable transaction class --- .../earlyreturn/EarlyReturnClient.java | 12 ++-- .../samples/earlyreturn/Transaction.java | 30 ++------- .../earlyreturn/TransactionActivities.java | 4 +- .../TransactionActivitiesImpl.java | 20 ++++-- .../earlyreturn/TransactionRequest.java | 61 +++++++++++++++++++ .../earlyreturn/TransactionWorkflow.java | 2 +- .../earlyreturn/TransactionWorkflowImpl.java | 10 +-- 7 files changed, 94 insertions(+), 45 deletions(-) create mode 100644 core/src/main/java/io/temporal/samples/earlyreturn/TransactionRequest.java diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java index f2656922..d87e2fda 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java @@ -31,7 +31,7 @@ public static void main(String[] args) { runWorkflowWithUpdateWithStart(client); } - // Setup the WorkflowClient + // Set up the WorkflowClient public static WorkflowClient setupWorkflowClient() { WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); return WorkflowClient.newInstance(service); @@ -39,10 +39,10 @@ public static WorkflowClient setupWorkflowClient() { // Run workflow using 'updateWithStart' private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { - Transaction tx = - new Transaction( - "", "Bob", "Alice", - 1000); // Change this amount to a negative number to have initTransaction fail + TransactionRequest txRequest = + new TransactionRequest( + "Bob", "Alice", + -1000); // Change this amount to a negative number to have initTransaction fail WorkflowOptions options = buildWorkflowOptions(); TransactionWorkflow workflow = client.newWorkflowStub(TransactionWorkflow.class, options); @@ -57,7 +57,7 @@ private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { TxResult updateResult = null; try { WorkflowUpdateHandle updateHandle = - WorkflowClient.updateWithStart(workflow::processTransaction, tx, updateOp); + WorkflowClient.updateWithStart(workflow::processTransaction, txRequest, updateOp); updateResult = updateHandle.getResultAsync().get(); diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java b/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java index 54e1adbf..0f910b4f 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/Transaction.java @@ -22,14 +22,11 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -public class Transaction { - private String id; // Mutable field - private String sourceAccount; - private String targetAccount; - private int amount; - - // No-arg constructor for serialization frameworks - public Transaction() {} +public final class Transaction { + private final String id; + private final String sourceAccount; + private final String targetAccount; + private final int amount; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public Transaction( @@ -43,43 +40,26 @@ public Transaction( this.amount = amount; } - // Getters and Setters for each field @JsonProperty("id") public String getId() { return id; } - public void setId(String id) { - this.id = id; - } - @JsonProperty("sourceAccount") public String getSourceAccount() { return sourceAccount; } - public void setSourceAccount(String sourceAccount) { - this.sourceAccount = sourceAccount; - } - @JsonProperty("targetAccount") public String getTargetAccount() { return targetAccount; } - public void setTargetAccount(String targetAccount) { - this.targetAccount = targetAccount; - } - @JsonProperty("amount") public int getAmount() { return amount; } - public void setAmount(int amount) { - this.amount = amount; - } - @Override public String toString() { return String.format( diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java index 156dece5..84ee3b76 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java @@ -25,10 +25,10 @@ @ActivityInterface public interface TransactionActivities { @ActivityMethod - Transaction initTransaction(Transaction tx); + Transaction initTransaction(TransactionRequest tx); @ActivityMethod - void cancelTransaction(Transaction tx); + void cancelTransaction(TransactionRequest txRequest); @ActivityMethod void completeTransaction(Transaction tx); diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java index 54830c56..6befc165 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java @@ -24,23 +24,31 @@ public class TransactionActivitiesImpl implements TransactionActivities { @Override - public Transaction initTransaction(Transaction tx) { + public Transaction initTransaction(TransactionRequest txRequest) { System.out.println("Initializing transaction"); sleep(500); - if (tx.getAmount() <= 0) { - System.out.println("Invalid amount: " + tx.getAmount()); + if (txRequest.getAmount() <= 0) { + System.out.println("Invalid amount: " + txRequest.getAmount()); throw ApplicationFailure.newNonRetryableFailure( "Non-retryable Activity Failure: Invalid Amount", "InvalidAmount"); } - tx.setId("TXID" + String.format("%010d", (long) (Math.random() * 1_000_000_0000L))); + // Simulate transaction ID generation + String txId = "TXID" + String.format("%010d", (long) (Math.random() * 1_000_000_0000L)); + Transaction tx = + new Transaction( + txId, + txRequest.getSourceAccount(), + txRequest.getTargetAccount(), + txRequest.getAmount()); + sleep(500); return tx; } @Override - public void cancelTransaction(Transaction tx) { + public void cancelTransaction(TransactionRequest txRequest) { System.out.println("Cancelling transaction"); - sleep(2000); + sleep(300); System.out.println("Transaction cancelled"); } diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionRequest.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionRequest.java new file mode 100644 index 00000000..c174a63f --- /dev/null +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionRequest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package io.temporal.samples.earlyreturn; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public final class TransactionRequest { + private final String sourceAccount; + private final String targetAccount; + private final int amount; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public TransactionRequest( + @JsonProperty("sourceAccount") String sourceAccount, + @JsonProperty("targetAccount") String targetAccount, + @JsonProperty("amount") int amount) { + this.sourceAccount = sourceAccount; + this.targetAccount = targetAccount; + this.amount = amount; + } + + @JsonProperty("sourceAccount") + public String getSourceAccount() { + return sourceAccount; + } + + @JsonProperty("targetAccount") + public String getTargetAccount() { + return targetAccount; + } + + @JsonProperty("amount") + public int getAmount() { + return amount; + } + + @Override + public String toString() { + return String.format( + "TransactionRequest{sourceAccount='%s', targetAccount='%s', amount=%d}", + sourceAccount, targetAccount, amount); + } +} diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java index 3e2d76ee..2d1ef6f5 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflow.java @@ -26,7 +26,7 @@ @WorkflowInterface public interface TransactionWorkflow { @WorkflowMethod - TxResult processTransaction(Transaction tx); + TxResult processTransaction(TransactionRequest txRequest); @UpdateMethod(name = "early-return") TxResult returnInitResult(); diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java index 750f6882..8af96d58 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java @@ -37,10 +37,9 @@ public class TransactionWorkflowImpl implements TransactionWorkflow { private Exception initError = null; @Override - public TxResult processTransaction(Transaction txInput) { - this.tx = txInput; + public TxResult processTransaction(TransactionRequest txRequest) { try { - this.tx = activities.initTransaction(this.tx); + this.tx = activities.initTransaction(txRequest); } catch (Exception e) { initError = e; } finally { @@ -48,8 +47,9 @@ public TxResult processTransaction(Transaction txInput) { } if (initError != null) { - activities.cancelTransaction(this.tx); - return new TxResult(this.tx.getId(), "Transaction cancelled."); + // If initialization failed, cancel the transaction + activities.cancelTransaction(txRequest); + return new TxResult("", "Transaction cancelled."); } else { activities.completeTransaction(this.tx); return new TxResult(this.tx.getId(), "Transaction completed successfully."); From 78bd0c2a06c818251e4fec5703ffce0dce3539a0 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Fri, 25 Oct 2024 08:46:17 -0700 Subject: [PATCH 09/15] Early Return UpdateWithStart unit test --- .../earlyreturn/TransactionWorkflowTest.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java diff --git a/core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java b/core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java new file mode 100644 index 00000000..e37c1452 --- /dev/null +++ b/core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package io.temporal.samples.earlyreturn; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import io.temporal.client.*; +import io.temporal.failure.ActivityFailure; +import io.temporal.failure.ApplicationFailure; +import io.temporal.testing.TestWorkflowRule; +import java.time.Duration; +import java.util.UUID; +import org.junit.Rule; +import org.junit.Test; + +public class TransactionWorkflowTest { + + private static final String SOURCE_ACCOUNT = "Bob"; + private static final String TARGET_ACCOUNT = "Alice"; + private static final String TEST_TRANSACTION_ID = "test-id-123"; + private static final int VALID_AMOUNT = 1000; + private static final int INVALID_AMOUNT = -1000; + + @Rule + public TestWorkflowRule testWorkflowRule = + TestWorkflowRule.newBuilder() + .setWorkflowTypes(TransactionWorkflowImpl.class) + .setDoNotStart(true) + .build(); + + @Test + public void testUpdateWithStartValidAmount() throws Exception { + // Mock activities + TransactionActivities activities = + mock(TransactionActivities.class, withSettings().withoutAnnotations()); + when(activities.initTransaction(any())) + .thenReturn( + new Transaction(TEST_TRANSACTION_ID, SOURCE_ACCOUNT, TARGET_ACCOUNT, VALID_AMOUNT)); + + testWorkflowRule.getWorker().registerActivitiesImplementations(activities); + testWorkflowRule.getTestEnvironment().start(); + + // Create workflow stub + WorkflowClient workflowClient = testWorkflowRule.getWorkflowClient(); + TransactionWorkflow workflow = + workflowClient.newWorkflowStub( + TransactionWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + + // Create update operation + UpdateWithStartWorkflowOperation updateOp = + UpdateWithStartWorkflowOperation.newBuilder(workflow::returnInitResult) + .setWaitForStage(WorkflowUpdateStage.COMPLETED) + .build(); + + // Execute UpdateWithStart + WorkflowUpdateHandle handle = + WorkflowClient.updateWithStart( + workflow::processTransaction, + new TransactionRequest(SOURCE_ACCOUNT, TARGET_ACCOUNT, VALID_AMOUNT), + updateOp); + + // Verify both update and final results + TxResult updateResult = handle.getResultAsync().get(); + assertEquals(TEST_TRANSACTION_ID, updateResult.getTransactionId()); + + TxResult finalResult = WorkflowStub.fromTyped(workflow).getResult(TxResult.class); + assertEquals("Transaction completed successfully.", finalResult.getStatus()); + + // Verify activities were called + verify(activities).initTransaction(any()); + verify(activities).completeTransaction(any()); + verifyNoMoreInteractions(activities); + } + + @Test + public void testUpdateWithStartInvalidAmount() throws Exception { + // Mock activities + TransactionActivities activities = + mock(TransactionActivities.class, withSettings().withoutAnnotations()); + when(activities.initTransaction(any())) + .thenThrow( + ApplicationFailure.newNonRetryableFailure( + "Non-retryable Activity Failure: Invalid Amount", "InvalidAmount")); + doNothing().when(activities).cancelTransaction(any()); + + testWorkflowRule.getWorker().registerActivitiesImplementations(activities); + testWorkflowRule.getTestEnvironment().start(); + + // Create workflow stub with explicit ID + WorkflowClient workflowClient = testWorkflowRule.getWorkflowClient(); + String workflowId = "test-workflow-" + UUID.randomUUID(); + WorkflowOptions options = + WorkflowOptions.newBuilder() + .setTaskQueue(testWorkflowRule.getTaskQueue()) + .setWorkflowId(workflowId) + .build(); + + TransactionWorkflow workflow = + workflowClient.newWorkflowStub(TransactionWorkflow.class, options); + + // Create update operation + UpdateWithStartWorkflowOperation updateOp = + UpdateWithStartWorkflowOperation.newBuilder(workflow::returnInitResult) + .setWaitForStage(WorkflowUpdateStage.COMPLETED) + .build(); + + // Execute UpdateWithStart and expect the exception + WorkflowServiceException exception = + assertThrows( + WorkflowServiceException.class, + () -> + WorkflowClient.updateWithStart( + workflow::processTransaction, + new TransactionRequest(SOURCE_ACCOUNT, TARGET_ACCOUNT, INVALID_AMOUNT), + updateOp)); + + // Verify the exception chain + assertTrue(exception.getCause() instanceof WorkflowUpdateException); + assertTrue(exception.getCause().getCause() instanceof ActivityFailure); + ApplicationFailure appFailure = (ApplicationFailure) exception.getCause().getCause().getCause(); + assertEquals("InvalidAmount", appFailure.getType()); + assertTrue(appFailure.getMessage().contains("Invalid Amount")); + + // Let workflow process error handling + testWorkflowRule.getTestEnvironment().sleep(Duration.ofSeconds(1)); + + // Create a new stub to get the result + TransactionWorkflow workflowById = + workflowClient.newWorkflowStub(TransactionWorkflow.class, workflowId); + TxResult finalResult = WorkflowStub.fromTyped(workflowById).getResult(TxResult.class); + assertEquals("", finalResult.getTransactionId()); + assertEquals("Transaction cancelled.", finalResult.getStatus()); + + // Verify activities were called in correct order + verify(activities).initTransaction(any()); + verify(activities).cancelTransaction(any()); + verifyNoMoreInteractions(activities); + } +} From b8513d92bff8f2338c64257cb75fd851e1a2ad41 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Fri, 25 Oct 2024 09:34:20 -0700 Subject: [PATCH 10/15] Update core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java Co-authored-by: Stephan Behnke --- .../io/temporal/samples/earlyreturn/TransactionActivities.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java index 84ee3b76..8dd8799c 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java @@ -25,7 +25,7 @@ @ActivityInterface public interface TransactionActivities { @ActivityMethod - Transaction initTransaction(TransactionRequest tx); + Transaction initTransaction(TransactionRequest txRequest); @ActivityMethod void cancelTransaction(TransactionRequest txRequest); From 9037387251638ae11c57f8f10c76e7607a1273a8 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Fri, 25 Oct 2024 10:22:16 -0700 Subject: [PATCH 11/15] remove sleep from test --- .../temporal/samples/earlyreturn/TransactionWorkflowTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java b/core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java index e37c1452..ab92cb68 100644 --- a/core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java +++ b/core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java @@ -27,7 +27,6 @@ import io.temporal.failure.ActivityFailure; import io.temporal.failure.ApplicationFailure; import io.temporal.testing.TestWorkflowRule; -import java.time.Duration; import java.util.UUID; import org.junit.Rule; import org.junit.Test; @@ -141,9 +140,6 @@ public void testUpdateWithStartInvalidAmount() throws Exception { assertEquals("InvalidAmount", appFailure.getType()); assertTrue(appFailure.getMessage().contains("Invalid Amount")); - // Let workflow process error handling - testWorkflowRule.getTestEnvironment().sleep(Duration.ofSeconds(1)); - // Create a new stub to get the result TransactionWorkflow workflowById = workflowClient.newWorkflowStub(TransactionWorkflow.class, workflowId); From cb93bfb0d4dd18eff38a1acfc2017ea7339b707d Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Fri, 25 Oct 2024 13:35:12 -0700 Subject: [PATCH 12/15] Minting has its own activity --- .../earlyreturn/EarlyReturnClient.java | 2 +- .../earlyreturn/TransactionActivities.java | 7 +++-- .../TransactionActivitiesImpl.java | 29 ++++++++++--------- .../earlyreturn/TransactionWorkflowImpl.java | 6 ++-- .../earlyreturn/TransactionWorkflowTest.java | 4 ++- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java index d87e2fda..6ccb11da 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/EarlyReturnClient.java @@ -42,7 +42,7 @@ private static void runWorkflowWithUpdateWithStart(WorkflowClient client) { TransactionRequest txRequest = new TransactionRequest( "Bob", "Alice", - -1000); // Change this amount to a negative number to have initTransaction fail + 1000); // Change this amount to a negative number to have initTransaction fail WorkflowOptions options = buildWorkflowOptions(); TransactionWorkflow workflow = client.newWorkflowStub(TransactionWorkflow.class, options); diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java index 8dd8799c..8f3e866f 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivities.java @@ -25,10 +25,13 @@ @ActivityInterface public interface TransactionActivities { @ActivityMethod - Transaction initTransaction(TransactionRequest txRequest); + Transaction mintTransactionId(TransactionRequest txRequest); @ActivityMethod - void cancelTransaction(TransactionRequest txRequest); + Transaction initTransaction(Transaction tx); + + @ActivityMethod + void cancelTransaction(Transaction tx); @ActivityMethod void completeTransaction(Transaction tx); diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java index 6befc165..52dc6180 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionActivitiesImpl.java @@ -24,29 +24,32 @@ public class TransactionActivitiesImpl implements TransactionActivities { @Override - public Transaction initTransaction(TransactionRequest txRequest) { + public Transaction mintTransactionId(TransactionRequest request) { + System.out.println("Minting transaction ID"); + // Simulate transaction ID generation + String txId = "TXID" + String.format("%010d", (long) (Math.random() * 1_000_000_0000L)); + sleep(100); + System.out.println("Transaction ID minted: " + txId); + return new Transaction( + txId, request.getSourceAccount(), request.getTargetAccount(), request.getAmount()); + } + + @Override + public Transaction initTransaction(Transaction tx) { System.out.println("Initializing transaction"); - sleep(500); - if (txRequest.getAmount() <= 0) { - System.out.println("Invalid amount: " + txRequest.getAmount()); + sleep(300); + if (tx.getAmount() <= 0) { + System.out.println("Invalid amount: " + tx.getAmount()); throw ApplicationFailure.newNonRetryableFailure( "Non-retryable Activity Failure: Invalid Amount", "InvalidAmount"); } - // Simulate transaction ID generation - String txId = "TXID" + String.format("%010d", (long) (Math.random() * 1_000_000_0000L)); - Transaction tx = - new Transaction( - txId, - txRequest.getSourceAccount(), - txRequest.getTargetAccount(), - txRequest.getAmount()); sleep(500); return tx; } @Override - public void cancelTransaction(TransactionRequest txRequest) { + public void cancelTransaction(Transaction tx) { System.out.println("Cancelling transaction"); sleep(300); System.out.println("Transaction cancelled"); diff --git a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java index 8af96d58..8f7c86d3 100644 --- a/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java +++ b/core/src/main/java/io/temporal/samples/earlyreturn/TransactionWorkflowImpl.java @@ -38,8 +38,10 @@ public class TransactionWorkflowImpl implements TransactionWorkflow { @Override public TxResult processTransaction(TransactionRequest txRequest) { + this.tx = activities.mintTransactionId(txRequest); + try { - this.tx = activities.initTransaction(txRequest); + this.tx = activities.initTransaction(this.tx); } catch (Exception e) { initError = e; } finally { @@ -48,7 +50,7 @@ public TxResult processTransaction(TransactionRequest txRequest) { if (initError != null) { // If initialization failed, cancel the transaction - activities.cancelTransaction(txRequest); + activities.cancelTransaction(this.tx); return new TxResult("", "Transaction cancelled."); } else { activities.completeTransaction(this.tx); diff --git a/core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java b/core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java index ab92cb68..5661d28a 100644 --- a/core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java +++ b/core/src/test/java/io/temporal/samples/earlyreturn/TransactionWorkflowTest.java @@ -85,7 +85,8 @@ public void testUpdateWithStartValidAmount() throws Exception { TxResult finalResult = WorkflowStub.fromTyped(workflow).getResult(TxResult.class); assertEquals("Transaction completed successfully.", finalResult.getStatus()); - // Verify activities were called + // Verify activities were calledgit + verify(activities).mintTransactionId(any()); verify(activities).initTransaction(any()); verify(activities).completeTransaction(any()); verifyNoMoreInteractions(activities); @@ -148,6 +149,7 @@ public void testUpdateWithStartInvalidAmount() throws Exception { assertEquals("Transaction cancelled.", finalResult.getStatus()); // Verify activities were called in correct order + verify(activities).mintTransactionId(any()); verify(activities).initTransaction(any()); verify(activities).cancelTransaction(any()); verifyNoMoreInteractions(activities); From f40e832bc90df9eb68618789c2562dcc5a6cf438 Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Fri, 18 Oct 2024 11:15:59 -0700 Subject: [PATCH 13/15] Fix nexus sample TLS settings (#688) --- core/src/main/java/io/temporal/samples/nexus/README.MD | 2 +- .../io/temporal/samples/nexus/options/ClientOptions.java | 5 ++--- .../java/io/temporal/samples/nexus/service/description.md | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/nexus/README.MD b/core/src/main/java/io/temporal/samples/nexus/README.MD index 2483a8b3..526d3d48 100644 --- a/core/src/main/java/io/temporal/samples/nexus/README.MD +++ b/core/src/main/java/io/temporal/samples/nexus/README.MD @@ -54,7 +54,7 @@ temporal operator nexus endpoint create \ --name my-nexus-endpoint-name \ --target-namespace my-target-namespace \ --target-task-queue my-handler-task-queue \ - --description-file ./service/description.md + --description-file ./core/src/main/java/io/temporal/samples/nexus/service/description.md ``` ## Getting started with a self-hosted service or Temporal Cloud diff --git a/core/src/main/java/io/temporal/samples/nexus/options/ClientOptions.java b/core/src/main/java/io/temporal/samples/nexus/options/ClientOptions.java index 49d0dde2..95e6d121 100644 --- a/core/src/main/java/io/temporal/samples/nexus/options/ClientOptions.java +++ b/core/src/main/java/io/temporal/samples/nexus/options/ClientOptions.java @@ -19,6 +19,7 @@ package io.temporal.samples.nexus.options; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.temporal.client.WorkflowClient; @@ -27,13 +28,11 @@ import io.temporal.serviceclient.WorkflowServiceStubsOptions; import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.util.Arrays; import javax.net.ssl.SSLException; import org.apache.commons.cli.*; public class ClientOptions { public static WorkflowClient getWorkflowClient(String[] args) { - System.out.println(Arrays.toString(args)); Options options = new Options(); Option targetHostOption = new Option("target-host", true, "Host:port for the Temporal service"); targetHostOption.setRequired(false); @@ -107,7 +106,7 @@ public static WorkflowClient getWorkflowClient(String[] args) { if (insecureSkipVerify) { sslContext.trustManager(InsecureTrustManagerFactory.INSTANCE); } - serviceStubOptionsBuilder.setSslContext(sslContext.build()); + serviceStubOptionsBuilder.setSslContext(GrpcSslContexts.configure(sslContext).build()); } catch (SSLException e) { throw new RuntimeException(e); } catch (FileNotFoundException e) { diff --git a/core/src/main/java/io/temporal/samples/nexus/service/description.md b/core/src/main/java/io/temporal/samples/nexus/service/description.md index 74eb4d88..98dc2708 100644 --- a/core/src/main/java/io/temporal/samples/nexus/service/description.md +++ b/core/src/main/java/io/temporal/samples/nexus/service/description.md @@ -1,8 +1,8 @@ Service Name: -my-hello-service +NexusService Operation Names: echo -say-hello +hello Input / Output arguments are in the following repository: https://github.com/temporalio/samples-java/core/src/main/java/io/temporal/samples/nexus/service/NexusService.java From ce6cc567b47919fbbec93fa7535a28770d37600e Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Mon, 21 Oct 2024 08:17:10 -0700 Subject: [PATCH 14/15] Add API key option to Nexus sample (#690) Add API key option to Nexus sample --- .../samples/nexus/options/ClientOptions.java | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/nexus/options/ClientOptions.java b/core/src/main/java/io/temporal/samples/nexus/options/ClientOptions.java index 95e6d121..7ba69930 100644 --- a/core/src/main/java/io/temporal/samples/nexus/options/ClientOptions.java +++ b/core/src/main/java/io/temporal/samples/nexus/options/ClientOptions.java @@ -19,9 +19,11 @@ package io.temporal.samples.nexus.options; +import io.grpc.Metadata; import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.grpc.stub.MetadataUtils; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; import io.temporal.serviceclient.WorkflowServiceStubs; @@ -47,14 +49,23 @@ public static WorkflowClient getWorkflowClient(String[] args) { serverRootCaOption.setRequired(false); options.addOption(serverRootCaOption); - Option clientCertOption = new Option("client-cert", true, "Optional path to client cert"); + Option clientCertOption = + new Option( + "client-cert", true, "Optional path to client cert, mutually exclusive with API key"); clientCertOption.setRequired(false); options.addOption(clientCertOption); - Option clientKeyOption = new Option("client-key", true, "Optional path to client key"); + Option clientKeyOption = + new Option( + "client-key", true, "Optional path to client key, mutually exclusive with API key"); clientKeyOption.setRequired(false); options.addOption(clientKeyOption); + Option apiKeyOption = + new Option("api-key", true, "Optional API key, mutually exclusive with cert/key"); + apiKeyOption.setRequired(false); + options.addOption(apiKeyOption); + Option serverNameOption = new Option( "server-name", true, "Server name to use for verifying the server's certificate"); @@ -89,9 +100,15 @@ public static WorkflowClient getWorkflowClient(String[] args) { String clientKey = cmd.getOptionValue("client-key", ""); String serverName = cmd.getOptionValue("server-name", ""); boolean insecureSkipVerify = cmd.hasOption("insecure-skip-verify"); + String apiKey = cmd.getOptionValue("api-key", ""); + // API key and client cert/key are mutually exclusive + if (!apiKey.isEmpty() && (!clientCert.isEmpty() || !clientKey.isEmpty())) { + throw new IllegalArgumentException("API key and client cert/key are mutually exclusive"); + } WorkflowServiceStubsOptions.Builder serviceStubOptionsBuilder = WorkflowServiceStubsOptions.newBuilder().setTarget(targetHost); + // Configure TLS if client cert and key are provided if (!clientCert.isEmpty() || !clientKey.isEmpty()) { if (clientCert.isEmpty() || clientKey.isEmpty()) { throw new IllegalArgumentException("Both client-cert and client-key must be provided"); @@ -112,10 +129,24 @@ public static WorkflowClient getWorkflowClient(String[] args) { } catch (FileNotFoundException e) { throw new RuntimeException(e); } + if (serverName != null && !serverName.isEmpty()) { + serviceStubOptionsBuilder.setChannelInitializer(c -> c.overrideAuthority(serverName)); + } } - if (serverName != null && !serverName.isEmpty()) { - serviceStubOptionsBuilder.setChannelInitializer(c -> c.overrideAuthority(serverName)); + // Configure API key if provided + if (!apiKey.isEmpty()) { + serviceStubOptionsBuilder.setEnableHttps(true); + serviceStubOptionsBuilder.addApiKey(() -> apiKey); + Metadata.Key TEMPORAL_NAMESPACE_HEADER_KEY = + Metadata.Key.of("temporal-namespace", Metadata.ASCII_STRING_MARSHALLER); + Metadata metadata = new Metadata(); + metadata.put(TEMPORAL_NAMESPACE_HEADER_KEY, namespace); + serviceStubOptionsBuilder.setChannelInitializer( + (channel) -> { + channel.intercept(MetadataUtils.newAttachHeadersInterceptor(metadata)); + }); } + WorkflowServiceStubs service = WorkflowServiceStubs.newServiceStubs(serviceStubOptionsBuilder.build()); return WorkflowClient.newInstance( From 8095cc78fd5fa5ba85d6a7a0ea7270d434c70150 Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Thu, 24 Oct 2024 15:26:01 -0700 Subject: [PATCH 15/15] Bump Java SDK to v1.26.1 (#695) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e9a3cae2..cdd85c16 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ subprojects { ext { otelVersion = '1.30.1' otelVersionAlpha = "${otelVersion}-alpha" - javaSDKVersion = '1.26.0' + javaSDKVersion = '1.26.1' camelVersion = '3.22.1' jarVersion = '1.0.0' }