Skip to content

Commit

Permalink
Make the Java SAGAs example easier to understand
Browse files Browse the repository at this point in the history
  • Loading branch information
StephanEwen committed Nov 27, 2024
1 parent b9339c9 commit 42a063e
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 139 deletions.
5 changes: 3 additions & 2 deletions patterns-use-cases/sagas/sagas-java/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SAGAs / Compensations

An example of a trip reservation workflow, using the SAGAs pattern to
undo previous steps in csase of an error.
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).

Expand All @@ -12,4 +12,5 @@ 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 `ctx.rpc()`.
operations, like `ctx.run()` (e.g., direct calls to 3rd party APIs) or
RPCs to other Restate-backed services (e.g., `FlightsClient.fromContex(ctx)`).
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()));
}
}
}

This file was deleted.

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
}
}
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
}
}
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
}
}

0 comments on commit 42a063e

Please sign in to comment.