-
Notifications
You must be signed in to change notification settings - Fork 764
Compensating Workflows Pattern #4665
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cicoyle
wants to merge
2
commits into
dapr:v1.16
Choose a base branch
from
cicoyle:feat-compensating-workflows
base: v1.16
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+189
−0
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1386,6 +1386,195 @@ func raiseEvent() { | |
|
||
External events don't have to be directly triggered by humans. They can also be triggered by other systems. For example, a workflow may need to pause and wait for a payment to be received. In this case, a payment system might publish an event to a pub/sub topic on receipt of a payment, and a listener on that topic can raise an event to the workflow using the raise event workflow API. | ||
|
||
## Compensation | ||
|
||
The compensation pattern provides a mechanism for rolling back or undoing operations that have already been executed when a workflow fails partway through. This pattern is particularly important for long-running workflows that span multiple microservices where traditional database transactions are not feasible. | ||
|
||
In distributed microservice architectures, you often need to coordinate operations across multiple services. When these operations cannot be wrapped in a single transaction, the compensation pattern provides a way to maintain consistency by defining compensating actions for each step in the workflow. | ||
|
||
The compensation pattern addresses several critical challenges: | ||
|
||
- **Distributed Transaction Management**: When a workflow spans multiple microservices, each with their own data stores, traditional ACID transactions are not possible. The compensation pattern provides transactional consistency by ensuring operations are either all completed successfully or all undone through compensation. | ||
- **Partial Failure Recovery**: If a workflow fails after some steps have completed successfully, the compensation pattern allows you to undo those completed steps gracefully. | ||
- **Business Process Integrity**: Ensures that business processes can be properly rolled back in case of failures, maintaining the integrity of your business operations. | ||
- **Long-Running Processes**: For workflows that may run for hours, days, or longer, traditional locking mechanisms are impractical. Compensation provides a way to handle failures in these scenarios. | ||
|
||
Common use cases for the compensation pattern include: | ||
|
||
- **E-commerce Order Processing**: Reserve inventory, charge payment, and ship orders. If shipping fails, you need to release the inventory and refund the payment. | ||
- **Financial Transactions**: In a money transfer, if crediting the destination account fails, you need to rollback the debit from the source account. | ||
- **Resource Provisioning**: When provisioning cloud resources across multiple providers, if one step fails, you need to clean up all previously provisioned resources. | ||
- **Multi-Step Business Processes**: Any business process that involves multiple irreversible steps that may need to be undone in case of later failures. | ||
|
||
Dapr Workflow provides support for the compensation pattern, allowing you to register compensation activities for each step and execute them in reverse order when needed. | ||
|
||
{{< tabs Java >}} | ||
|
||
{{% codetab %}} | ||
<!--java--> | ||
|
||
```java | ||
public class PaymentProcessingWorkflow implements Workflow { | ||
|
||
@Override | ||
public WorkflowStub create() { | ||
return ctx -> { | ||
ctx.getLogger().info("Starting Workflow: " + ctx.getName()); | ||
var orderId = ctx.getInput(String.class); | ||
List<String> compensations = new ArrayList<>(); | ||
|
||
try { | ||
// Step 1: Reserve inventory | ||
String reservationId = ctx.callActivity(ReserveInventoryActivity.class.getName(), orderId, String.class).await(); | ||
ctx.getLogger().info("Inventory reserved: {}", reservationId); | ||
compensations.add("ReleaseInventory"); | ||
|
||
// Step 2: Process payment | ||
String paymentId = ctx.callActivity(ProcessPaymentActivity.class.getName(), orderId, String.class).await(); | ||
ctx.getLogger().info("Payment processed: {}", paymentId); | ||
compensations.add("RefundPayment"); | ||
|
||
// Step 3: Ship order | ||
String shipmentId = ctx.callActivity(ShipOrderActivity.class.getName(), orderId, String.class).await(); | ||
ctx.getLogger().info("Order shipped: {}", shipmentId); | ||
compensations.add("CancelShipment"); | ||
|
||
// Step 4: Send confirmation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest to remove this activity. It's a bit confusing now if the SendConfirmation throws, then the rest is still compensated. |
||
ctx.callActivity(SendConfirmationActivity.class.getName(), orderId, Void.class).await(); | ||
ctx.getLogger().info("Confirmation sent for order: {}", orderId); | ||
|
||
ctx.complete("Order processed successfully: " + orderId); | ||
|
||
} catch (TaskFailedException e) { | ||
ctx.getLogger().error("Activity failed: {}", e.getMessage()); | ||
|
||
// Execute compensations in reverse order | ||
Collections.reverse(compensations); | ||
for (String compensation : compensations) { | ||
try { | ||
switch (compensation) { | ||
case "CancelShipment": | ||
String shipmentCancelResult = ctx.callActivity( | ||
CancelShipmentActivity.class.getName(), | ||
orderId, | ||
String.class).await(); | ||
ctx.getLogger().info("Shipment cancellation completed: {}", shipmentCancelResult); | ||
break; | ||
|
||
case "RefundPayment": | ||
String refundResult = ctx.callActivity( | ||
RefundPaymentActivity.class.getName(), | ||
orderId, | ||
String.class).await(); | ||
ctx.getLogger().info("Payment refund completed: {}", refundResult); | ||
break; | ||
|
||
case "ReleaseInventory": | ||
String releaseResult = ctx.callActivity( | ||
ReleaseInventoryActivity.class.getName(), | ||
orderId, | ||
String.class).await(); | ||
ctx.getLogger().info("Inventory release completed: {}", releaseResult); | ||
break; | ||
} | ||
} catch (TaskFailedException ex) { | ||
ctx.getLogger().error("Compensation activity failed: {}", ex.getMessage()); | ||
} | ||
} | ||
ctx.complete("Order processing failed, compensation applied"); | ||
} | ||
}; | ||
} | ||
} | ||
|
||
// Example activities | ||
class ReserveInventoryActivity implements WorkflowActivity { | ||
@Override | ||
public Object run(WorkflowActivityContext ctx) { | ||
String orderId = ctx.getInput(String.class); | ||
// Logic to reserve inventory | ||
String reservationId = "reservation_" + orderId; | ||
System.out.println("Reserved inventory for order: " + orderId); | ||
return reservationId; | ||
} | ||
} | ||
|
||
class ReleaseInventoryActivity implements WorkflowActivity { | ||
@Override | ||
public Object run(WorkflowActivityContext ctx) { | ||
String reservationId = ctx.getInput(String.class); | ||
// Logic to release inventory reservation | ||
System.out.println("Released inventory reservation: " + reservationId); | ||
return "Released: " + reservationId; | ||
} | ||
} | ||
|
||
class ProcessPaymentActivity implements WorkflowActivity { | ||
@Override | ||
public Object run(WorkflowActivityContext ctx) { | ||
String orderId = ctx.getInput(String.class); | ||
// Logic to process payment | ||
String paymentId = "payment_" + orderId; | ||
System.out.println("Processed payment for order: " + orderId); | ||
return paymentId; | ||
} | ||
} | ||
|
||
class RefundPaymentActivity implements WorkflowActivity { | ||
@Override | ||
public Object run(WorkflowActivityContext ctx) { | ||
String paymentId = ctx.getInput(String.class); | ||
// Logic to refund payment | ||
System.out.println("Refunded payment: " + paymentId); | ||
return "Refunded: " + paymentId; | ||
} | ||
} | ||
|
||
class ShipOrderActivity implements WorkflowActivity { | ||
@Override | ||
public Object run(WorkflowActivityContext ctx) { | ||
String orderId = ctx.getInput(String.class); | ||
// Logic to ship order | ||
String shipmentId = "shipment_" + orderId; | ||
System.out.println("Shipped order: " + orderId); | ||
return shipmentId; | ||
} | ||
} | ||
|
||
class CancelShipmentActivity implements WorkflowActivity { | ||
@Override | ||
public Object run(WorkflowActivityContext ctx) { | ||
String shipmentId = ctx.getInput(String.class); | ||
// Logic to cancel shipment | ||
System.out.println("Canceled shipment: " + shipmentId); | ||
return "Canceled: " + shipmentId; | ||
} | ||
} | ||
|
||
class SendConfirmationActivity implements WorkflowActivity { | ||
@Override | ||
public Object run(WorkflowActivityContext ctx) { | ||
String orderId = ctx.getInput(String.class); | ||
// Logic to send confirmation | ||
System.out.println("Sent confirmation for order: " + orderId); | ||
return null; | ||
} | ||
} | ||
``` | ||
|
||
{{% /codetab %}} | ||
|
||
{{< /tabs >}} | ||
|
||
The key benefits of using Dapr Workflow's compensation pattern include: | ||
|
||
- **Compensation Control**: You have full control over when and how compensation activities are executed. | ||
- **Flexible Configuration**: You can implement custom logic for determining which compensations to run. | ||
- **Error Handling**: Handle compensation failures according to your specific business requirements. | ||
- **Simple Implementation**: No additional framework dependencies - just standard workflow activities and exception handling. | ||
|
||
The compensation pattern ensures that your distributed workflows can maintain consistency and recover gracefully from failures, making it an essential tool for building reliable microservice architectures. | ||
|
||
## Next steps | ||
|
||
{{< button text="Workflow architecture >>" page="workflow-architecture.md" >}} | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.