Introducing Durable Entities for Serverless State
The dungeon does not forget
The combination of Azure Functions <⚡> and the extension Durable Functions enables long running workflows and the implementation of multiple patterns that I wrote about in a previous blog post. Although it is possible to associate metadata with workflows, referred to as orchestrations, it is limited in scope and usually used for identification. Tracking more complex data and state still required interaction with a back-end data store or database. With Durable Functions v2, this changes and now data related to state can be encapsulated in Durable Entities.
The code and functionality in this post is preview code and subject to change.
I recently introduced the concept of managing state and complex workflows in serverless applications using a simple game called the Durable Dungeon. If you haven’t already, I recommend looking at the original article before continuing. It’s available here:
Stateful Serverless: Long-Running Workflows with Durable Functions
The Durable Task Framework makes it easy to address various workflow patterns implemented with Azure Functions.
In the example application, I tracked four entities in a “game”:
user👤 who is playing
monster👹 to challenge the user
Inventory, including a weapon 🔪 and a treasure 💎
room🏠 where the action all happens
The application used Durable Functions to track game state and enable various workflows, but the entities were all manually tracked using Azure Table Storage. The following (simplified) code illustrates the steps to check for an existing user (other non-related code has been removed for clarity).
The user entity is defined as a
TableEntity to accommodate the requirements of Table Storage.
BaseHasInventory class contains properties and methods to convert between a single string to serialize the inventory list and an actual searchable list of individual strings). Here is the code to insert a new entry:
Although this approach works fine for a game demo, it has some inherent problems. First, the state has an affinity to the storage, so regardless of how the application scales, the storage could become a bottleneck. Second, the code doesn’t address concurrency. If
NewUser is called simultaneously for the username, a race condition could occur that would result in one of the insert operations failing.
Durable entities solves these problems. I updated the repo to include a new project, DungeonEntities, that removes any dependency on storage and instead uses durable entities.
Introducing Durable Entities
Durable entities provide a mechanism to track state explicitly within orchestrations rather than implicitly as part of the control flow. They are managed by Durable Functions and will work with whatever storage option you choose. One advantage with Durable Entities over managing your own data is that concurrency is handled for you. Instead of manipulating an entity and storing it to a database, Durable Entities are managed via operations that are dispatched with the guarantee only a single operation is run at any given time for a given entity. This prevents race conditions from occurring.
The new functionality is available via a NuGet package:
This post was written with
The new package doesn’t default all the host settings, so at a minimum you want to specify a hub name and a storage provider, like this:
There are two approaches to defining your entities. You can use a functional approach, like this:
…or a class-based approach. I was already using multiple entities, so I chose to go with the latter.
It is important to note that even if you choose the class-based approach, you are essentially signaling and reading entity state. Instead of obtaining an object, mutating it, then updating it as you might be used to in a database-driven approach, durable entities are message-based and every operation that mutates state should be wrapped in a method call.
Here is the definition for the user:
The data being tracked is the user’s name, the room the user is in and the user’s state of health (either alive or dead). The possible operations are
Run method is the key to defining User as a durable entity and dispatches a context able to interact with the methods on the class. Notice that it is a trigger like anything else that signals code to execute in the Azure Functions serverless environment. I’ll cover the
IUserOperations interface soon.
Reading and Creating State
Now that the entity is defined, it is possible to interact with the entity to read and manipulate state. These operations are performed using the
OrchestrationClient that is passed in as
IDurableOrchestrationClient. This is the code to check if the user exists:
Every entity is accessed with a unique identifier that is the name of the entity and a key. In this case, the key is the username. If the user state has already been created,
EntityExists returns as
true. The state itself is available as the property
EntityState that is of type
Any signal to an entity will result in it being created. I can call any operation on the entity, but I chose
New to set the name and flag the user as “alive.”
await client.SignalEntityAsync(id, nameof(User.New), username);
That’s it! Behind the scenes, the entity is stored as an instance in the same table that tracks other orchestrations. For my user, it created a key of
@User@Jeremy (type and key) with a serialized JSON payload that looks like this:
It may seem a little odd to use
nameof to grab a function name and call it without validation or strong types. Fortunately, it is possible to use a proxy to call methods directly on the target class. The proxy only supports methods, not properties, so the first thing to do is create an interface with the available operations:
User entity implements the interface, so they are always in sync. The new interface will then allow you to call via proxy like this:
Notice I create the identifier based on the entity, and signal using the interface. I also created an extension method to make it easier to create identifiers. The extension method looks like this:
There is an optional parameter for “treasure name” that I’ll explain later.
Making Room for the Monster: Updating Entities
I always try to make my code simple and easy to read. If I find I’m duplicating code, I wrap it in an API or extension method. The first pattern I identified was loading an entity and calling
RestoreLists to build the inventory list. This is a carry-over from table storage that doesn’t serialize lists (something durable entities is capable of, but it was easier for me to use the existing code). Every entity except for individual inventory items is identified by the user, so this method:
Makes it possible to execute this code:
var check = await user.ReadUserEntityAsync<Room>(client);
In many cases the state should already exist, so I want to either throw an exception or return the state object itself. To make life easier, I created this extension method:
Now placing a monster in a room and updating each entity to reference the other looks like this:
For the most part, all operations are a combination of fetching the state to inspect it, then dispatching an operation. Inventory works a little differently.
Weapons and Loot: Dealing with Lists
The inventory entity has multiple instances (a weapon and a treasure) so a user key won’t work (it would be duplicated). If I use the name of the inventory as the key, I end up with a problem because I must know the inventory name to fetch it, but I don’t know what weapon or treasure was generated without inspecting it. See the catch-22? Although I only have two inventory items, I decided to implement it as a list to illustrate the solution for 1..N. Inventory works like this:
- Save the list of inventory names with the key user
- Save each inventory item with the key user:item-name
Using the storage explorer, this is what inventory looks like for user “Flint”:
This is the logic to place the treasure on a monster:
- Read the inventory list (just a list of names)
- Read each inventory item on the list
- Find the item that is the treasure
- Set the monster property on the treasure
- Add the inventory item to the monster’s inventory
…and the code:
Notice that the name of the item is passed to the extension method for the id, so it is created as user:item-name as opposed to just user.
Return on Aggregation
So far, I’ve demonstrated the narrow use case of tracking state for individual sessions. The power of durable entities truly shines when implementing the aggregation pattern. Imagine you have an Internet of Things solution and you are tracking metrics for devices. In a traditional approach, concurrency is a major concern with multiple updates happening simultaneously. Durable entities ensure you can perform aggregate operations safely due to the guarantee that operations on state are completely atomic.
UserCounter definition, I used the functional approach rather than the class-based approach. I declared the operations and a static key because there is just one state (“total active users”) for the entire application. This creates the literal key
The entity keeps track of active users. The operations are defined like this:
If the entity hasn’t been created yet,
currentValue defaults to 0. After a user is added, the entity is signaled to increment.
await starter.SignalEntityAsync(UserCounter.Id, UserCounter.NewUser);
Conversely, when a user finds the treasure or is “killed” for not confirming in time, a signal is raised to decrease the aggregate count.
await client.SignalEntityAsync(UserCounter.Id, UserCounter.UserDone);
GameStatus API returns the total count of active users:
This will handle any number of users simultaneously accessing the system and will aggregate across all the distributed nodes used to scale the application. Welcome to the “easy button” for distributed transactions!
That concludes my lap around the new durable entities. You’ve seen how to define them as both functions and classes. I covered strategy for defining unique identifiers and dealing with things like scoped lists. I demonstrated how to check for the existence of an entity, read state and dispatch operations. Finally, the project uses the aggregation pattern to track active users.
Access the repository here:
A game designed to teach and learn serverless durable functions in C# - JeremyLikness/DurableDungeon
Are you intrigued by durable functions? Jump right in with a hands-on tutorial that walks you step-by-step through creating and managing durable functions. No Azure subscription is required: Create a long-running serverless workflow with Durable Functions.