Using a Memory Cache with .NET Lambda Functions

Download full source code.

This post is a bit heretical. You need to assess for yourself if it is of benefit to you, or if it offends your serverless sensibilities too much. This is very much a case of “your mileage may vary”. You’ve been warned!

Introduction

When a Lambda function is invoked, an execution environment is created. Creating this environment takes a bit of time and is referred to as a “cold start”. Initialization code is run during this first invocation. That’s why it’s a good idea to put things like database connection initialization, S3 client creation, etc, outside the function’s body.

Once this execution environment is up and running the function is referred to as “warm”, and it will stay warm for an undefined period, the initialization code will not be run again. The function will remain warm if it is being called regularly.

If your Lambda function is receiving more requests than can be handled by a single execution environment, the Lambda service will create a new execution environment with its own memory cache. These two execution environments will be separate and will not share cached data. In a upcoming post I will show how to use a shared cache with Lambda functions.

Now that you know a Lambda function can be somewhat long-lived, it opens the possibility of using a memory cache that runs in process, i.e. in the execution environment.

The simple application below will look for a key in the cache, if found, it will be returned, but if it is not found, a simulated call to a database will be made. The value from the “database” will be stored in the cache (for 10 seconds) and returned to the original caller. A subsequent call to the function (less than 10 seconds later), for the same key, will find the value in the cache. The database will not be accessed.

To demonstrate this you are going to deploy a Lambda function and invoke it using Lambda Function URLs.

Create the Lambda function

From the command line, run -

dotnet new lambda.EmptyFunction --name MemoryCacheInALambdaFunction

Replace the code in the function

Change to the MemoryCacheInALambdaFunction/src/MemoryCacheInALambdaFunction directory.

Add two NuGet packages - Amazon.Lambda.APIGatewayEvents, and Microsoft.Extensions.Caching.Memory.

Open the Function.cs file and replace what’s there with the following -

 1using Microsoft.Extensions.Caching.Memory;
 2using System.Net;
 3using System.Text.Json;
 4using Amazon.Lambda.APIGatewayEvents;
 5using Amazon.Lambda.Core;
 6
 7// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
 8[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
 9
10namespace MemoryCacheInALambdaFunction;
11
12public class Function
13{
14    private readonly MemoryCache cache;
15    private readonly string functionId;
16    private readonly string functionCreationTime; 
17    private readonly Random random;
18    public Function()
19    {
20        cache = new MemoryCache(new MemoryCacheOptions());
21        functionId = Guid.NewGuid().ToString().Substring(0,8); 
22        functionCreationTime = DateTime.Now.ToString("HH:mm:ss.fff");
23        random = new Random();
24    }
25
26    public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest request, ILambdaContext context)
27    {
28        Console.WriteLine(JsonSerializer.Serialize(request)); // not needed, but great for seeing what a request looks like
29        string itemId = request.QueryStringParameters["itemId"];
30        Console.WriteLine($"Got itemId: {itemId}");
31
32        ResponseBody responseBody = new ResponseBody() {
33            FunctionId = functionId,
34            FunctionCreationTime = functionCreationTime,
35            ItemId = itemId
36        };  
37
38        if (cache.TryGetValue(itemId, out string quantity))
39        {
40            responseBody.Quantity = quantity;
41            responseBody.FromCache = true;
42        }
43        else
44        {   
45            quantity = random.Next(1000).ToString(); // simulate a database call
46            responseBody.Quantity = quantity;
47            responseBody.FromCache = false;
48            cache.Set(itemId, quantity, TimeSpan.FromSeconds(10));
49        }
50        
51        var response = new APIGatewayProxyResponse
52        {
53            StatusCode = (int)HttpStatusCode.OK,
54            Body = JsonSerializer.Serialize(responseBody),
55            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
56        };
57
58        return response;
59    }
60}
61
62public class ResponseBody
63{
64    public string FunctionId { get; set; }
65    public string FunctionCreationTime { get; set; } 
66    public string ItemId { get; set; }  
67    public string Quantity { get; set; }
68    public bool FromCache { get; set; }   
69}

Deploy the function

Use the following to build the code and deploy the Lambda function -

dotnet lambda deploy-function MemoryCacheInALambdaFunction 

You will be asked - “Select IAM Role that to provide AWS credentials to your code:”, select “*** Create new IAM Role ***”

You will then be asked - “Enter name of the new IAM Role:”, put in “MemoryCacheInALambdaFunctionRole”.

Then you will be asked to - “Select IAM Policy to attach to the new role and grant permissions”, select “AWSLambdaBasicExecutionRole”, for me it is number 6 on the list.

Wait as the function and permissions are created.

Configuring the function for HTTP requests

From the command line, run -

aws lambda create-function-url-config --function-name MemoryCacheInALambdaFunction --auth-type NONE

You will see -

{
    "FunctionUrl": "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.us-east-1.on.aws/",
    "FunctionArn": "arn:aws:lambda:us-east-1:xxxxxxxxxxx:function:MemoryCacheInALambdaFunction",
    "AuthType": "NONE",
    "CreationTime": "2022-07-13T00:43:45.699805Z"
}

At this point, a URL is attached to the Lambda function, requiring no authentication, but you’re not quite finished yet.

Note the FunctionUrl, you will use this to invoke the function.

Add a Resource-based policy

One more thing to do - add a resource-based policy to allow the Lambda function to be called via the URL. See here for more.

aws lambda add-permission --function-name MemoryCacheInALambdaFunction --statement-id AuthNone --action lambda:InvokeFunctionUrl --principal * --function-url-auth-type NONE

You will get a response like -

{
    "Statement": "{"Sid":"AuthNone","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunctionUrl","Resource":"arn:aws:lambda:us-east-1:xxxxxxxxx:function:MemoryCacheInALambdaFunction","Condition":{"StringEquals":{"lambda:FunctionUrlAuthType":"NONE"}}}"
}

Now the Lambda function can be accessed from the URL with no authentication needed.

Testing it out

Open your browser and go to the URL you just created, adding ?itemId=1 to the end. N.B. itemId is case sensitive.

Say you open - https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.us-east-1.on.aws/?itemId=1

The first response will be -

{
    "FunctionId": "fd382df2",
    "FunctionCreationTime": "01:29:50.243",
    "ItemId": "1",
    "Quantity": "796",
    "FromCache": false
}

Now reload the page a few seconds later, you will get -

{
    "FunctionId": "fd382df2",
    "FunctionCreationTime": "01:29:50.243",
    "ItemId": "1",
    "Quantity": "796",
    "FromCache": true
}

See how the FromCache went from false to true.

Also, keep an eye on the FunctionId and the FunctionCreationTime - they won’t change if you keep the function “warm” by invoking it regularly.

Download full source code.

comments powered by Disqus

Related