8

Using Actors

In this chapter, you will learn about the powerful virtual actor model, as implemented in Dapr, and how to leverage it in a microservices-style architecture, along with the pros and cons of different approaches. The actor model enables your Dapr application to efficiently respond to scenarios of high resource contention by streamlining state management.

In this chapter, we will cover the following main topics:

  • Using actors in Dapr
  • Actor concurrency, consistency, and lifetime
  • Implementing actors in an e-commerce reservation system

The entry barrier for adopting actors in Dapr is lower than the complexity behind the core concepts of the virtual actor pattern. Nonetheless, a solid understanding of the scenarios for actors – including the ability to recognize bad practices and avoid any pitfalls – is a prerequisite for their adoption. Therefore, we will start by providing an overview of the actor model before moving on to its lab implementation with Dapr.

Technical requirements

The code for this chapter can be found on GitHub at https://github.com/PacktPublishing/Practical-Microservices-with-Dapr-and-.NET-Second-Edition/tree/main/chapter08.

In this chapter, the working area for the scripts and code can be found at <repository path>chapter08. In my local environment, it can be found here: C:Reposdapr-sampleschapter08.

Please refer to the Setting up Dapr section of Chapter 1, Introducing Dapr, for a complete guide on the tools needed to develop with Dapr and work with the examples provided.

Using actors in Dapr

The actor model in Dapr adopts the concept of virtual actors: a simplified approach to a complex combination of design challenges. Virtual actors originate from the Microsoft Orleans project – a project that inspired the design of Dapr. If you want to deepen your knowledge of its history, the respective research paper can be found at https://www.microsoft.com/en-us/research/project/orleans-virtual-actors/.

In the virtual actor pattern, the state and behavior of a service are tightly intertwined, and the actor’s lifetime becomes orchestrated by an external service or runtime. Because of this, the developers are lifted from the responsibility of governing concurrent access to the resource (the virtual actor) and its underlying state.

These concepts will become clearer when we analyze how the virtual actor pattern is implemented in Dapr in the next section.

Introduction to the virtual actor pattern

In Dapr, the interaction between a client and a service counterpart happens via a direct call, with a service-to-service invocation, or indirectly via a message, with a publisher and a subscriber, as seen in the previous chapters. The Dapr application acting as the service then accesses the state to read and/or manipulate it.

If we consider the components involved in this class of interactions, it all boils down to the following:

  • A client remote call
  • A service processing the request
  • A database managing the stored information

We have just listed the same number of interactions as seen in a classic three-tier architecture. A microservice architecture affects several aspects of client/service communication by introducing new patterns and capabilities; nevertheless, it can’t escape the laws of physics, such as network latency and storage input/output (I/O) latency.

As our Dapr application is going to operate in a highly distributed environment (starting from Chapter 9, Deployment to Kubernetes), the three aforementioned components could be located in separate nodes or even be part of different services, and therefore traverse the network and/or application boundaries, each of which adds its share of latency.

Caching helps reduce latency, bringing information closer to the service by improving the performance of repeated reads from the source of information: the database. At the same time, it introduces the complexity of managing consistency, which was previously solved by the database: it’s difficult to keep a cache relevant while updates are performed. Once you try to solve this consistency issue by controlling access to cached information, the complexity of concurrency promptly emerges. Caching is a powerful mechanism, but once you try to strengthen it from its consistency and concurrency perspectives, it risks falling short of its declared objective.

The virtual actor pattern that’s implemented in Dapr tries to approach this question differently. It could help us understand the actor model if we consider the starting point and its challenges. Let’s do this by analyzing the current status of the backend of our cookie-selling Biscotti Brutti Ma Buoni e-commerce site.

The following screenshot depicts the interactions between our sample Dapr applications and their state:

Figure 8.1 – Status quo of the sample architecture

Figure 8.1 – Status quo of the sample architecture

