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

feat: switch from event bridge to lambda #1

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
19 changes: 4 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,28 +151,17 @@ The request body has one property **guess**, which only allows either "down" or

The input validation is done through the API Gateway itself.

### EventBridge
As there is no way to integrate Step Functions in an async workflow with
API Gateway and the CDK, I use Event Bridge as a layer between API Gateway and Step Functions. There is only one rule, which triggers the execution of the Step Function workflow
and returns the event ID, which is used as the game ID.
```json
{
"detail-type": ["putEvent"]
}
```

### Lambda
There are 3 lambda functions:

1. new-guess: stores the initial game data to DynamoDB
1. new-guess: stores the initial game data to DynamoDB and starts the state machine with a 60 seconds wait state.
2. handle-result: checks if the initial price differs from the current price and updates the DynamoDB entry when the game is finished
3. check-result: allows the user to get status updates about the given game ID

### Step Functions
There are 3 tasks:
1. newGuessTasks: is triggered through EventBridge and starts the above mentioned lambda function, it returns the game data and a property "waitSeconds" with 60 seconds as its value.
2. waitTask: gets a variable "waitSeconds" and waits for this amount of time
3. handleResultTask triggers the above mentioned handle-result lambda and returns whether the price did change or not with a property "didPriceChange" and "waitSeconds" with 20 seconds. That means that as long as the price did not change it will wait 20 seconds again and again until it changed and is then resolved by updating the DynamoDB items gameStatus to "finished".
There are 2 tasks:
1. waitTask: gets a variable "waitSeconds" and waits for this amount of time
2. handleResultTask triggers the above mentioned handle-result lambda and returns whether the price did change or not with a property "didPriceChange" and "waitSeconds" with 20 seconds. That means that as long as the price did not change it will wait 20 seconds again and again until it changed and is then resolved by updating the DynamoDB items gameStatus to "finished".

![stepfunctionsgraph.png](/diagrams/stepfunctions.png)
### DynamoDB
Expand Down
Binary file modified diagrams/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified diagrams/stepfunctions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 0 additions & 86 deletions packages/backend/infrastructure/constructs/eb-integration.ts

This file was deleted.

63 changes: 40 additions & 23 deletions packages/backend/infrastructure/constructs/rest-api.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { CfnOutput } from "aws-cdk-lib";
import {
AwsIntegration,
JsonSchemaType,
Model,
RequestValidator,
RestApi as AwsRestApi,
LambdaIntegration,
Cors,
MethodLoggingLevel,
LambdaRestApi
LambdaRestApi,
} from "aws-cdk-lib/aws-apigateway";
import { Construct } from "constructs";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";

interface Props {
eventBridgeIntegration: AwsIntegration;
newGuessLambda: NodejsFunction;
checkResultLambda: NodejsFunction;
}

