// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.DocumentModel; using Amazon.EventBridge; using Amazon.EventBridge.Model; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; using Amazon.StepFunctions; using Amazon.StepFunctions.Model; using KellermanSoftware.CompareNetObjects; using ManagementConsoleBackend.Common; using ManagementConsoleBackend.ManagementService.Data; using ManagementConsoleBackend.ManagementService.Lib; using Newtonsoft.Json; namespace ManagementConsoleBackend.ManagementService; public class StepFunctions { /// Main state poller function. Polls GameLift and compares the result to stored state in DDB. If there are differences or 15 minutes have elapsed, sends the state as a custom event to EventBridge and stores in DDB public async Task StatePollHandler(APIGatewayProxyRequest request, ILambdaContext context) { var client = new AmazonEventBridgeClient(); var dynamoDbClient = new AmazonDynamoDBClient(); var stateEventDetail = await GameLiftStateHandler.GetStateEventDetail(); // get last stored state from DDB var databaseStateItem = await GameLiftStateHandler.GetLatestStateDatabaseItem(); var comparisonConfig = new ComparisonConfig { IgnoreObjectTypes = true, MaxDifferences = 20, MaxStructDepth = 5, MembersToIgnore = new[] { "PreSignedLogUrl" }.ToList() }; var compareLogic = new CompareLogic(comparisonConfig); var comparisonResult = compareLogic.Compare(databaseStateItem?.State, stateEventDetail); //LambdaLogger.Log(comparisonResult.DifferencesString); //LambdaLogger.Log(JsonConvert.SerializeObject(comparisonResult.Differences)); // remove this data to stop Differences from bloating in size foreach (var comparisonResultDifference in comparisonResult.Differences) { comparisonResultDifference.ParentObject1 = null; comparisonResultDifference.ParentObject2 = null; } var fifteenMinutesAgo = DateTime.UtcNow - TimeSpan.FromMinutes(15); // send + store event if database event 15 minutes old, or there are differences if (comparisonResult.Differences.Count > 0 || databaseStateItem?.Time < fifteenMinutesAgo) { var entry = new PutEventsRequestEntry { Source = "CustomGameLift", EventBusName = Environment.GetEnvironmentVariable("EventBusName"), Detail = JsonConvert.SerializeObject(stateEventDetail), DetailType = "CustomGameLift.GameLiftState" }; //LambdaLogger.Log(JsonConvert.SerializeObject(entry)); var putEventsResponse = await client.PutEventsAsync(new PutEventsRequest { Entries = new List {entry} }); var stateLogTable = Table.LoadTable(dynamoDbClient, Environment.GetEnvironmentVariable("StateLogTableName")); var dbItem = new GameLiftStateDatabaseItem { State = stateEventDetail, Differences = comparisonResult.Differences, TimeToLive = (Utils.GetUnixTimestamp() + (86400*7)), }; var item = Document.FromJson(JsonConvert.SerializeObject(dbItem)); item["Date"] = DateTime.UtcNow.Date; item["Time"] = DateTime.UtcNow; await stateLogTable.PutItemAsync(item); LambdaLogger.Log(JsonConvert.SerializeObject(putEventsResponse)); } return new { PollAgain = true, PollFrequency = 15 }; } public async Task GameSessionPollHandler(APIGatewayProxyRequest request, ILambdaContext context) { await GameLiftStateHandler.UpdateGameSessions(); return new { PollAgain = true, PollFrequency = 15 }; } /// Function checks if zero running executions, and starts one if not /// public static async Task StartStateMachineExecutionIfNotRunning() { try { var sfnClient = new AmazonStepFunctionsClient(); var listExecutionsResult = await sfnClient.ListExecutionsAsync(new ListExecutionsRequest { StateMachineArn = Environment.GetEnvironmentVariable("StateMachineArn"), StatusFilter = ExecutionStatus.RUNNING }); if (listExecutionsResult.Executions.Count == 0) { await sfnClient.StartExecutionAsync(new StartExecutionRequest { StateMachineArn = Environment.GetEnvironmentVariable("StateMachineArn") }); } } catch (Exception e) { LambdaLogger.Log(e.ToString()); } try { var sfnClient = new AmazonStepFunctionsClient(); var listExecutionsResult = await sfnClient.ListExecutionsAsync(new ListExecutionsRequest { StateMachineArn = Environment.GetEnvironmentVariable("GameSessionPollerStateMachineArn"), StatusFilter = ExecutionStatus.RUNNING }); if (listExecutionsResult.Executions.Count == 0) { await sfnClient.StartExecutionAsync(new StartExecutionRequest { StateMachineArn = Environment.GetEnvironmentVariable("GameSessionPollerStateMachineArn") }); } } catch (Exception e) { LambdaLogger.Log(e.ToString()); } } /// Simple count function to count iterations of the state machine public object StepFunctionIteratorHandler(Stream stream, ILambdaContext context) { string requestStr; using (StreamReader reader = new StreamReader(stream)) { requestStr = reader.ReadToEnd(); } LambdaLogger.Log(requestStr); LambdaLogger.Log(JsonConvert.SerializeObject(context)); var inputEvent = JsonConvert.DeserializeObject(requestStr); int index = inputEvent?.Iterator.Index; int step = inputEvent?.Iterator.Step; int count = inputEvent?.Iterator.Count; index += step; var result = new { Index = index, Count = count, Step = step, Continue = (index < count) }; LambdaLogger.Log(JsonConvert.SerializeObject(result)); return result; } /// Starts a new Step Functions execution for a given State Machine public async Task StepFunctionRestartHandler(Stream stream, ILambdaContext context) { string requestStr; using (var reader = new StreamReader(stream)) { requestStr = reader.ReadToEnd(); } var inputEvent = JsonConvert.DeserializeObject(requestStr); try { var sfnClient = new AmazonStepFunctionsClient(); await sfnClient.StartExecutionAsync(new StartExecutionRequest { StateMachineArn = inputEvent?.StateMachine.Id }); var result = new { sfnRestarted = true }; return result; } catch (Exception e) { LambdaLogger.Log(e.ToString()); return null; } } }