As you can see from Figure 8.1, the Reservation Service (also known as the reservation-service Dapr application) sits at the center of many interactions and participates with other Dapr applications in the saga we explored in Chapter 6, Publish and Subscribe, which receives updates from the manufacturing service and responds to requests from our storefront service (which is generic since using a much richer user interface (UI) is outside the scope of this book).

The reservation-service Dapr application relies on two state stores – reservationstate and reservationitemstate:

  • The reservationstate state store keeps track of the products (SKU) that have been reserved by an order. This is useful when we’re compensating the order in case of a product customization failure.

We can predict that there will be a limited number of interactions with the state for the specific order of successfully fulfilled ones.

The population of reservationstate is always increasing, with operations that concentrate on the state for newly added orders.

  • The reservationitemstate state store is used differently: it keeps track of the balance of each product’s quantity; orders diminish its value, while replenishments increase it. Each order (and its potential compensating activities), storefront request, or manufacturing action equates to state retrieval and updating of the specific item.

We adopt strong concurrency when managing the reservation-service state to avoid inconsistent updates. As a side effect, we risk increasing conflicts of updates in the case of sustained growth in requests, leading to more retries to avoid transferring the impact to clients.

The population of reservationitemstate becomes stable over time, one for each SKU, with operations being distributed evenly or unevenly, depending on the SKU’s popularity.

As shown in the preceding diagram, the same number of data retrieval or manipulation requests are reaching the state store – Azure Cosmos DB, in our example.

By introducing the Dapr actor model, we can create a virtual representation of each reservationitemstate state and bind them with the service code counterpart.

The following diagram shows where the Dapr actor model will be introduced:

Figure 8.2 – Dapr actor model introduced in the sample architecture

Figure 8.2 – Dapr actor model introduced in the sample architecture

The preceding diagram shows the evolution of our sample architecture: we introduce a new reservationactor-service Dapr service that offers access to the population of Dapr actors of the ReservationItemActor type.

As we will learn later in this chapter, Dapr manages the lifetime and distribution of actors in the hosting environment (locally or in Kubernetes). We can expect that each of the SKUs typically sold by our e-commerce site will have a corresponding actor instance, ready to receive requests.

What do Dapr (virtual) actors provide to our sample microservice architecture? The business logic that manages the SKU balance quantity, refactored from reservation-service to the code of the ReservationItemActor actor, will leverage the state that’s being maintained in the actor instance itself. This is kept closely in sync by the Dapr runtime.

With actors in Dapr, their state and behavior become intertwined, and the load gets distributed over the hosting platform for resiliency. No additional effort is required from the developer since every aspect is managed by the Dapr runtime and platform.

Accessing a Dapr actor is governed by a turn-based policy: a read operation will find the state information in memory, equally as fast as an item in the cache, while update operations will reach the state store’s database without facing the concurrency issues we have discussed so far.

Before we finish this overview, let’s learn how to configure a state store for Actors in Dapr.

Configuring a new state store

We learned how to configure a state store in Chapter 5, Introducing State Management. With that knowledge, we will set up a new Azure Cosmos DB state store to keep the state of our actors.

In the same chapter, we also learned that only state stores that support transactions and ETAg can be used with actors in Dapr: the Azure Cosmos DB state store is one of these.

For a complete list, see the Dapr documentation at https://docs.dapr.io/reference/components-reference/supported-state-stores/.

The following .yaml file is an example of a state store configuration:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: reservationitemactorstore
  namespace: default
spec:
  type: state.azure.cosmosdb
  metadata:
  - name: url
    value: …omitted…
  - name: masterKey
    value: …omitted…
  - name: database
    value: state
  - name: collection
    value: reservationitemactorstore
  - name: actorStateStore
    value: "true"

The changes we’ve made to support the actor model are minimal. We only need to set the actorStateStore metadata to true.

Now that we have configured a new state store, which I have named reservationitemactorstore, we should verify it.

Verifying the configuration

We can verify that the configuration has been applied by launching any of the existing Dapr applications; it will look for the .yaml files in the components path.

In the following Dapr application log, only the significant lines have been extracted:

