Skip to content
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

Improve understandability of Java Sagas Example #219

Merged
merged 1 commit into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}