Skip to content

Commit

Permalink
Merge branch 'main' into restructure
Browse files Browse the repository at this point in the history
# Conflicts:
#	java/patterns-use-cases/microservices-payment-state-machines/src/main/java/my/example/utils/TypeChecks.java
  • Loading branch information
gvdongen committed Dec 18, 2024
2 parents fca9b87 + 6821fb8 commit f8e59c3
Show file tree
Hide file tree
Showing 20 changed files with 524 additions and 66 deletions.
26 changes: 19 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
**/node_modules
**/dist
**/.vscode
**/.idea
**/package-lock.json
**/target
**/restate-data
# IDEs
.vscode
.idea
*.iml

# NodeJS
node_modules
dist
package-lock.json

# Java
.gradle
build
target
out
**/src/main/generated
hs_err_pid*

# Restate Server
restate-data
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@

package my.example;

import static my.example.types.PaymentStatus.*;

import dev.restate.sdk.ObjectContext;
import dev.restate.sdk.annotation.Handler;
import dev.restate.sdk.annotation.VirtualObject;
import dev.restate.sdk.common.StateKey;
import dev.restate.sdk.common.TerminalException;
import dev.restate.sdk.serde.jackson.JacksonSerdes;
import java.time.Duration;
import my.example.accounts.AccountClient;
Expand All @@ -24,35 +25,38 @@
import my.example.types.Result;