== DAPR == time="…" level=warning msg="either no actor state
store or multiple actor state stores are specified in the
configuration, actor stores specified: 0" app_id=order-service
instance=DB-XYZ scope=dapr.runtime type=log ver=1.8.0
== DAPR == time="…" level=info msg="component loaded. name:
customizationstore, type: state.azure.cosmosdb" app_id=order-
service instance=DB-XYZ scope=dapr.runtime type=log ver=1.8.0

In the previous output, we can see that the message "either no actor state store or multiple actor state stores are specified" is presented when a state store is found that has not been configured as an actor state store:

== DAPR == time="…" level=info msg="component loaded. name:
reservationitemactorstore, type: state.azure.cosmosdb" app_
id=order-service instance=DB-XYZ scope=dapr.runtime type=log
ver=1.8.0
== DAPR == time="…" level=info msg="actor runtime started.
actor idle timeout: 1h0m0s. actor scan interval: 30s" app_
id=order-service instance=DB-XYZ scope=dapr.runtime.actor
type=log ver=1.8.0
== DAPR == time="…" level=info msg="starting connection attempt
to placement service: localhost:6050" app_id=order-service
instance=DB-XYZ scope=dapr.runtime.actor type=log ver=1.8.0
…
== DAPR == time="…" level=info msg="established connection
to placement service at localhost:6050" app_id=order-service
instance=DB-XYZ scope=dapr.runtime.actor type=log ver=1.8.0

Since it has not been preceded by any warning message, the reservationitemactorstore component has been recognized as a state store for actors. If you incorrectly configure two state stores as actorStateStore, you will receive the same warning we saw previously since only one is permitted. No warning is a good sign. In the final output message, we received confirmation that a connection to the Dapr placement service has been established.

This extensive overview gave us a better understanding of how actors in Dapr work to help us build scalable applications. Before we move on to the implementation, we need to understand a few more concepts, including the actor’s lifetime.

Actor concurrency, consistency, and lifetime

The Dapr actor model relies on two main components: the Dapr runtime, operating in the sidecar, and the Dapr placement service. We’ll understand each of these in the following sections.

Placement service

The placement service is responsible for keeping a map of the Dapr instances that are capable of serving actors. Considering our example, the reservationactor-service application is an example of such a service.

Once a new instance of our new reservationactor-service Dapr application starts, it informs the placement service that it is ready to serve actors of the ReservationItemActor type.

The placement service broadcasts a map – in the form of a hash table with the host’s information and the served actor types – to all the Dapr sidecars operating in the environment.

Thanks to the host’s map being constantly updated, actors are uniformly distributed over the actor service instances.

In the Kubernetes deployment mode of Dapr, the host is a Pod (a group of containers that are deployed together), while in standalone mode, the host is the local node itself. In Kubernetes, Pods can be terminated or initiated in response to many events (such as scaling out Pods and nodes, or a node being evicted from the cluster to be upgraded, added, or removed).

Considering the example that we deployed in a Kubernetes cluster, we should have at least three replicated Pods with reservationactor-service running together with the Dapr sidecar container. If our cookie-selling e-commerce site has about 300 active cookies’ SKU, approximately 100 actors of the ReservationItemActor type will reside in each of the reservationactor-service Pods.

You can learn more about the placement service in the Dapr documentation at https://docs.dapr.io/developing-applications/building-blocks/actors/actors-overview/#actor-placement-service.

Now, let’s learn how the Dapr actor model deals with concurrency and consistency.

Concurrency and consistency

The actor model’s implementation in Dapr approaches the state’s concurrency manipulation by transparently enforcing turn-based access with per-actor locks. This lock is acquired at the beginning of each interaction and is released afterward.

The following diagram shows turn-based access for Dapr actors:

Figure 8.3 – Turn-based concurrency access to Dapr actors

Figure 8.3 – Turn-based concurrency access to Dapr actors

