Yesterday I finally got presence channel working with Pusher, which I need to check who's in what game and to distribute game updates to them.
Today was about getting myself educated in the art of state-machines with AWS Step Functions.
Step Functions
AWS Step Functions is a serverless (autoscale, pay per use, etc.) service, that lets you define and execute state-machines in the AWS cloud.
It can control Lambda functions and most importantly it can be controlled by Lambda functions.
I created a test implementation to see how this could work.
First I created a state-machine definition and integrated it into my CloudFormation template.yaml. I also created a IAM role that now will be used by Lambda and Step Functions to do their thing.
The definition of a state-machine is generally straight forward, but kinda ugly to integrate into CloudFormation, because it needs to be done as string and the definition is written in JSON.
Anyway, this is the definition inside the template:
GameStateMachine:
Type: "AWS::StepFunctions::StateMachine"
Properties:
RoleArn: !GetAtt [ ExecutionRole, Arn ]
DefinitionString:
Fn::Sub:
- |-
{
"StartAt": "WaitingForPlayers",
"States": {
"WaitingForPlayers": {
"Type": "Task",
"Resource": "${joinGameActivityArn}",
"TimeoutSeconds": 20,
"End": true
}
}
}
-
joinGameActivityArn: !Ref JoinGameActivity
JoinGameActivity:
Type: "AWS::StepFunctions::Activity"
Properties:
Name: JoinGame
As one can see, some string substitution is taking place to get the ARN the JoinGameActivity
into the definition. The activity is also defined in the template.
It goes like this:
1) startGame
Lambda function is called via API-Gateway
module.exports = async (event, context) => {
const executionParams = {
// The GameStateMachine ARN is available via env-var
// it's passed here by CloudFormation
stateMachineArn: process.env.GAME_STATE_MACHINE_ARN,
// some input data that is used as start input for the state-machine
input: JSON.stringify({ gameId: GAME_ID })
};
// starting a new execution via the AWS-SDK
await stepFunctions
.startExecution(executionParams)
.promise();
};
2) GameStateMachine
execution goes into the WaitingForPlayers
state until some kind of worker sends a success via the AWS-SDK or if the timeout hits.
3) joinGame
Lambda function is called via API-Gateway
module.exports = async (event, context) => {
let task;
{
const getTaskParams = {
// The JoinGame activity ARN is available via env-var
// it's passed here by CloudFormation
activityArn: process.env.JOIN_GAME_ACTIVITY_ARN
};
// If a task for this activity is available it will be polled here
task = await stepFunctions.getActivityTask(getTaskParams).promise();
}
// some game logic happening, haha
const input = JSON.parse(task.input);
{
// this token is need to send a success or fail state later
const { taskToken } = task;
const taskSuccessParams = {
taskToken,
output: JSON.stringify(input)
};
// the success is send to the activity
// so the GameStateMachine can transition to the next state
await stepFunctions.sendTaskSuccess(taskSuccessParams).promise();
}
};
4) The GameStateMachine
transitions to its end state and succeeds execution.
Next
The problem is, there are multiple executions, one for every game, but is still only one JoinGame
activity that can be polled. Now, when players for 2 games poll this activity they should only get the activity tasks for their game, which is not possible at the moment.
Well, maybe tomorrow :)