/**
* The service that processes the payment requests.
* A service that processes the payment requests.
*
* <p>This is implemented as a virtual object to ensure that only one concurrent request can happen
* per token (requests are queued and processed sequentially per token).
* per payment-id. Requests are queued and processed sequentially per id.
*
* <p>Note that this idempotency-token is more of an operation/payment-id. Methods can be called
* multiple times with the same token, but payment will be executed only once. Also, if a
* cancellation is triggered for that token, the payment will not happen or be undine, regardless of
* whether the cancel call comes before or after the payment call.
* <p>Methods can be called multiple times with the same payment-id, but payment will be executed
* only once. If a 'cancelPayment' is called for an id, the payment will either be undone, or
* blocked from being made in the future, depending on whether the cancel call comes before or after
* the 'makePayment' call.
*/
@VirtualObject
public class PaymentProcessor {

private static final Duration EXPIRY_TIMEOUT = Duration.ofHours(1);

/** The key under which we store the status. */
private static final StateKey<PaymentStatus> STATUS =
StateKey.of("status", JacksonSerdes.of(PaymentStatus.class));

/** The key under which we store the original payment request. */
private static final StateKey<Payment> PAYMENT =
StateKey.of("payment", JacksonSerdes.of(Payment.class));

private static final Duration EXPIRY_TIMEOUT = Duration.ofDays(1);

@Handler
public Result makePayment(ObjectContext ctx, Payment payment) {
// De-duplication to make calls idempotent
PaymentStatus status = ctx.get(STATUS).orElse(PaymentStatus.NEW);
if (status == PaymentStatus.CANCELLED) {
final String paymentId = ctx.key();
final PaymentStatus status = ctx.get(STATUS).orElse(NEW);

if (status == CANCELLED) {
return new Result(false, "Payment already cancelled");
}

if (status == PaymentStatus.COMPLETED_SUCCESSFULLY) {
if (status == COMPLETED_SUCCESSFULLY) {
return new Result(false, "Payment already completed in prior call");
}

Expand All @@ -63,40 +67,36 @@ public Result makePayment(ObjectContext ctx, Payment payment) {
.await();

// Remember only on success, so that on failure (when we didn't charge) the external
// caller may retry this (with the same token), for the sake of this example
// caller may retry this (with the same payment-id), for the sake of this example
if (paymentResult.isSuccess()) {
ctx.set(STATUS, PaymentStatus.COMPLETED_SUCCESSFULLY);
ctx.set(STATUS, COMPLETED_SUCCESSFULLY);
ctx.set(PAYMENT, payment);

String idempotencyToken = ctx.key();
PaymentProcessorClient.fromContext(ctx, idempotencyToken).send(EXPIRY_TIMEOUT).expireToken();
PaymentProcessorClient.fromContext(ctx, paymentId).send(EXPIRY_TIMEOUT).expire();
}

return paymentResult;
}

@Handler
public void cancelPayment(ObjectContext ctx) {
PaymentStatus status = ctx.get(STATUS).orElse(PaymentStatus.NEW);
PaymentStatus status = ctx.get(STATUS).orElse(NEW);

switch (status) {
case NEW -> {
// not seen this token before, mark as canceled, in case the cancellation
// not seen this payment-id before, mark as canceled, in case the cancellation
// overtook the actual payment request (on the external caller's side)
ctx.set(STATUS, PaymentStatus.CANCELLED);

PaymentProcessorClient.fromContext(ctx, ctx.key()).send(EXPIRY_TIMEOUT).expireToken();
}
case CANCELLED -> {
// already cancelled, this is a repeated request
ctx.set(STATUS, CANCELLED);
PaymentProcessorClient.fromContext(ctx, ctx.key()).send(EXPIRY_TIMEOUT).expire();
}

case CANCELLED -> {}

case COMPLETED_SUCCESSFULLY -> {
// remember this as cancelled
ctx.set(STATUS, PaymentStatus.CANCELLED);
ctx.set(STATUS, CANCELLED);

// undo the payment
Payment payment =
ctx.get(PAYMENT).orElseThrow(() -> new TerminalException("Payment not found"));
Payment payment = ctx.get(PAYMENT).get();
AccountClient.fromContext(ctx, payment.getAccountId())
.send()
.deposit(payment.getAmountCents());
Expand All @@ -105,8 +105,7 @@ public void cancelPayment(ObjectContext ctx) {
}

@Handler
public void expireToken(ObjectContext ctx) {
ctx.clear(STATUS);
ctx.clear(PAYMENT);
public void expire(ObjectContext ctx) {
ctx.clearAll();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,13 @@ public Result(boolean success, String reason) {
public boolean isSuccess() {
return success;
}

public String getReason() {
return reason;
}

@Override
public String toString() {
return "Result{" + "success=" + success + ", reason='" + reason + '\'' + '}';
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +0,0 @@
/*
* 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 my.example.utils;

import dev.restate.sdk.common.TerminalException;
import my.example.types.Payment;

public class TypeChecks {

public static void validatePayment(Payment payment) {
if (payment.getAccountId() == null || payment.getAccountId().isEmpty()) {
throw new TerminalException("Account ID is required");
}
if (payment.getAmountCents() <= 0) {
throw new TerminalException("Amount must be greater than 0");
}
}
}
3 changes: 3 additions & 0 deletions templates/go-lambda-cdk/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
cdk.out
15 changes: 15 additions & 0 deletions templates/go-lambda-cdk/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"env": {
"browser": true,
"commonjs": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"overrides": [],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest"
},
"plugins": ["@typescript-eslint"],
"rules": {}
}
10 changes: 10 additions & 0 deletions templates/go-lambda-cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
!jest.config.js
node_modules
.cdk.staging
cdk.out
.gradle
lambda/go
*.js
*.d.ts
*.class
cdk.context.json
7 changes: 7 additions & 0 deletions templates/go-lambda-cdk/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"arrowParens": "always",
"printWidth": 120
}
69 changes: 69 additions & 0 deletions templates/go-lambda-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Go Lambda (CDK) example

Sample project deploying a Go-based Restate service to AWS Lambda using the AWS Cloud Development Kit (CDK).
The stack uses the Restate CDK constructs library to register the service with a Restate Cloud environment.

For more information on CDK, please see [Getting started with the AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html).

* [CDK app entry point `lambda-go-cdk.ts`](bin/lambda-go-cdk.ts)
* [CDK stack consisting of a Lambda function and providing Restate service registration](cdk/lambda-go-cdk-stack.ts)
* [Go Lambda handler](lambda) - based on [`go` template](../go)

## Download the example

- Via the CLI:
```shell
restate example go-lambda-cdk && cd go-lambda-cdk
```

- Via git clone:
```shell
git clone [email protected]:restatedev/examples.git
cd examples/templates/go-lambda-cdk
```

- Via `wget`:
```shell
wget https://github.com/restatedev/examples/releases/latest/download/go-lambda-cdk.zip && unzip go-lambda-cdk.zip -d go-lambda-cdk && rm go-lambda-cdk.zip
```

## Deploy

**Pre-requisites:**

* npm
* Go >= 1.21
* AWS account, bootstrapped for CDK use
* valid AWS credentials with sufficient privileges to create the necessary resources
* an existing [Restate Cloud](https://restate.dev) environment (environment id + API key)

Install npm dependencies:

```shell
npm install
```

To deploy the stack, export the Restate Cloud environment id and admin API key, and run `cdk deploy`:

```shell
export RESTATE_ENV_ID=env_... RESTATE_API_KEY=key_...
npx cdk deploy
```

The stack output will print out the Restate server ingress URL.

### Test

You can send a test request to the Restate ingress endpoint to call the newly deployed service:

```shell
curl -k ${restateIngressUrl}/Greeter/Greet \
-H "Authorization: Bearer $RESTATE_API_KEY" \
-H 'content-type: application/json' -d '"Restate"'
```

### Useful commands

* `npm run build` compile the Lambda handler and synthesize CDK deployment artifacts
* `npm run deploy` perform a CDK deployment
* `npm run destroy` delete the stack and all its resources
24 changes: 24 additions & 0 deletions templates/go-lambda-cdk/bin/go-lambda-cdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env node

/*
* 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 "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { GoLambdaCdkStack } from "../cdk/go-lambda-cdk-stack";

const app = new cdk.App();
new GoLambdaCdkStack(app, "GoLambdaCdkStack", {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
59 changes: 59 additions & 0 deletions templates/go-lambda-cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"app": "npx ts-node --prefer-ts-exts bin/go-lambda-cdk.ts",
"watch": {
"include": ["**"],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true
}
}
Loading

0 comments on commit f8e59c3

Please sign in to comment.