In the preceding diagram, we can appreciate the impact of locks being acquired and released on a per-actor basis. Considering the two actors in this example – the ReservationItemActor actor for the bussola8 SKU and the ReservationItemActor actor for the rockiecookie SKU – the following steps correspond to client (other Dapr or non-Dapr applications) requests in order of time, as follows:

  1. A request to reserve the quantity on the actor with an ID of rockiecookie. The actor is ready, and no locks are present, so it is immediately granted, and the interaction can start immediately.
  2. A request to reserve quantity on the actor with an ID of bussola8. This actor is ready with no locks present either, so the interaction can start immediately.
  3. A request to replenish the quantity for actor ID rockiecookie is received. The actor is still locked since step 1, so the request waits for the lock to be released. Once the actor becomes available, the interaction can start.
  4. A request to retrieve the current balance quantity for actor ID bussola8 is received. The actor is still locked since step 2, so the request waits for it to be released. Once the actor becomes available, the quick (read) operation can happen.
  5. A similar request reaches actor ID rockiecookie. Since all the interactions with the actor are treated equally and the actor is locked to working on step 3, the request waits for the lock’s release to quickly interact with the actor.

None of these interactions required any additional effort from the Dapr application’s code; everything is handled by the Dapr runtime.

While turn-based access to Dapr Actors imposes strict concurrency control, it’s important to remember that this is enforced on a per-actor basis: concurrent requests to different actors can be fulfilled independently at the same time.

This approach gives Dapr an advantage in maintaining data consistency while acting as a cache. Since the actor’s state could be as simple as a record in the Dapr state store, receiving data manipulation requests on a specific record with a serial approach puts any database in the best condition to operate.

From an individual actor’s perspective, it’s important to avoid executing long or variable operations as these would influence the lock’s duration and impact client interaction. As an example, an actor should avoid I/O operations or any other blocking interaction.

The actor pattern is best used with many fast, independent actors.

From a client perspective, though, trying to coordinate or aggregate information from too many actors may not result in the best possible experience. As we learned in Chapter 5, Introducing State Management, it’s possible to submit queries directly to the underlying database to extract or aggregate data from state stores.

In our specific sample, reservation-service evolves from being the Dapr application in charge of managing the reservationitemstate state to becoming the main client of the ReservationItemActor actor type, which is part of the reservationactor-service application.

Any Dapr-enabled application can interact with the Actors via a software development kit (SDK), or by directly invoking the Dapr application programming interface (API) at the endpoint that was exposed by the Dapr sidecar.

At this stage in developing Actors in Dapr, an actor cannot directly subscribe to a publish/subscribe topic. This is an example in which an actor needs to be invoked indirectly from another service.

Let’s briefly explore how the resiliency feature can improve the interactions between actor instances.

Resiliency

The resiliency feature in Dapr allows us to configure how to handle transient errors, which are encountered by applications interacting with each other or with external components. As we learned in the Resiliency section in Chapter 4, Service-to-Service Invocation, a few types of policies can be applied to targets, including actors.

Resiliency is very useful once applied to the Actor building block; let’s see why. With apps as the target, resiliency policies influence how to handle transient errors experienced by the invoking application so that the invoked application does not get bogged down by more unnecessary retries. Taking into consideration the constraints imposed by the locking mechanism in Actors, applying resiliency policies can be even more beneficial both to the actor instance and to the client Dapr applications.

A special trait of the resiliency feature that’s applied to Actors is that the circuit breaker policy type can have a scope that involves all the actor instances of a certain type, each actor by its ID, or both.

There is still one major concept left to assimilate regarding the Dapr actor model: the actor’s lifetime.

Lifetime

Actors in Dapr are not explicitly created; they are brought to life by invoking them (say Actor, Actor, Actor three times in front of a mirror and they will appear!) instead.

In the scope of the Dapr .NET SDK, the actor’s implementation can override the OnActivateAsync method from the base class to intercept the activation moment.

An actor will be deactivated after a period of inactivity. Deactivation is temporary as the Dapr runtime is ready to rehydrate it back into memory from the persisted state store if a new request arises. The idle timeout of each actor type can be configured. So, in your architecture, you can define both long-running actors and short-lived ones.