Expand All @@ -24,7 +23,7 @@ export class RestApi extends Construct {
constructor(scope: Construct, id: string, props: Props) {
super(scope, id);

const { eventBridgeIntegration, checkResultLambda } = props;
const { newGuessLambda, checkResultLambda } = props;

const api = new AwsRestApi(this, "api", {
description: "entry point for the bitcoin guessr api",
Expand Down Expand Up @@ -65,27 +64,45 @@ export class RestApi extends Construct {
.addResource("check-result")
.addResource("{id}");

createNewGameResource.addMethod("POST", eventBridgeIntegration, {
methodResponses: [
{
statusCode: "201",
responseParameters: {
"method.response.header.Access-Control-Allow-Headers": true,
"method.response.header.Access-Control-Allow-Methods": true,
"method.response.header.Access-Control-Allow-Credentials": true,
"method.response.header.Access-Control-Allow-Origin": true,
createNewGameResource.addMethod(
"POST",
new LambdaIntegration(newGuessLambda, {
proxy: true,
integrationResponses: [
{
statusCode: "201",
responseParameters: {
"method.response.header.Access-Control-Allow-Headers":
"'Content-Type,Accept,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
"method.response.header.Access-Control-Allow-Methods":
"'GET,POST,OPTIONS'",
"method.response.header.Access-Control-Allow-Origin": "'*'",
},
},
},
],
requestValidator: new RequestValidator(this, "body-validator", {
restApi: api,
requestValidatorName: "body-validator",
validateRequestBody: true,
],
}),
requestModels: {
"application/json": apiValidationModel,
},
});
{
methodResponses: [
{
statusCode: "201",
responseParameters: {
"method.response.header.Access-Control-Allow-Headers": true,
"method.response.header.Access-Control-Allow-Methods": true,
"method.response.header.Access-Control-Allow-Credentials": true,
"method.response.header.Access-Control-Allow-Origin": true,
},
},
],
requestValidator: new RequestValidator(this, "body-validator", {
restApi: api,
requestValidatorName: "body-validator",
validateRequestBody: true,
}),
requestModels: {
"application/json": apiValidationModel,
},
}
);

checkResultResource.addMethod(
"GET",
Expand Down
10 changes: 3 additions & 7 deletions packages/backend/infrastructure/constructs/state-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ export class StateMachine extends Construct {

const { handleResultLambda, newGuessLambda } = props;

const newGuessTask = new tasks.LambdaInvoke(this, "New guess", {
lambdaFunction: newGuessLambda,
outputPath: "$.Payload",
});

const waitTask = new sfn.Wait(this, "Wait x seconds", {
time: sfn.WaitTime.secondsPath("$.waitSeconds"),
});
Expand All @@ -31,8 +26,7 @@ export class StateMachine extends Construct {
outputPath: "$.Payload",
});

const definition = newGuessTask
.next(waitTask)
const definition = waitTask
.next(handleResultTask)
.next(
new sfn.Choice(this, "Did the price change?")
Expand All @@ -48,5 +42,7 @@ export class StateMachine extends Construct {
stateMachineType: sfn.StateMachineType.EXPRESS,
timeout: Duration.minutes(2),
});

this.machine.grantStartExecution(newGuessLambda);
}
}
15 changes: 4 additions & 11 deletions packages/backend/infrastructure/stacks/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@ import { DynamoDbTable } from "../constructs/ddb";
import { RestApi } from "../constructs/rest-api";
import { StateMachine } from "../constructs/state-machine";
import { Lambdas } from "../constructs/lambdas";
import { EventBus } from "aws-cdk-lib/aws-events";
import { APIEventBridgeIntegration } from "../constructs/eb-integration";

export class BackendStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const table = new DynamoDbTable(this, "GuessTable");
const eventBus = new EventBus(this, "eventBus");

const { handleResultLambda, newGuessLambda, checkResultLambda } =
new Lambdas(this, "LambdaFns", {
Expand All @@ -25,17 +22,13 @@ export class BackendStack extends Stack {
handleResultLambda: handleResultLambda.lambda,
});

const { eventBridgeIntegration } = new APIEventBridgeIntegration(
this,
"APIEventBridgeIntegration",
{
eventBus,
stateMachine: machine,
}
newGuessLambda.lambda.addEnvironment(
"STEP_FUNCTION_ARN",
machine.stateMachineArn
);

new RestApi(this, "NewGuessApi", {
eventBridgeIntegration,
newGuessLambda: newGuessLambda.lambda,
checkResultLambda: checkResultLambda.lambda,
});
}
Expand Down
36 changes: 25 additions & 11 deletions packages/backend/lib/lambda/new-guess.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import { Logger } from "@aws-lambda-powertools/logger";
import { DynamoDB } from "aws-sdk";
import { DynamoDB, StepFunctions } from "aws-sdk";
import { getEnvVarOrThrow } from "../../utils/helper";
import { getCurrentBitcoinPriceInUSD } from "../../utils/bitcoin-api";
import { EventBridgeEvent } from "aws-lambda";
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
import { GameStatus, GuessData } from "../../types";
import { nanoid } from "nanoid";

const NEW_GUESS_TABLE_NAME = getEnvVarOrThrow("NEW_GUESS_TABLE_NAME");
const STEP_FUNCTION_ARN = getEnvVarOrThrow("STEP_FUNCTION_ARN");

const logger = new Logger({ serviceName: "New guess" });
const ddb = new DynamoDB.DocumentClient();
const stepfunctions = new StepFunctions();

type NewGuessBody = Pick<GuessData, "guess">;

export async function main(
event: EventBridgeEvent<any, any>
): Promise<{ body: string }> {
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> {
logger.info("new guess", { event: JSON.stringify(event) });

try {
const body = event.detail as NewGuessBody;
const body = JSON.parse(event.body!) as NewGuessBody;

const oldPrice = await getCurrentBitcoinPriceInUSD();

const id = nanoid();
const newGuessEntry = {
id: event.id,
id,
timestamp: new Date().toISOString(),
oldPrice,
gameStatus: GameStatus.Processing,
Expand All @@ -39,14 +42,25 @@ export async function main(

await ddb.put(ddbNewGuessParam).promise();

return Promise.resolve({
body: JSON.stringify(newGuessEntry),
waitSeconds: 60,
stepfunctions.startExecution({
stateMachineArn: STEP_FUNCTION_ARN,
name: "state-machine",
input: JSON.stringify({
body: JSON.stringify(newGuessEntry),
waitSeconds: 60,
}),
});

return {
statusCode: 201,
body: JSON.stringify({ id }),
};
} catch (error) {
logger.error("error on api execution for new guess init", {
msg: JSON.stringify(error),
});
return Promise.reject(error);
return {
statusCode: 500,
};
}
}