-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
316 additions
and
188 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
This file contains 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 |
---|---|---|
@@ -1,16 +1,78 @@ | ||
# SAGAs / Compensations | ||
|
||
An example of a trip reservation workflow, using the SAGAs pattern to | ||
undo previous steps in case of an error. | ||
This is a minimal version of the holiday reservation demo in the | ||
[Restate Holiday Repository](https://github.com/restatedev/restate-holiday). | ||
|
||
Durable Execution's guarantee to run code to the end in the presence | ||
of failures, and to deterministically recover previous steps from the | ||
journal, makes SAGAs easy. | ||
Every step pushes a compensation action (an undo operation) to a stack. | ||
in the case of an error, those operations are run. | ||
|
||
The main requirement is that steps are implemented as journalled | ||
operations, like `ctx.run()` (e.g., direct calls to 3rd party APIs) or | ||
RPCs to other Restate-backed services (e.g., `FlightsClient.fromContex(ctx)`). | ||
# Microservices: Sagas | ||
|
||
An example of a trip reservation workflow, using the saga pattern to undo previous steps in case of an error. | ||
|
||
Durable Execution's guarantee to run code to the end in the presence of failures, and to deterministically recover previous steps from the journal, makes sagas easy. | ||
Every step pushes a compensation action (an undo operation) to a stack. In the case of an error, those operations are run. | ||
|
||
The main requirement is that steps are implemented as journaled operations, like `ctx.run()` or RPC/messaging. | ||
|
||
## Adding compensations | ||
The example shows two ways you can implement the compensation, depending on the characteristics of the API/system you interact with. | ||
|
||
The flight and car reservations work in a two-phase commit way, where you first create a reservation, get a reservation ID back, and then confirm or cancel the reservation with its ID. | ||
In this case, you need to add the compensation to the list after creating the reservation, because you need the reservation ID to cancel it. | ||
If the failure happens while making the reservation, you can be sure that it never takes effect, because you didn't confirm it. | ||
|
||
The payment on the other hand uses a client generated idempotency key. | ||
The payment goes through in one shot (single API call). | ||
If we receive an error, we might not be sure if this occurred before or after the payment took effect. | ||
Therefore, we need to add the compensation to the list before the payment is made. | ||
If a failure happens during the payment, the compensation will run. | ||
The downstream API then uses the idempotency key to check if the payment went through, and whether it needs to be refunded. | ||
|
||
Note that the compensating action needs to be idempotent. | ||
|
||
## Running the examples | ||
|
||
1. [Start the Restate Server](https://docs.restate.dev/develop/local_dev) in a separate shell: | ||
`restate-server` | ||
|
||
2. Start the service: `./gradlew run` | ||
|
||
3. Register the example at Restate server by calling | ||
`restate -y deployment register localhost:9080` | ||
|
||
## Demo scenario | ||
|
||
Have a look at the logs to see how the compensations run in case of a terminal error. | ||
|
||
Start the workflow: | ||
```shell | ||
curl -X POST localhost:8080/BookingWorkflow/trip12883/run -H 'content-type: application/json' -d '{ | ||
"flights": { | ||
"flightId": "12345", | ||
"passengerName": "John Doe" | ||
}, | ||
"car": { | ||
"pickupLocation": "Airport", | ||
"rentalDate": "2024-12-16" | ||
}, | ||
"paymentInfo": { | ||
"cardNumber": "4111111111111111", | ||
"amount": 1500 | ||
} | ||
}' | ||
``` | ||
|
||
Have a look at the logs to see the cancellations of the flight and car booking in case of a terminal error: | ||
```shell | ||
2024-12-18 11:35:48 INFO [BookingWorkflow/run][inv_12ogPnVefk1c3clc9wNhEa4pMxxRh9IRyx] dev.restate.sdk.core.InvocationStateMachine - Start invocation | ||
2024-12-18 11:35:49 INFO [Flights/reserve][inv_1ccelXW8IxuW6QpLWQu9ykt5aMAqRTl7pL] dev.restate.sdk.core.InvocationStateMachine - Start invocation | ||
2024-12-18 11:35:49 INFO [Flights/reserve][inv_1ccelXW8IxuW6QpLWQu9ykt5aMAqRTl7pL] dev.restate.patterns.activities.Flights - Flight reservation created with id: 35ab7c68-6f32-48f6-adb9-a2a74076f4df | ||
2024-12-18 11:35:49 INFO [Flights/reserve][inv_1ccelXW8IxuW6QpLWQu9ykt5aMAqRTl7pL] dev.restate.sdk.core.InvocationStateMachine - End invocation | ||
2024-12-18 11:35:49 INFO [CarRentals/reserve][inv_13cgaqr4XecK2ztj72BfVPuscdL1SJwMCZ] dev.restate.sdk.core.InvocationStateMachine - Start invocation | ||
2024-12-18 11:35:49 INFO [CarRentals/reserve][inv_13cgaqr4XecK2ztj72BfVPuscdL1SJwMCZ] dev.restate.patterns.activities.CarRentals - Car rental reservation created with id: c103022e-9dda-4a34-a6ef-0c95d2911b2c | ||
2024-12-18 11:35:49 INFO [CarRentals/reserve][inv_13cgaqr4XecK2ztj72BfVPuscdL1SJwMCZ] dev.restate.sdk.core.InvocationStateMachine - End invocation | ||
2024-12-18 11:35:49 ERROR [BookingWorkflow/run][inv_12ogPnVefk1c3clc9wNhEa4pMxxRh9IRyx] dev.restate.patterns.clients.PaymentClient - This payment should never be accepted! Aborting booking. | ||
2024-12-18 11:35:49 INFO [Flights/cancel][inv_19STR0U1v5Xo5W2UsYS3rhZEI02VGDVJM5] dev.restate.sdk.core.InvocationStateMachine - Start invocation | ||
2024-12-18 11:35:49 INFO [Flights/cancel][inv_19STR0U1v5Xo5W2UsYS3rhZEI02VGDVJM5] dev.restate.patterns.activities.Flights - Flight reservation cancelled with id: 35ab7c68-6f32-48f6-adb9-a2a74076f4df | ||
2024-12-18 11:35:49 INFO [Flights/cancel][inv_19STR0U1v5Xo5W2UsYS3rhZEI02VGDVJM5] dev.restate.sdk.core.InvocationStateMachine - End invocation | ||
2024-12-18 11:35:49 INFO [CarRentals/cancel][inv_14PS98BWOeNn1zw3yn2RqJ0wSp7V5sEJMd] dev.restate.sdk.core.InvocationStateMachine - Start invocation | ||
2024-12-18 11:35:49 INFO [CarRentals/cancel][inv_14PS98BWOeNn1zw3yn2RqJ0wSp7V5sEJMd] dev.restate.patterns.activities.CarRentals - Car rental reservation cancelled with id: c103022e-9dda-4a34-a6ef-0c95d2911b2c | ||
2024-12-18 11:35:49 INFO [CarRentals/cancel][inv_14PS98BWOeNn1zw3yn2RqJ0wSp7V5sEJMd] dev.restate.sdk.core.InvocationStateMachine - End invocation | ||
2024-12-18 11:35:49 INFO [BookingWorkflow/run][inv_12ogPnVefk1c3clc9wNhEa4pMxxRh9IRyx] dev.restate.patterns.clients.PaymentClient - Refunding payment with id: 1a640cda-bd5f-9751-b6b9-274817549b58 | ||
2024-12-18 11:35:49 WARN [BookingWorkflow/run][inv_12ogPnVefk1c3clc9wNhEa4pMxxRh9IRyx] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Error when processing the invocation | ||
dev.restate.sdk.common.TerminalException: Payment could not be accepted! | ||
... rest of trace ... | ||
``` |
This file contains 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
15 changes: 15 additions & 0 deletions
15
java/patterns-use-cases/microservices-sagas/src/main/java/dev/restate/patterns/AppMain.java
This file contains 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 |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package dev.restate.patterns; | ||
|
||
import dev.restate.patterns.activities.CarRentals; | ||
import dev.restate.patterns.activities.Flights; | ||
import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder; | ||
|
||
public class AppMain { | ||
public static void main(String[] args) { | ||
RestateHttpEndpointBuilder.builder() | ||
.bind(new BookingWorkflow()) | ||
.bind(new CarRentals()) | ||
.bind(new Flights()) | ||
.buildAndListen(); | ||
} | ||
} |
This file contains 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
16 changes: 11 additions & 5 deletions
16
...e-cases/microservices-sagas/src/main/java/dev/restate/patterns/activities/CarRentals.java
This file contains 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 |
---|---|---|
@@ -1,34 +1,40 @@ | ||
package dev.restate.patterns.activities; | ||
|
||
import dev.restate.patterns.BookingWorkflow.TravelBookingRequest; | ||
import dev.restate.sdk.Context; | ||
import dev.restate.sdk.annotation.Handler; | ||
import dev.restate.sdk.annotation.Service; | ||
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
|
||
import java.util.UUID; | ||
|
||
@Service | ||
public class CarRentals { | ||
private static final Logger logger = LogManager.getLogger(CarRentals.class); | ||
|
||
public record CarRentalRequest(TravelBookingRequest req /* rental details */) {} | ||
public record CarRentalRequest(String pickupLocation, String rentalDate) {} | ||
|
||
@Handler | ||
public String reserve(Context ctx, CarRentalRequest request) { | ||
// this should implement the communication with the rental | ||
// provider's APIs | ||
// just return a mock random id representing the reservation | ||
return "car-" + UUID.randomUUID().toString(); | ||
String bookingId = UUID.randomUUID().toString(); | ||
logger.info("Car rental reservation created with id: {}", bookingId); | ||
return bookingId; | ||
} | ||
|
||
@Handler | ||
public void confirm(Context ctx, String carRentalBookingId) { | ||
public void confirm(Context ctx, String bookingId) { | ||
// this should implement the communication with the rental | ||
// provider's APIs | ||
logger.info("Car rental reservation confirmed with id: {}", bookingId); | ||
} | ||
|
||
@Handler | ||
public void cancel(Context ctx, String carRentalBookingId) { | ||
public void cancel(Context ctx, String bookingId) { | ||
// this should implement the communication with the rental | ||
// provider's APIs | ||
logger.info("Car rental reservation cancelled with id: {}", bookingId); | ||
} | ||
} |
Oops, something went wrong.