-
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.
Make the Java SAGAs example easier to understand
- Loading branch information
1 parent
b9339c9
commit 42a063e
Showing
6 changed files
with
210 additions
and
139 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
110 changes: 110 additions & 0 deletions
110
patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/BookingWorkflow.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,110 @@ | ||
/* | ||
* Copyright (c) 2024 - Restate Software, Inc., Restate GmbH | ||
* | ||
* This file is part of the Restate examples, | ||
* which is released under the MIT license. | ||
* | ||
* You can find a copy of the license in the file LICENSE | ||
* in the root directory of this repository or package or at | ||
* https://github.com/restatedev/examples/ | ||
*/ | ||
|
||
package dev.restate.patterns; | ||
|
||
import dev.restate.patterns.activities.*; | ||
import dev.restate.sdk.WorkflowContext; | ||
import dev.restate.sdk.annotation.Workflow; | ||
import dev.restate.sdk.common.TerminalException; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
// | ||
// SAGAs / Compensations | ||
// | ||
// An example of a trip reservation workflow, using the SAGAs 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 journalled | ||
// operations, like `ctx.run()` or RPC calls/messages executed | ||
// through the Restate Context. | ||
// | ||
|
||
/** | ||
* Trip reservation workflow which has been instrumented with compensations. The workflow tries to | ||
* reserve the flight and the car rental before it processes the payment. If at any point one of | ||
* the calls fails or gets cancelled, then the trip reservation workflow will undo all | ||
* successfully completed steps by running the compensations. | ||
* | ||
* <p>Note: that the compensation logic is purely implemented in the user code and runs durably | ||
* until it completes. Moreover, an invocation failure and an invocation cancellation are handled | ||
* in the exact same way by the caller. | ||
*/ | ||
@Workflow | ||
public class BookingWorkflow { | ||
|
||
// The workflow parameters, like the car and flight to book, the | ||
// payment details (card/token, amount, ...) | ||
public record TravelBookingRequest( /* car, flights, payment info, ... */ ) { } | ||
|
||
@Workflow | ||
public void run(WorkflowContext context, TravelBookingRequest request) throws TerminalException { | ||
// Create a list of compensations to run in case of a failure or cancellation. | ||
final List<Runnable> compensations = new ArrayList<>(); | ||
|
||
try { | ||
// Reserve the flights and let Restate remember the reservation ID | ||
final var flightsRpcClient = FlightsClient.fromContext(context); | ||
final String flightReservationId = | ||
flightsRpcClient | ||
.reserve(new Flights.FlightBookingRequest(request)) | ||
.await(); | ||
// Register the compensation to undo the flight reservation. | ||
compensations.add(() -> flightsRpcClient.cancel(flightReservationId).await()); | ||
|
||
// Reserve the car and let Restate remember the reservation ID | ||
final var carRentalRpcClient = CarRentalsClient.fromContext(context); | ||
final String carReservationId = | ||
carRentalRpcClient | ||
.reserve(new CarRentals.CarRentalRequest(request)) | ||
.await(); | ||
// Register the compensation to undo the car rental reservation. | ||
compensations.add(() -> carRentalRpcClient.cancel(carReservationId).await()); | ||
|
||
// call the payment service to make the payment and let Restate remember | ||
// the payment ID | ||
final var paymentRpcClient = PaymentClient.fromContext(context); | ||
final String paymentId = | ||
paymentRpcClient | ||
.process(new Payment.PaymentRequest(request)) | ||
.await(); | ||
// Register the compensation to undo the payment. | ||
compensations.add(() -> paymentRpcClient.refund(paymentId).await()); | ||
|
||
// confirm the reserved flight / rental | ||
// failures here will still trigger the SAGA compensations | ||
flightsRpcClient.confirm(flightReservationId).await(); | ||
carRentalRpcClient.confirm(carReservationId).await(); | ||
|
||
} catch (TerminalException e) { | ||
|
||
// Run the compensations | ||
for (Runnable compensation : compensations) { | ||
compensation.run(); | ||
} | ||
|
||
// rethrow error to fail this workflow | ||
throw new TerminalException( | ||
e.getCode(), | ||
String.format( | ||
"Failed to reserve the trip: %s. Ran %d compensations.", | ||
e.getMessage(), compensations.size())); | ||
} | ||
} | ||
} |
137 changes: 0 additions & 137 deletions
137
patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/Compensations.java
This file was deleted.
Oops, something went wrong.
34 changes: 34 additions & 0 deletions
34
...-use-cases/sagas/sagas-java/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 |
---|---|---|
@@ -0,0 +1,34 @@ | ||
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 java.util.UUID; | ||
|
||
@Service | ||
public class CarRentals { | ||
|
||
public record CarRentalRequest(TravelBookingRequest req /* rental details */) {} | ||
|
||
@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(); | ||
} | ||
|
||
@Handler | ||
public void confirm(Context ctx, String carRentalBookingId) { | ||
// this should implement the communication with the rental | ||
// provider's APIs | ||
} | ||
|
||
@Handler | ||
public void cancel(Context ctx, String carRentalBookingId) { | ||
// this should implement the communication with the rental | ||
// provider's APIs | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
...rns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/activities/Flights.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,34 @@ | ||
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 java.util.UUID; | ||
|
||
@Service | ||
public class Flights { | ||
|
||
public record FlightBookingRequest(TravelBookingRequest req /* flight number, class, ... */) {} | ||
|
||
@Handler | ||
public String reserve(Context ctx, FlightBookingRequest 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(); | ||
} | ||
|
||
@Handler | ||
public void confirm(Context ctx, String flightBookingId) { | ||
// this should implement the communication with the rental | ||
// provider's APIs | ||
} | ||
|
||
@Handler | ||
public void cancel(Context ctx, String flightBookingId) { | ||
// this should implement the communication with the rental | ||
// provider's APIs | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
...rns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/activities/Payment.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,29 @@ | ||
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 java.util.UUID; | ||
|
||
@Service | ||
public class Payment { | ||
|
||
public record PaymentRequest(TravelBookingRequest req /* paymentToken, amount, ... */ ) {} | ||
|
||
@Handler | ||
String process(Context context, PaymentRequest request) { | ||
// this should implement the actual payment processing, or communication | ||
// to the external provider's APIs | ||
// just return a mock random id representing the payment | ||
return UUID.randomUUID().toString(); | ||
} | ||
|
||
@Handler | ||
public void refund(Context context, String paymentId) { | ||
// refund the payment identified by this paymentId | ||
// this should implement the actual payment processing, or communication | ||
// to the external provider's APIs | ||
} | ||
} |