Executing an AWS .NET Lambda at an Exact Time with Step Functions

Full source code available here.

There are a few ways to execute a Lambda at a given time with AWS. One of the most common is to use Event Bridge, but it is granular down to a minute, i.e. you want something to run tomorrow at 18:30, it will run within a minute of that. For many cases, that is fine.

But if you want more precision there are a few approaches. The first is to handle it yourself by setting your job to start at 18:29, then in the code sleep until the exact second.

Another is to use a Step Function, they let you schedule a Lambda execution up to a year in the future. In my tests, the execution takes place within a quarter to a tenth of a second of the specified time.

This post will walkthrough everything you need to create the Step Function and schedule the execution of the Lambda.

The Lambda

Before I get to the Step Function, I will create the Lambda and the code to run in it.

AWS has provided templates for .NET Core projects. If you haven’t done so already, install the templates.

dotnet new -i Amazon.Lambda.Templates

Create a new project.

dotnet new lambda.EmptyFunction --name LambdaPersonToUpper

This will generate two projects in the src and the test directories.

Inside src\LambdaPersonToUpper there is a Function.cs file, this is what the Lambda in AWS will call.

It is very simple, it takes a Person, converts the first name and last name to uppercase, and adds a created date timestamp (this will be visible in the output of the step function).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using Amazon.Lambda.Core;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace LambdaPersonToUpper
{
    public class Function
    {
        
        public Person FunctionHandler(Person person, ILambdaContext context)
        {   
            person.FirstName = person.FirstName.ToUpper();
            person.LastName = person.LastName.ToUpper();
            person.CreatedDate = DateTime.Now;

            return person;
        }
    }
}

If you’re wondering how the Lambda will understand what a Person is, take a look at line 6, which is where serialization is handled.

That’s all there is to the function. I have included the test project in the source code attached to this post.

In AWS, create a new Lambda, follow the steps from this post if you are not familiar. But set the function handler to LambdaPersonToUpper::LambdaPersonToUpper.Function::FunctionHandler.

Testing the Lambda

In AWS I can test the function with a simple block of Json.

{
  "firstName": "alan",
  "lastName": "adams"
}

And here is the result.

That’s the Lambda in place.

Step Function

Step Functions are great (I rarely start a paragraph like that), there is no point in me trying to explain everything they do when you can find it on the AWS site, so here is the link - AWS Step Functions.

I have used them to manage how I call multiple small Lambdas, passing the output from one to the input of another. There are numerous other uses, and one of those is doing something at an exact time.

The Step Function I will show takes Json as an input parameter - the Json is made up of the date and time to execute the Lambda, and the person to pass to the Lambda.

Go to Step Functions in the AWS Console and hit create.

There are two general types, standard, and express. Because I want to schedule my Lambda to run some time into the future, the express option with a max duration of 5 minutes will not work for me.

Next, I start building the workflow. The main thing I want is for the workflow to wait until some designated time in the future. On the left, type in “wait” and drag the Wait Step to the graph. On the right, select “Wait until a specified date and time”, and tell the step where to find the timestamp in the input by using $.startTime. ($ refers to the whole input.)

The Lambda needs the Json related to the person. I add an output filter to pass only that information to the next step.

Next, I add the Lambda step. Again, search on the left. Drag the Lambda to the graph below the Wait.

On the right search for the Lambda function.

In the “Payload” section, I leave “Use state input as payload”, because the wait step is going to pass only the person.

Specify a name for the state machine, leave the permissions the way they are, and hit “Create state machine” at the bottom (not shown in the image below).

I get a confirmation that the state machine has been created.

Now for the fun, running it.

Hit “Start execution” and paste in this Json, adjusting for a time not too far in the future, time is in GMT.

{
  "startTime": "2021-06-23T02:16:00.00Z",
  "person": {
    "firstName": "alan",
    "lastName": "adams"
  }
}

I set the startTime to a minute in the future as I was writing this.

The wait step pauses until that time is reached.

When 02:16:00.000Z was reached, the Lambda was invoked, and the person passed to it.

A detailed event history is also shown, with the output of the Lambda. On the right, you can see that the Step Function started executing the Lambda a little ahead of the scheduled time. In the output, you can see the CreatedDate was 2021-06-23T02:15:59.9067915+00:00, again slightly ahead of the scheduled time, but close enough for many uses.

How Exact Is It?

From the image above, you can see the time the .NET application ran was less than 100 milliseconds before the scheduled time. For me, that is close enough, but this was a trivial application. Its startup time is small, its processing time is tiny.

In a more complex situation, I recommend calling the Lambda a few seconds before the time you want it to run and pause within the .NET code using a delay or sleep.

The Full State Machine

For completeness, here is the full source code of the state machine.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
  "Comment": "This is your state machine",
  "StartAt": "Wait",
  "States": {
    "Wait": {
      "Type": "Wait",
      "TimestampPath": "$.startTime",
      "Next": "Lambda Invoke",
      "OutputPath": "$.person"
    },
    "Lambda Invoke": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "OutputPath": "$.Payload",
      "Parameters": {
        "FunctionName": "arn:aws:lambda:us-east-1:111111111111:function:PersonToUpper:$LATEST",
        "Payload.$": "$"
      },
      "Retry": [
        {
          "ErrorEquals": [
            "Lambda.ServiceException",
            "Lambda.AWSLambdaException",
            "Lambda.SdkClientException"
          ],
          "IntervalSeconds": 2,
          "MaxAttempts": 6,
          "BackoffRate": 2
        }
      ],
      "End": true
    }
  }
}

 
Full source code available here.

comments powered by Disqus

Related