Skip to content

Commit

Permalink
Improve typescript examples for basics and sagas
Browse files Browse the repository at this point in the history
  • Loading branch information
gvdongen committed Dec 12, 2024
1 parent c9d2f6c commit c6a4254
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 112 deletions.
9 changes: 3 additions & 6 deletions typescript/basics/src/1_durable_execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,17 @@ restate
// Parameters are durable across retries
const { userId, creditCard, subscriptions } = req;

// 1. Generate an idempotency key
// This value is retained after a failure
const paymentId = ctx.rand.uuidv4();
// Recoverable after failures
const stableIdempotencyKey = ctx.rand.uuidv4();

// 2. Create a recurring payment via API call
// Retried in case of timeouts, API downtime, etc.
const { success } = await ctx.run(() =>
createRecurringPayment(userId, creditCard, paymentId),
createRecurringPayment(creditCard, stableIdempotencyKey),
);
if (!success) {
return;
}

// 3. Create subscriptions via API calls
// Persists successful subscriptions and skip them on retries
for (const subscription of subscriptions) {
await ctx.run(() => createSubscription(userId, subscription));
Expand Down
22 changes: 5 additions & 17 deletions typescript/basics/src/2_durable_execution_sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,38 +24,26 @@ restate
add: async (ctx: restate.Context, req: SubscriptionRequest) => {
const { userId, creditCard, subscriptions } = req;

// We will add the undo actions to this list
const compensations = [];
try {
const paymentId = ctx.rand.uuidv4();
// Register compensating actions for steps that need to be undone in case of a terminal error
compensations.push(() => removeRecurringPayment(paymentId));
const { success } = await ctx.run(() =>
createRecurringPayment(userId, creditCard, paymentId),
const res = await ctx.run(() =>
createRecurringPayment(creditCard, paymentId),
);
if (!success) {
if (!res.success) {
return;
}

for (const subscription of subscriptions) {
// Register compensating actions for the subscriptions, to run in case of a terminal error
compensations.push(() =>
removeSubscription(userId, subscription),
);
const result = await ctx.run(() =>
createSubscription(userId, subscription),
);
// If the subscription already exists, then revert the payment and other subscriptions
// and surface the error to the user
if (result == "ALREADY_EXISTS") {
throw new restate.TerminalError("Duplicate subscription");
}
compensations.push(() => removeSubscription(userId, subscription));
await ctx.run(() => createSubscription(userId, subscription));
}
} catch (err) {
// On TerminalError, Restate runs compensations without retrying.
// On other errors, Restate does not run compensations but retries from the last successful operation.
if (err instanceof restate.TerminalError) {
console.error(">>> Terminal error occurred. Running compensations.");
for (const compensation of compensations.reverse()) {
await ctx.run(compensation);
}
Expand Down
6 changes: 3 additions & 3 deletions typescript/basics/src/3_workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { createUserEntry, sendEmailWithLink } from "./utils/stubs";
// - Additional methods interact with the workflow.
// Each workflow instance has a unique ID and runs only once (to success or failure).
//
const singupWorkflow = restate.workflow({
const signupWorkflow = restate.workflow({
name: "usersignup",
handlers: {
// --- The workflow logic ---
Expand Down Expand Up @@ -50,9 +50,9 @@ const singupWorkflow = restate.workflow({
},
});

export type SignupApi = typeof singupWorkflow;
export type SignupApi = typeof signupWorkflow;

restate.endpoint().bind(singupWorkflow).listen(9080);
restate.endpoint().bind(signupWorkflow).listen(9080);
// or .handler() to run on Lambda, Deno, Bun, Cloudflare Workers, ...

/*
Expand Down
7 changes: 2 additions & 5 deletions typescript/basics/src/4_virtual_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@ import * as restate from "@restatedev/restate-sdk";
const greeterObject = restate.object({
name: "greeter",
handlers: {
greet: async (
ctx: restate.ObjectContext,
req: { greeting: string },
) => {
greet: async (ctx: restate.ObjectContext, req: { greeting: string }) => {
// Access the state attached to this object (this 'name')
// State access and updates are exclusive and consistent with the invocations
// State access and updates are exclusive and consistent with the execution progress.
let count = (await ctx.get<number>("count")) ?? 0;
count++;
ctx.set("count", count);
Expand Down
6 changes: 4 additions & 2 deletions typescript/basics/src/utils/stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* https://github.com/restatedev/examples/blob/main/LICENSE
*/

import * as restate from "@restatedev/restate-sdk";

/**
* Utility to let the service crash with a probability to show how the system recovers.
*/
Expand Down Expand Up @@ -43,7 +45,8 @@ export function createSubscription(
console.log(`>>> Creating subscription ${subscription} for user ${userId}`);

if (Math.random() < 0.5) {
return "ALREADY_EXISTS";
console.error("Duplicate subscription.");
throw new restate.TerminalError("Duplicate subscription");
}

return "SUCCESS";
Expand All @@ -53,7 +56,6 @@ export function createSubscription(
* Simulates calling a payment API, with a random probability of API downtime.
*/
export function createRecurringPayment(
userId: string,
creditCard: string,
paymentId: any,
): { success: boolean } {
Expand Down
4 changes: 1 addition & 3 deletions typescript/patterns-use-cases/microservices-sagas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

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
Expand All @@ -12,4 +10,4 @@ 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 journald
operations, like `ctx.sideEffect()` or rpc/messaging.
operations, like `ctx.run()` or rpc/messaging.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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/
*/

import * as restate from "@restatedev/restate-sdk";

import { flights } from "./services/flights";
import { cars } from "./services/cars";
import { payments } from "./services/payments";

//
// An example of a trip reservation workflow, using the SAGAs pattern to
// undo previous steps in case of an error.
//
// The 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 journald
// operations, like `ctx.run()` or rpc/messaging.
restate.endpoint().bind(
restate.workflow({
name: "BookingWorkflow",
handlers: {
run: async (ctx: restate.WorkflowContext, tripID: string) => {
// create a list of undo actions
const compensations = [];
try {

// call the flight API to reserve, keeping track of how to cancel
const flightBooking = await ctx.run(() => flights.reserve(tripID));
compensations.push(() => flights.cancel(tripID, flightBooking));

// call the car rental service API to reserve, keeping track of how to cancel
const carBooking = await ctx.run(() => cars.reserve(tripID));
compensations.push(() => cars.cancel(tripID, carBooking));

// call the payments API, keeping track of how to refund
const paymentId = ctx.rand.uuidv4();
compensations.push(() => payments.refund({ paymentId }));
await ctx.run(() => payments.process({ tripID }));

// confirm the flight and car reservations
await flights.confirm(tripID, flightBooking);
await cars.confirm(tripID, carBooking);

} catch (e) {

if (e instanceof restate.TerminalError) {
// undo all the steps up to this point by running the compensations
for (const compensation of compensations.reverse()) {
await compensation();
}
}

throw e;
}
}
},
})
).listen(9080)

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const payments = {
return "payment_id";
},

refund: async (request: { tripID: string; paymentId: string }) => {
refund: async (request: { paymentId: string }) => {
// refund the payment
},
};
Expand Down

This file was deleted.

0 comments on commit c6a4254

Please sign in to comment.