An actor instance can become aware of its deactivation if we override the OnDeactivateAsync method of the base class. Any interaction with the actor’s instance extends its lifetime as it restarts the timeout clock.

Reminders and timers are two important features of Dapr actors. Both are useful for scheduling activity on an actor, whether it is recurring or intended to be executed only once. As an example, let’s say you want to delay executing your service code from the initial request. The two differ in terms of their behavior, as highlighted here:

  • Timers can trigger while the actor is active, but once deactivated, the timer is no longer effective, and it does not keep the actor active.
  • Reminders are persistent, as they trigger the registered method even on a deactivated actor. Due to this, a reminder extends the actor’s lifetime.

Now that we have learned about the lifetime, placement, consistency, and concurrency of the Dapr actor model, we are ready to apply this knowledge and implement the changes we’ve discussed so far in our small e-commerce solution.

Implementing actors in an e-commerce reservation system

Equipped with information about the actor’s pattern and with a plan to implement the evolution of our sample project by introducing Dapr actors, we now have several steps to complete, as follows:

  1. Preparing the actor’s projects
  2. Implementing the actor’s model
  3. Accessing actors from other Dapr applications
  4. Inspecting the actor’s state

Let’s start by creating the .NET projects.

Preparing the Actor’s projects

To implement actors with Dapr in C#, we must create an actor interface project that’s separate from the actor’s service implementation in two different projects.

The actor’s interface will be referenced by the other services or clients that need to interact with the actors. Let’s create the interface project, as follows:

PS C:Reposdapr-sampleschapter07> dotnet new classlib -o
sample.microservice.reservationitemactor.interfaces
PS C:Reposdapr-sampleschapter07> cd .sample.microservice.
reservationactor.interfaces
PS C:Reposdapr-sampleschapter08sample.microservice.
reservationactor.service> dotnet add package Dapr.Actors -v
1.7.0

The following command is going to create the project for the reservationactor-service Dapr application:

PS C:Reposdapr-sampleschapter07> dotnet new webapi -o
sample.microservice.reservationitemactor.service
PS C:Reposdapr-sampleschapter07> cd .sample.microservice.
reservationactor.service
PS C:Reposdapr-sampleschapter08sample.microservice.
reservationactor.service> dotnet add reference ..sample.
microservice.reservationactor.interfacessample.microservice.
reservationactor.interfaces.csproj
PS C:Reposdapr-sampleschapter08sample.microservice.
reservationactor.service> dotnet add package Dapr.Actors -v
1.7.0
PS C:Reposdapr-sampleschapter08sample.microservice.
reservationactor.service> dotnet add package Dapr.Actors.
AspNetCore -v 1.7.0

Each project also refers to the .NET SDK for Actors, located in the Dapr.Actors package. The reservationactor-service project refers to the Dapr.Actors.AspNetCore package for Actors in ASP.NET.

Now that our projects are ready, we can implement the actors.

Implementing the actor’s model

Let’s start by implementing the actor interface in the IReservationItemActor.cs class of the sample.microservice.reservationitemactor.interfaces project, as follows:

using Dapr.Actors;
namespace sample.microservice.reservationactor.interfaces;
public interface IReservationItemActor : IActor
{
    Task<int> AddReservation(int quantity);
    Task<int> GetBalance();
    Task RegisterReminder();
    Task UnregisterReminder();
    Task RegisterTimer();
    Task UnregisterTimer();
}

The IReservationItemActor interface, which inherits from IActor in Dapr.Actors, is as simple as the tasks our actor will fulfill: AddReservation will receive a reservation for an SKU, while GetBalance will return its available quantity.

The other methods in IReservationItemActor are present in the code to showcase the Reminder and Timer features of Dapr Actors, which we briefly described in the previous section, Actor concurrency, consistency, and lifetime. To learn more about Reminder and Timer, I recommend the Dapr documentation at https://docs.dapr.io/developing-applications/building-blocks/actors/howto-actors/.

