From 7ff2f0f608d2102d7123fc96d46f0bb576b34cf8 Mon Sep 17 00:00:00 2001 From: Marc Duiker Date: Fri, 11 Jul 2025 17:28:31 +0200 Subject: [PATCH] Add challenges ant tips Signed-off-by: Marc Duiker Signed-off-by: salaboy --- .../DeterministicWorkflow.java | 44 +++++++++++++++++ .../challenges-tips/IdempotentActivity.java | 13 +++++ .../challenges-tips/PayloadSizeWorkflow.java | 41 ++++++++++++++++ .../workflow/java/challenges-tips/README.md | 10 ++++ .../challenges-tips/VersioningWorkflow.java | 48 +++++++++++++++++++ 5 files changed, 156 insertions(+) create mode 100644 tutorials/workflow/java/challenges-tips/DeterministicWorkflow.java create mode 100644 tutorials/workflow/java/challenges-tips/IdempotentActivity.java create mode 100644 tutorials/workflow/java/challenges-tips/PayloadSizeWorkflow.java create mode 100644 tutorials/workflow/java/challenges-tips/README.md create mode 100644 tutorials/workflow/java/challenges-tips/VersioningWorkflow.java diff --git a/tutorials/workflow/java/challenges-tips/DeterministicWorkflow.java b/tutorials/workflow/java/challenges-tips/DeterministicWorkflow.java new file mode 100644 index 00000000..5d39c5f4 --- /dev/null +++ b/tutorials/workflow/java/challenges-tips/DeterministicWorkflow.java @@ -0,0 +1,44 @@ +@Component +public class NonDeterministicWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + var orderItem = ctx.getInput(String.class); + + // Do not use non-deterministic operations in a workflow! + // These operations will create a new value every time the + // workflow is replayed. + var orderId = UUID.randomUUID();; + var orderDate = Instant.now(); + + String idResult = ctx.callActivity(SubmitIdActivity.class.getName(), orderId, String.class).await(); + String dateResult = ctx.callActivity(SubmitDateActivity.class.getName(), orderDate, String.class).await(); + + ctx.complete(idResult); + }; + } +} + +@Component +public class DeterministicWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + var orderItem = ctx.getInput(String.class); + + // Do use these deterministic methods and properties on the WorkflowContext instead. + // These operations create the same value when the workflow is replayed. + var orderId = ctx.newUuid();; + var orderDate = ctx.getCurrentInstant(); + + String idResult = ctx.callActivity(SubmitIdActivity.class.getName(), orderId, String.class).await(); + String dateResult = ctx.callActivity(SubmitDateActivity.class.getName(), orderDate, String.class).await(); + + ctx.complete(idResult); + }; + } +} \ No newline at end of file diff --git a/tutorials/workflow/java/challenges-tips/IdempotentActivity.java b/tutorials/workflow/java/challenges-tips/IdempotentActivity.java new file mode 100644 index 00000000..1a58ce66 --- /dev/null +++ b/tutorials/workflow/java/challenges-tips/IdempotentActivity.java @@ -0,0 +1,13 @@ +@Component +public class IdempotentActivity implements WorkflowActivity { + + @Override + public Object run(WorkflowActivityContext ctx) { + // Beware of non-idempotent operations in an activity. + // Dapr Workflow guarantees at-least-once execution of activities, so activities might be executed more than once + // in case an activity is not ran to completion successfully. + // For instance, can you insert a record to a database twice without side effects? + // var insertSql = $"INSERT INTO Orders (Id, Description, UnitPrice, Quantity) VALUES ('{orderItem.Id}', '{orderItem.Description}', {orderItem.UnitPrice}, {orderItem.Quantity})"; + // It's best to perform a check if an record already exists before inserting it. + } +} diff --git a/tutorials/workflow/java/challenges-tips/PayloadSizeWorkflow.java b/tutorials/workflow/java/challenges-tips/PayloadSizeWorkflow.java new file mode 100644 index 00000000..07d0de14 --- /dev/null +++ b/tutorials/workflow/java/challenges-tips/PayloadSizeWorkflow.java @@ -0,0 +1,41 @@ +@Component +public class LargePayloadSizeWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + var docId = ctx.getInput(String.class); + + // Do not pass large payloads between activities. + // They are stored in the Dapr state store twice, one as output argument + // for GetDocument, and once as input argument for UpdateDocument. + var document = ctx.callActivity(GetDocument.class.getName(), docId, LargeDocument.class).await(); + var updatedDocument = ctx.callActivity(UpdateDocument.class.getName(), document, LargeDocument.class).await(); + + // More activities to process the updated document... + + ctx.complete(updatedDocument); + }; + } +} + +@Component +public class SmallPayloadSizeWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + var docId = ctx.getInput(String.class); + + // Do pass small payloads between activities, preferably IDs only, or objects that are quick to (de)serialize in large volumes. + // Combine multiple actions, such as document retrieval and update, into a single activity. + docId = ctx.callActivity(GetAndUpdateDocument.class.getName(), docId, String.class).await(); + + // More activities to process the updated document... + + ctx.complete(docId); + }; + } +} \ No newline at end of file diff --git a/tutorials/workflow/java/challenges-tips/README.md b/tutorials/workflow/java/challenges-tips/README.md new file mode 100644 index 00000000..e330849d --- /dev/null +++ b/tutorials/workflow/java/challenges-tips/README.md @@ -0,0 +1,10 @@ +# Workflow Challenges & Tips + +Workflow systems are very powerful tools but also have their challenges & limitations as described in the [Dapr docs](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-features-concepts/#limitations). + +This section provides some tips with code snippets to understand the limitations and get the most out of the Dapr Workflow API. Read through the following examples to learn best practices to develop Dapr workflows. + +- [Deterministic workflows](DeterministicWorkflow.java) +- [Idempotent activities](IdempotentActivity.java) +- [Versioning workflows](VersioningWorkflow.java) +- [Workflow & activity payload size](PayloadSizeWorkflow.java) diff --git a/tutorials/workflow/java/challenges-tips/VersioningWorkflow.java b/tutorials/workflow/java/challenges-tips/VersioningWorkflow.java new file mode 100644 index 00000000..9fd4afce --- /dev/null +++ b/tutorials/workflow/java/challenges-tips/VersioningWorkflow.java @@ -0,0 +1,48 @@ +/* + This is the initial version of the workflow. + Note that the input argument for both activities is the orderItem (string). +*/ +@Component +public class VersioningWorkflow1 implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + var orderItem = ctx.getInput(String.class); + + var resultA = ctx.callActivity(ActivityA.class.getName(), orderItem, Integer.class).await(); + var resultB = ctx.callActivity(ActivityB.class.getName(), orderItem, Integer.class).await(); + + ctx.complete(resultA + resultB); + }; + } +} + +/* + This is the updated version of the workflow. + The input for ActivityB has changed from orderItem (string) to resultA (int). + If there are in-flight workflow instances that were started with the previous version + of this workflow, these will fail when the new version of the workflow is deployed + and the workflow name remains the same, since the runtime parameters do not match with the persisted state. + It is recommended to version workflows by creating a new workflow class with a new name: + {workflowname}1 -> {workflowname}2 + Try to avoid making breaking changes in perpetual workflows (that use the `ContinueAsNew` method) + since these are difficult to replace with a new version. +*/ +@Component +public class VersioningWorkflow1 implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + var orderItem = ctx.getInput(String.class); + + var resultA = ctx.callActivity(ActivityA.class.getName(), orderItem, Integer.class).await(); + var resultB = ctx.callActivity(ActivityB.class.getName(), resultA, Integer.class).await(); + + ctx.complete(resultA + resultB); + }; + } +} \ No newline at end of file