introduce
The actor mode describes the actor as the lowest level "cell". In other words, you write code in a separate unit (called an actor) that receives messages and processes them one at a time without any concurrency or threads.
In other words, after dividing independent computing units according to ActorId, the same ActorId re-entry needs to be queued, which can be understood as lock(ActorId)
Note: there is a counter example here, that is, the introduction of reentry. This concept is still Preview at present. It allows repeated entry in the same chain. The judgment standard is not as simple as ActorId, that is, it is allowed to adjust itself. This is off by default and needs to be turned on manually, that is, it is not allowed to adjust itself by default.
When your code processes a message, it can send one or more messages to other participants, or create new participants. The underlying runtime manages how, when and where each participant runs, and routes messages between participants.
A large number of actors can be executed at the same time, and the actors execute independently of each other.
Dapr contains a runtime that specifically implements the Virtual Actor pattern. Through the implementation of dapr, you can write Dapr Actor according to the Actor model, and dapr takes advantage of the scalability and reliability guarantee provided by the underlying platform.
When do I use Actors
The Actor design pattern is well suited to many distributed system problems and scenarios, but the first thing you should consider is the constraints of the pattern. In general, consider using Actors mode to model your problem or scene if:
- Your problem space involves a large number (thousands or more) of small, independent, and isolated States and logical units.
- You want to use single threaded objects that do not require a lot of interaction with external components, including querying status across a set of Actors.
- Your Actor instance does not block callers with unpredictable delays by issuing I/O operations.
Dapr Actor
Each Actor is defined as an instance of the Actor type, just as an object is an instance of a class. For example, there may be an Actor type that performs calculator functions, and there may be many actors of this type distributed on various nodes of the cluster. Each such Actor is uniquely identified by an Acotr ID.
life cycle
Dapr Actors are virtual, which means that their life cycle has nothing to do with their memory performance. Therefore, they do not need to be explicitly created or destroyed. The Dapr Actors runtime automatically activates the Actor when it first receives a request for the Actor ID. If an Actor is not used for a period of time, the Dapr Actors runtime will garbage collect objects in memory. If it needs to be reactivated later, it will also maintain an understanding of the presence of participants. If it needs to be reactivated later, it will also keep all the original data to the Actor.
Calling the Actor method and the reminder will reset the idle time. For example, the reminder trigger will keep the Actor active. Whether the Actor is active or inactive, the Actor reminder will be triggered. If it is triggered for an inactive Actor, it will activate the Actor first. The Actor timer does not reset the idle time, so timer triggering does not keep the Actor active. The timer is triggered only when the Actor is active.
The biggest difference between Reminders and Timers is that Reminders will keep the Actor active, while Timers will not
The idle timeout and scan interval used by the Dapr runtime to check whether the Actor can be garbage collected are configurable. This information can be passed when the Dapr runtime calls the Actor service to get supported Actor types.
Due to the existence of the Virtual Actor model, this Virtual Actor life cycle abstraction brings some considerations. In fact, the implementation of Dapr Actors sometimes deviates from this model.
The first time a message is sent to the Actor ID, the Actor is automatically activated (causing the Actor object to be built). After a period of time, the Actor object will be garbage collected. Using the Actor ID again after being recycled will result in the construction of a new Actor object. The state of the Actor is longer than the life cycle of the object because the state is stored in the state management component configured by the Dapr runtime.
Note: before the Actor is garbage collected, the Actor object will be reused. This leads to a problem. In the. Net Actor class, the constructor will only be called once during the lifetime of the Actor.
Distribution and failover
In order to provide scalability and reliability, Actor instances are distributed throughout the cluster, and Dapr automatically migrates them from failed nodes to healthy nodes as needed.
Actors are distributed among instances of Actor services, which are distributed among nodes in the cluster. For a given Actor type, each service instance contains a set of actors.
Dapr placement service
The Dapr Actor runtime manages the distribution scheme and key range settings for you. This is done by the Actor Placement service. When a new instance of a service is created, the corresponding Dapr runtime registers the Actor types it can create, and places the service to calculate the partitions of all instances of a given Actor type. The partition information table of each Actor type is updated and stored in each Dapr instance running in the environment, and can change dynamically with the creation and destruction of a new instance of the Actor service. This is shown in the figure below:
When the client calls an actor with a specific ID (for example, Actor ID 123), the client's Dapr instance will hash the actor type and ID, and use this information to call the corresponding Dapr instance that can serve the request of a specific Actor ID. Therefore, the same partition (or service instance) is always called for any given Actor ID. This is shown in the figure below:
This simplifies some options, but also brings some considerations:
- By default, actors are randomly placed in the pod to achieve uniform distribution.
- Because actors are placed randomly, it should be expected that Actor operations always require network communication, including serialization and deserialization of method call data, resulting in latency and overhead.
Note: the Dapr Actor placement service is only used for Actor placement, so if your service does not use Dapr Actors, it is not required. The placement service can run in all managed environments, including self managed and Kubernetes.
Actor communication
You can call Actor via HTTP/gRPC or SDK.
POST/GET/PUT/DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/<method/state/timers/reminders>
Concurrent
The Dapr Actor runtime provides a simple turn based access model for accessing Actor methods. This means that at no time can more than one thread be active in the code of the Actor object.
A single Actor instance cannot process multiple requests at a time. If concurrent requests are expected to be processed, the Actor instance may cause throughput bottlenecks.
A single Actor instance refers to the Actor object corresponding to each Actor ID. If a single Actor is not concurrent, there is no problem
If there is a circular request between two actors and an external request is made to one of them at the same time, there may be an impasse between the actors. The Dapr Actor runtime automatically times out the Actor call and throws an exception to the caller to interrupt a possible deadlock condition.
Reentrant (Preview)
As an enhancement to the basic Actor in dapr. Now reentry is the preview function. Interested partners can see the official documents.
Turn based access
A round includes the full execution of an Actor method in response to requests from other actors or clients, or the full execution of a timer / reminder callback. Even if these methods and callbacks are asynchronous, the Dapr Actor runtime does not cross them. A round must be completely completed before a new round is allowed. In other words, the currently executing Actor method or timer / reminder callback must be fully completed to allow new calls to the method or callback.
The Dapr Actor runtime achieves round based concurrency by obtaining the lock of each Actor at the beginning of the round and releasing the lock at the end of the round. Therefore, round based concurrency is performed on a per Actor basis, not across actors. The Actor method and timer / reminder callback can be executed simultaneously on behalf of different actors.
The following examples illustrate the above concepts. Consider implementing Actor types for two asynchronous methods (for example, Method1 and Method2), timers, and reminders. The following figure shows an example timeline representing the execution of these methods and callbacks by two Actors (ActorId1 and ActorId2) belonging to this Actor type.
Actor state management
Actor can use the state management function to reliably save the state. You can interact with Dapr through the HTTP/gRPC endpoint for state management.
To use actor s, your state store must support transactions. This means that your state store component must implement the TransactionalStore interface. Only one state store component can be used as a state store for all participants.
Transaction support list: https://docs.dapr.io/referenc...
Note: it is recommended to use Redis when learning. All official examples are also based on Redis, which is easy to use, and Dapr init is integrated by default
Actor timer and reminder
Actor can schedule its regular work by registering timers or reminders.
The functions of timer and reminder are very similar. The main difference is that the Dapr Actor runtime does not retain any information about timers after deactivation, but uses the Dapr Actor status provider to retain information about reminders.
The scheduling configuration of timer and reminder is the same, which is summarized as follows:
DueTime is an optional parameter that sets the time or interval before the callback is called for the first time. If DueTime is omitted, the callback is called immediately after timer / reminder registration.
Supported formats:
- RFC3339 date format, e.g. 2020-10-02T15:00:00Z
- time.Duration format, for example 2h30m
- ISO 8601 duration format, e.g. PT2H30M
Period is an optional parameter that sets the time interval between two consecutive callback calls. When specified in ISO 8601-1 duration format, you can also configure the number of repetitions to limit the total number of callback calls. If period is omitted, the callback will be called only once.
Supported formats:
- time.Duration format, for example 2h30m
- ISO 8601 duration format, e.g. PT2H30M, R5/PT1M30S
ttl is an optional parameter used to set the time or interval between timer / reminder expiration and deletion. If ttl is omitted, no restrictions apply.
Supported formats:
- RFC3339 date format, e.g. 2020-10-02T15:00:00Z
- time.Duration format, for example 2h30m
- ISO 8601 duration format, e.g. PT2H30M
When you specify both the number of repetitions in the cycle and ttl, the timer / reminder will stop when either condition is met.
Actor runtime configuration
- Actor idletimeout - timeout before deactivating an idle actor. Each actor scaninterval interval checks for timeouts. Default: 60 minutes
- actorScanInterval - specifies the duration of how often actors are scanned to deactivate idle actors. Idle time exceeds Actor_ idle_ The timeout Actor will be disabled. Default: 30 seconds
- Drawongoingcalltimeout - the duration during which Rebalanced actors are exhausted. This specifies the timeout for the completion of the currently active Actor method. If there is currently no Actor method call, this item is ignored. Default: 60 seconds
Drawnrebalancedactors - if true, Dapr will wait for the drawongoingcalltimeout duration to allow the current role call to complete before attempting to deactivate the role. Default value: true
Drawnrebalancedactors and drawongoingcalltimeout above should be used together
- Reentrance - (actorreentracyconfig) - configure the role's reentry behavior. If not provided, reentrant is disabled. Default: disabled, 0
- Reminderstoragepartitions - configure the number of partitions for Actor alerts. If not provided, all reminders are saved as a single record in the Actor status store. Default: 0
// In Startup.cs public void ConfigureServices(IServiceCollection services) { // Register actor runtime with DI services.AddActors(options => { // Register actor types and configure actor settings options.Actors.RegisterActor<MyActor>(); // Configure default settings options.ActorIdleTimeout = TimeSpan.FromMinutes(60); options.ActorScanInterval = TimeSpan.FromSeconds(30); options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(60); options.DrainRebalancedActors = true; options.RemindersStoragePartitions = 7; // reentrancy not implemented in the .NET SDK at this time }); // Register additional services for use with actors services.AddSingleton<BankService>(); }
Partition reminder (Preview)
After the sidecar restarts, the actor reminder remains and continues to trigger. Before Dapr runtime version 1.3, reminders were saved on a single record in the actor state store.
This is the Preview function. If you are interested, you can see the official documents
. Net calls the Actor of Dapr
Unlike in the past, the Actor example will create a shared class library to store the parts shared by the Server and Client
Create Assignment.Shared
Create a class library project, add the Dapr.ActorsNuGet package reference, and finally add the following classes:
AccountBalance.cs
namespace Assignment.Shared; public class AccountBalance { public string AccountId { get; set; } = default!; public decimal Balance { get; set; } }
IBankActor.cs
Note: This is the Actor interface, and IActor is provided by the Dapr SDK
using Dapr.Actors; namespace Assignment.Shared; public interface IBankActor : IActor { Task<AccountBalance> GetAccountBalance(); Task Withdraw(WithdrawRequest withdraw); }
OverdraftException.cs
namespace Assignment.Shared; public class OverdraftException : Exception { public OverdraftException(decimal balance, decimal amount) : base($"Your current balance is {balance:c} - that's not enough to withdraw {amount:c}.") { } }
WithdrawRequest.cs
namespace Assignment.Shared; public class WithdrawRequest { public decimal Amount { get; set; } }
Create Assignment.Server
Create a class library project, add the Dapr.Actors.AspNetCoreNuGet package reference and Assignment.Shared project reference, and finally modify the program port to 5000.
Note: the Server is different from the NuGet package of Shared and Client. The Server integrates some functions of the Server
Modify program.cs
var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton<BankService>(); builder.Services.AddActors(options => { options.Actors.RegisterActor<DemoActor>(); }); var app = builder.Build(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapActorsHandlers(); }); app.Run();
Add BankService.cs
using Assignment.Shared; namespace Assignment.Server; public class BankService { // Allow overdraft of up to 50 (of whatever currency). private readonly decimal OverdraftThreshold = -50m; public decimal Withdraw(decimal balance, decimal amount) { // Imagine putting some complex auditing logic here in addition to the basics. var updated = balance - amount; if (updated < OverdraftThreshold) { throw new OverdraftException(balance, amount); } return updated; } }
Add BankActor.cs
using Assignment.Shared; using Dapr.Actors.Runtime; using System; namespace Assignment.Server; public class BankActor : Actor, IBankActor, IRemindable // IRemindable is not required { private readonly BankService bank; public BankActor(ActorHost host, BankService bank) : base(host) { // BankService is provided by dependency injection. // See Program.cs this.bank = bank; } public async Task<AccountBalance> GetAccountBalance() { var starting = new AccountBalance() { AccountId = this.Id.GetId(), Balance = 10m, // Start new accounts with 100, we're pretty generous. }; var balance = await StateManager.GetOrAddStateAsync("balance", starting); return balance; } public async Task Withdraw(WithdrawRequest withdraw) { var starting = new AccountBalance() { AccountId = this.Id.GetId(), Balance = 10m, // Start new accounts with 100, we're pretty generous. }; var balance = await StateManager.GetOrAddStateAsync("balance", starting)!; if (balance.Balance <= 0) { // Simulated reminder deposit if (Random.Shared.Next(100) > 90) { await RegisterReminderAsync("Deposit", null, TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(-1)); } } // Throws Overdraft exception if the account doesn't have enough money. var updated = this.bank.Withdraw(balance.Balance, withdraw.Amount); balance.Balance = updated; await StateManager.SetStateAsync("balance", balance); } public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period) { if (reminderName == "Deposit") { var balance = await StateManager.GetStateAsync<AccountBalance>("balance")!; if (balance.Balance <= 0) { balance.Balance += 60; // 50(Overdraft Threshold) + 10 = 60 Console.WriteLine("Deposit: 10"); } else { Console.WriteLine("Deposit: ignore"); } } } }
Run Assignment.Server
Use the Dapr CLI to start. First use the command line tool to jump to the directory dapr study room \ assignment07 \ assignment.server, and then execute the following command
dapr run --app-id testactor --app-port 5000 --dapr-http-port 3500 --dapr-grpc-port 50001 dotnet run
Create Assignment.Client
Create the console project and add the Dapr.ActorsNuGet package reference and the Assignment.Shared project reference.
Modify Program.cs
using Assignment.Shared; using Dapr.Actors; using Dapr.Actors.Client; Console.WriteLine("Creating a Bank Actor"); var bank = ActorProxy.Create<IBankActor>(ActorId.CreateRandom(), "BankActor"); Parallel.ForEach(Enumerable.Range(1, 10), async i => { while (true) { var balance = await bank.GetAccountBalance(); Console.WriteLine($"[Worker-{i}] Balance for account '{balance.AccountId}' is '{balance.Balance:c}'."); Console.WriteLine($"[Worker-{i}] Withdrawing '{1m:c}'..."); try { await bank.Withdraw(new WithdrawRequest() { Amount = 1m }); } catch (ActorMethodInvocationException ex) { Console.WriteLine("[Worker-{i}] Overdraft: " + ex.Message); } Task.Delay(1000).Wait(); } }); Console.ReadKey();
Run Assignment.Client
Use the Dapr CLI to start. First use the command line tool to jump to the directory dapr study room \ assignment07 \ assignment.client, and then execute the following command
dotnet run
Source code of this chapter
Assignment07
https://github.com/doddgu/dap...
We are moving towards a new framework and a new ecology
Our goal is free, easy to use, flexible, functional and robust.
So we are learning from the design concept of Building blocks and are making a new framework, MASA Framework. What are its characteristics?
- The native supports Dapr and allows Dapr to be replaced by traditional communication methods
- The architecture is unlimited, and single applications, SOA and microservices are supported
- Support. Net native framework, reduce learning burden, and insist on not making new wheels except for concepts that must be introduced in specific fields
- Rich ecological support, in addition to the framework, there are a series of products such as component library, permission center, configuration center, troubleshooting center, alarm center and so on
- The unit test coverage of the core code base is 90%+
- Open source, free, community driven
- What else? We're waiting for you to discuss it together
After several months of production project practice, POC has been completed, and the previous accumulation is being reconstructed into a new open source project
At present, the source code has been synchronized to Github (the document site is under planning and will be gradually improved):
QQ group: 7424099
Wechat group: add technology operation wechat (MasaStackTechOps), note the purpose, and invite to join the group