How-to: Enable and use actor reentrancy in Dapr

Learn more about actor reentrancy

A core tenet of the virtual actor pattern is the single-threaded nature of actor execution. Without reentrancy, the Dapr runtime locks on all actor requests. A second request wouldn’t be able to start until the first had completed. This means an actor cannot call itself, or have another actor call into it, even if it’s part of the same call chain.

Reentrancy solves this by allowing requests from the same chain, or context, to re-enter into an already locked actor. This proves useful in scenarios where:

  • An actor wants to call a method on itself
  • Actors are used in workflows to perform work, then call back onto the coordinating actor.

Examples of chains that reentrancy allows are shown below:

  1. Actor A -> Actor A
  2. ActorA -> Actor B -> Actor A

With reentrancy, you can perform more complex actor calls, without sacrificing the single-threaded behavior of virtual actors.

Diagram showing reentrancy for a coordinator workflow actor calling worker actors or an actor calling an method on itself

The maxStackDepth parameter sets a value that controls how many reentrant calls can be made to the same actor. By default, this is set to 32, which is more than sufficient in most cases.

Configure the actor runtime to enable reentrancy

The reentrant actor must provide the appropriate configuration. This is done by the actor’s endpoint for GET /dapr/config, similar to other actor configuration elements.

  1. public class Startup
  2. {
  3. public void ConfigureServices(IServiceCollection services)
  4. {
  5. services.AddSingleton<BankService>();
  6. services.AddActors(options =>
  7. {
  8. options.Actors.RegisterActor<DemoActor>();
  9. options.ReentrancyConfig = new Dapr.Actors.ActorReentrancyConfig()
  10. {
  11. Enabled = true,
  12. MaxStackDepth = 32,
  13. };
  14. });
  15. }
  16. }
  1. import { CommunicationProtocolEnum, DaprClient, DaprServer } from "@dapr/dapr";
  2. // Configure the actor runtime with the DaprClientOptions.
  3. const clientOptions = {
  4. actor: {
  5. reentrancy: {
  6. enabled: true,
  7. maxStackDepth: 32,
  8. },
  9. },
  10. };
  1. from fastapi import FastAPI
  2. from dapr.ext.fastapi import DaprActor
  3. from dapr.actor.runtime.config import ActorRuntimeConfig, ActorReentrancyConfig
  4. from dapr.actor.runtime.runtime import ActorRuntime
  5. from demo_actor import DemoActor
  6. reentrancyConfig = ActorReentrancyConfig(enabled=True)
  7. config = ActorRuntimeConfig(reentrancy=reentrancyConfig)
  8. ActorRuntime.set_actor_config(config)
  9. app = FastAPI(title=f'{DemoActor.__name__}Service')
  10. actor = DaprActor(app)
  11. @app.on_event("startup")
  12. async def startup_event():
  13. # Register DemoActor
  14. await actor.register_actor(DemoActor)
  15. @app.get("/MakeExampleReentrantCall")
  16. def do_something_reentrant():
  17. # invoke another actor here, reentrancy will be handled automatically
  18. return

Here is a snippet of an actor written in Golang providing the reentrancy configuration via the HTTP API. Reentrancy has not yet been included into the Go SDK.

  1. type daprConfig struct {
  2. Entities []string `json:"entities,omitempty"`
  3. ActorIdleTimeout string `json:"actorIdleTimeout,omitempty"`
  4. ActorScanInterval string `json:"actorScanInterval,omitempty"`
  5. DrainOngoingCallTimeout string `json:"drainOngoingCallTimeout,omitempty"`
  6. DrainRebalancedActors bool `json:"drainRebalancedActors,omitempty"`
  7. Reentrancy config.ReentrancyConfig `json:"reentrancy,omitempty"`
  8. }
  9. var daprConfigResponse = daprConfig{
  10. []string{defaultActorType},
  11. actorIdleTimeout,
  12. actorScanInterval,
  13. drainOngoingCallTimeout,
  14. drainRebalancedActors,
  15. config.ReentrancyConfig{Enabled: true, MaxStackDepth: &maxStackDepth},
  16. }
  17. func configHandler(w http.ResponseWriter, r *http.Request) {
  18. w.Header().Set("Content-Type", "application/json")
  19. w.WriteHeader(http.StatusOK)
  20. json.NewEncoder(w).Encode(daprConfigResponse)
  21. }

Handle reentrant requests

The key to a reentrant request is the Dapr-Reentrancy-Id header. The value of this header is used to match requests to their call chain and allow them to bypass the actor’s lock.

The header is generated by the Dapr runtime for any actor request that has a reentrant config specified. Once it is generated, it is used to lock the actor and must be passed to all future requests. Below is an example of an actor handling a reentrant request:

  1. func reentrantCallHandler(w http.ResponseWriter, r *http.Request) {
  2. /*
  3. * Omitted.
  4. */
  5. req, _ := http.NewRequest("PUT", url, bytes.NewReader(nextBody))
  6. reentrancyID := r.Header.Get("Dapr-Reentrancy-Id")
  7. req.Header.Add("Dapr-Reentrancy-Id", reentrancyID)
  8. client := http.Client{}
  9. resp, err := client.Do(req)
  10. /*
  11. * Omitted.
  12. */
  13. }

Demo

Watch this video on how to use actor reentrancy.

Next steps

Actors in the Dapr SDKs

Last modified March 21, 2024: Merge pull request #4082 from newbe36524/v1.13 (f4b0938)