Let’s move on to the implementation in the ReservationItemActor.cs class in the sample.microservice.reservationactor.service.service project, as follows:

namespace sample.microservice.reservationactor.service;
internal class ReservationItemActor : Actor,
  IReservationItemActor, IRemindable
{
    public const string StateKey = "reservationitem";
    public ReservationItemActor(ActorHost host)
        : base(host)
    {
    }
    protected override Task OnActivateAsync()
    {
        Console.WriteLine($"Activating actor id: {this.Id}");
        return Task.CompletedTask;
    }
    protected override Task OnDeactivateAsync()
    {
        Console.WriteLine($"Deactivating actor id: {this.Id}");
        return Task.CompletedTask;
    }
… class continue below …

In the previous section of the ReservationItemActor.cs file, we can see a typical definition of a Dapr actor type in C#: the ReservationItemActor class derives from Dapr.Actors.Runtime.Actor, which implements the interface of our IReservationItemActor actor interface and the IRemindable interface from the Dapr.Actors.Runtime namespace. The latter interface is used to enable reminders, a powerful feature that’s used to influence the actor life cycle.

The ReservationItemActor.cs file continues like this:

… class continue from above …
    public async Task<int> AddReservation(int quantity)
    {
        var SKU = this.Id.GetId();
        var state = await this.StateManager.
        TryGetStateAsync<ItemState>(StateKey);
… omitted …
        await this.StateManager.SetStateAsync<ItemState>(
            StateKey, value);
        Console.WriteLine($"Balance of {SKU} was
        {initialBalanceQuantity}, now 
        {value.BalanceQuantity}");
        return value.BalanceQuantity;
    }
    public async Task<int> GetBalance()
    {
        var state = await this.StateManager.
          GetStateAsync<ItemState>(StateKey);
        return state.BalanceQuantity;
    }
… omitted …
}

In the method’s implementation, it’s worth noting how an instance can access the state store via the Dapr actor object model.

With the TryGetStateAsync<type>(StateKey) method of StateManager, which is implemented in the Dapr.Actors.Runtime.Actor base class, the actor attempts to retrieve a state with a StateKey key from the store. The state might not exist yet if the actor instance has only just been implicitly created. Alternatively, you can use StateManager.GetStateAsync<type>(StateKey).

With the StateManager.SetStateAsync<type>(StateKey, value) method, you inform the Dapr runtime to save the serializable value in the state element with the StateKey key, after the actor method completes successfully.

Our actor implementation is not ready yet; the actor type we just implemented must be registered with the Dapr runtime. The following is from the Program.cs file, which is leveraging the ASP.NET 6 Minimal Hosting feature:

var builder = WebApplication.CreateBuilder(args);
var jsonOpt = new JsonSerializerOptions()
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    PropertyNameCaseInsensitive = true,
};
builder.Services.AddActors(options =>
{
    options.Actors.RegisterActor<ReservationItemActor>();
    options.JsonSerializerOptions = jsonOpt;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.MapActorsHandlers();
app.Run();

As you can see, an actor service has a different implementation than a standard service in Dapr, also from a configuration perspective. builder.Services.AddActors adds support for the Actor runtime, while options.Actors.RegisterActor registers the actor of the ReservationItemActor type. In the end, the MapActorsHandlers method registers the Dapr-specific routes for Actors.

The ASP.NET project that’s implementing our Dapr Actors is now ready to be made reachable by the other Dapr applications.

Accessing actors from other Dapr applications

First, we will create a separate project that will contain the actor interface so that we can reference it from clients.

As depicted in Figure 8.2, we decided to keep the interaction with actors of the ReservationItemActor type in the scope of reservationactor-service. We will add the reference here, as follows:

PS C:Reposdapr-sampleschapter08> cd .sample.microservice.
reservation
PS C:Reposdapr-sampleschapter08sample.microservice.
reservation> dotnet add reference ..sample.microservice.
reservationactor.interfacessample.microservice.
reservationactor.interfaces.csproj

We should also add the reference to the Dapr.Actors package in reservationactor-service, as we did in the previous projects.

In the previous chapters, our code directly accessed the state store information via the DaprClient instance, which is what our ASP.NET controllers have been injected with by Dapr. The change we will apply to ReservationController.cs will impact this part:

… omitted …
[Topic(PubSub, common.Topics.OrderSubmittedTopicName)]
[HttpPost(common.Topics.OrderSubmittedTopicName)]
public async Task<ActionResult<OrderReservation>>
ReserveOrder(Order order, [FromServices] DaprClient daprClient)
{
   var stateReservation = await daprClient.
   GetStateEntryAsync<ReservationState>(StoreName_reservation,
    order.Id.ToString());
   stateReservation.Value ??= new ReservationState(){OrderId =
      order.Id, ReservedItems = new List<ItemReservation>() };
   var result = new OrderReservation(){OrderId = order.Id,
      ReservedItems = new List<Item>()};
   foreach (var item in order.Items)
   {
      var SKU = item.ProductCode;
      var quantity = item.Quantity;
      var actorID = new ActorId(SKU);
      var proxy = ActorProxy.Create<IReservationItemActor>
        (actorID,"ReservationItemActor");
      var balanceQuantity = await proxy.
        AddReservation(quantity);
      result.ReservedItems.Add(new Item{SKU = SKU,
        BalanceQuantity = balanceQuantity});
}
… omitted …

There are a few things to examine in the previous code snippet. By instantiating the ActorId(SKU) type, we pass it as a parameter in ActorProxy.Create<IReservationItemActor>(actorID,"ReservationItemActor"). We are instructing the Dapr runtime to look for – in the map kept in sync by the Dapr placement service – the actor service instance that’s responsible for handling the specific actor with the key equal to the SKU.

The instance of Dapr.Actors.Client.ActorProxy we just created lets us invoke the actor by leveraging its interface as if it were a local object. The proxy.AddReservation(quantity) method is what we defined previously in the IReservationItemActor interface and implemented in ReservationItemActor.

All the concepts we described previously in this chapter hide behind this simple object model – isn’t it nice, elegant, and simple?

We can launch the Dapr applications as we did previously, with the command-line interface (CLI) or Tye, now with the addition of the newly created reservationactor-service Actor service from the sample.microservice.reservationactor.service.csproj project, as follows:

dapr run --app-id "reservationactor-service" --app-port "5004"
--dapr-grpc-port "50040" --dapr-http-port "5014" --components-
path "./components" -- dotnet run --project ./sample.
microservice.reservationactor.service/sample.microservice.
reservationactor.service.csproj --urls="http://+:5004"

To appreciate the changes that have been applied to the architecture, we can submit an order, as described in the order.test.http test file, by invoking the ASP.NET controller for order-service, which is currently configured to be exposed at http://localhost:5001/order, or by reaching it via the Dapr sidecar at http://localhost:5010/v1.0/invoke/order-service/method/order.

The following is the output from reservation-service:

== APP == Reservation in d6082d80-1239-45db-9d35-95e587d7b299 of rockiecookie for 4, balance 32
== APP == Reservation in d6082d80-1239-45db-9d35-95e587d7b299 of bussola8 for 7, balance 59
== APP == Reservation in d6082d80-1239-45db-9d35-95e587d7b299 of crazycookie for 2, balance 245
== APP == Reservation in d6082d80-1239-45db-9d35-95e587d7b299 completed

Let’s focus on the output from reservationactor-service, shown here:

== APP == Actor: rockiecookie Activated
== APP == Balance of rockiecookie was 36, now 32
== APP == Activating actor id: bussola8
== APP == Actor: bussola8 Activated
== APP == Balance of bussola1 was 6, now 4
== APP == Balance of crazycookie was 247, now 245

Here, we can see that some actors have been implicitly activated by us interacting with them, while other ones were already active in the host’s memory.

In the previous test interaction, reservation-service, reached by order-service, invoked the Dapr application’s reservationactor-service by leveraging the .NET SDK. Even though we do not manually compose URLs, it is useful to understand the different syntax to interact with the Actor building block.

In Chapter 4, Service-to-Service Invocation, we understood that the Dapr service-to-service building block follows this pattern in composing URLs:

http://localhost:3500/v1.0/invoke/app-id/method/methodname

The app-id and methodname elements are the name of the Dapr application we need to reach and the name of the method we want to invoke, respectively.

Similarly, in Chapter 5, Introducing State Management, we understood how the Dapr state management building block adopts the following pattern:

http://localhost:3500/v1.0/state/storename/key

The storename element is the name of a configured state store, and key is used to identify the key/value pair of interest.

Now, let’s observe the more complex URL pattern adopted by the Actor building block:

http://localhost:3500/v1.0/actors/actortype/actorid/method/
methodname

The previous URL invokes a method of an instance. The actortype element specifies the type of the Actor, actorid identifies the specific instance, and methodname is the name of the method we want to invoke on the Actor instance.

To interact with the state, the Actor building block relies on the following URL pattern:

http://localhost:3500/v1.0/actors/actortype/actorid/state/key

The key element identifies the key/value pair of interest in the state store. In the Actor building block, only one state store can be configured, and no element is needed in the URL.

Now that we have proved how easy it is to introduce the actor model to an existing Dapr application, we should verify how our actors’ data gets persisted.

Inspecting the actor’s state

So far, we’ve learned how to configure a Dapr state store to make it suitable for actors. It is interesting to note how the state key is composed, as shown by the following item in Azure Cosmos DB, which corresponds to an actor’s state:

{
    "id": "reservationactor-service||ReservationItemActor||
        cookie2||reservationitem",
    "value": {
        "BalanceQuantity": -12,
        "Changes": [
            {
                "Quantity": 12,
                "ReservedOn": "2022-04-
                  09T22:17:47.0511873Z",
                "SKU": "cookie2"
            }
        ],
        "SKU": "cookie2"
    },
    "partitionKey": "reservationactor-service||ReservationItem
      Actor||cookie2",
    "_rid": "h+ETAIFd00cBAAAAAAAAAA==",
    "_self": "dbs/h+ETAA==/colls/h+ETAIFd00c=/docs/
      h+ETAIFd00cBAAAAAAAAAA==/",
    "_etag": ""0000dd23-0000-0d00-0000-5f80e18a0000"",
    "_attachments": "attachments/",
    "_ts": 1602281866
}

As you can see, the key has been composed with the <application ID>||<actor type>||<actor Id>||<key> pattern, where the following apply:

  • reservationactor-service is the application ID.
  • actor Id is the actor’s unique identifier (UID) that we chose to adopt as the SKU. Here, this is cookie2.
  • ReservationItemActor is the actor type.
  • key identifies the state key that’s used, which is reservationitem here. An actor can have multiple states.

With that, we have completed our journey of learning about the actor model in Dapr.

Summary

In this chapter, we learned that the actor model that’s supported by Dapr is a very powerful tool in our toolbox.

We understood the scenarios that benefit the most from applying actors, and how to avoid the most common implementation pitfalls.

By configuring Dapr Actors, from the state store to the ASP.NET perspective, we appreciated how the simplicity of Dapr extends to this building block too.

Next, we introduced an actor type to our existing architecture. By doing so, we learned how to separate the contract (interface) from the implementation and invoke it from other Dapr services.

Note that this is another example of how Dapr facilitates the development of microservice architectures by addressing the communication and discovery of services (how easy is it for a client to interact with an actor?). It also unleashes the independent evolution of our architecture’s components; introducing actors in our example has been seamless for the rest of the services. With this new information and experience, you will be able to identify how to best introduce Dapr Actors into your new or existing solution.

Now that our sample solution is complete, we are ready to move it to an environment that’s capable of exposing it to the hungry and festive customers of Biscotti Brutti Ma Buoni. Starting from the next chapter, we will learn how to prepare the Dapr-based solution so that we can deploy it on Kubernetes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.210.133