Production Reference: Actors

Running PHP actors in production

Proxy modes

There are four different modes actor proxies are handled. Each mode presents different trade-offs that you’ll need to weigh during development and in production.

  1. <?php
  2. \Dapr\Actors\Generators\ProxyFactory::GENERATED;
  3. \Dapr\Actors\Generators\ProxyFactory::GENERATED_CACHED;
  4. \Dapr\Actors\Generators\ProxyFactory::ONLY_EXISTING;
  5. \Dapr\Actors\Generators\ProxyFactory::DYNAMIC;

It can be set with dapr.actors.proxy.generation configuration key.

This is the default mode. In this mode, a class is generated and eval‘d on every request. It’s mostly for development and shouldn’t be used in production.

This is the same as ProxyModes::GENERATED except the class is stored in a tmp file so it doesn’t need to be regenerated on every request. It doesn’t know when to update the cached class, so using it in development is discouraged but is offered for when manually generating the files isn’t possible.

In this mode, an exception is thrown if the proxy class doesn’t exist. This is useful for when you don’t want to generate code in production. You’ll have to make sure the class is generated and pre-/autoloaded.

Generating proxies

You can create a composer script to generate proxies on demand to take advantage of the ONLY_EXISTING mode.

Create a ProxyCompiler.php

  1. <?php
  2. class ProxyCompiler {
  3. private const PROXIES = [
  4. MyActorInterface::class,
  5. MyOtherActorInterface::class,
  6. ];
  7. private const PROXY_LOCATION = __DIR__.'/proxies/';
  8. public static function compile() {
  9. try {
  10. $app = \Dapr\App::create();
  11. foreach(self::PROXIES as $interface) {
  12. $output = $app->run(function(\DI\FactoryInterface $factory) use ($interface) {
  13. return \Dapr\Actors\Generators\FileGenerator::generate($interface, $factory);
  14. });
  15. $reflection = new ReflectionClass($interface);
  16. $dapr_type = $reflection->getAttributes(\Dapr\Actors\Attributes\DaprType::class)[0]->newInstance()->type;
  17. $filename = 'dapr_proxy_'.$dapr_type.'.php';
  18. file_put_contents(self::PROXY_LOCATION.$filename, $output);
  19. echo "Compiled: $interface";
  20. }
  21. } catch (Exception $ex) {
  22. echo "Failed to generate proxy for $interface\n{$ex->getMessage()} on line {$ex->getLine()} in {$ex->getFile()}\n";
  23. }
  24. }
  25. }

Then add a psr-4 autoloader for the generated proxies and a script in composer.json:

  1. {
  2. "autoload": {
  3. "psr-4": {
  4. "Dapr\\Proxies\\": "path/to/proxies"
  5. }
  6. },
  7. "scripts": {
  8. "compile-proxies": "ProxyCompiler::compile"
  9. }
  10. }

And finally, configure dapr to only use the generated proxies:

  1. <?php
  2. // in config.php
  3. return [
  4. 'dapr.actors.proxy.generation' => ProxyFactory::ONLY_EXISTING,
  5. ];

In this mode, the proxy satisfies the interface contract, however, it does not actually implement the interface itself (meaning instanceof will be false). This mode takes advantage of a few quirks in PHP to work and exists for cases where code cannot be eval‘d or generated.

Requests

Creating an actor proxy is very inexpensive for any mode. There are no requests made when creating an actor proxy object.

When you call a method on a proxy object, only methods that you implemented are serviced by your actor implementation. get_id() is handled locally, and get_reminder(), delete_reminder(), etc. are handled by the daprd.

Actor implementation

Every actor implementation in PHP must implement \Dapr\Actors\IActor and use the \Dapr\Actors\ActorTrait trait. This allows for fast reflection and some shortcuts. Using the \Dapr\Actors\Actor abstract base class does this for you, but if you need to override the default behavior, you can do so by implementing the interface and using the trait.

Activation and deactivation

When an actor activates, a token file is written to a temporary directory (by default this is in '/tmp/dapr_' + sha256(concat(Dapr type, id)) in linux and '%temp%/dapr_' + sha256(concat(Dapr type, id)) on Windows). This is persisted until the actor deactivates, or the host shuts down. This allows for on_activation to be called once and only once when Dapr activates the actor on the host.

Performance

Actor method invocation is very fast on a production setup with php-fpm and nginx, or IIS on Windows. Even though the actor is constructed on every request, actor state keys are only loaded on-demand and not during each request. However, there is some overhead in loading each key individually. This can be mitigated by storing an array of data in state, trading some usability for speed. It is not recommended doing this from the start, but as an optimization when needed.

Versioning state

The names of the variables in the ActorState object directly correspond to key names in the store. This means that if you change the type or name of a variable, you may run into errors. To get around this, you may need to version your state object. In order to do this, you’ll need to override how state is loaded and stored. There are many ways to approach this, one such solution might be something like this:

  1. <?php
  2. class VersionedState extends \Dapr\Actors\ActorState {
  3. /**
  4. * @var int The current version of the state in the store. We give a default value of the current version.
  5. * However, it may be in the store with a different value.
  6. */
  7. public int $state_version = self::VERSION;
  8. /**
  9. * @var int The current version of the data
  10. */
  11. private const VERSION = 3;
  12. /**
  13. * Call when your actor activates.
  14. */
  15. public function upgrade() {
  16. if($this->state_version < self::VERSION) {
  17. $value = parent::__get($this->get_versioned_key('key', $this->state_version));
  18. // update the value after updating the data structure
  19. parent::__set($this->get_versioned_key('key', self::VERSION), $value);
  20. $this->state_version = self::VERSION;
  21. $this->save_state();
  22. }
  23. }
  24. // if you upgrade all keys as needed in the method above, you don't need to walk the previous
  25. // keys when loading/saving and you can just get the current version of the key.
  26. private function get_previous_version(int $version): int {
  27. return $this->has_previous_version($version) ? $version - 1 : $version;
  28. }
  29. private function has_previous_version(int $version): bool {
  30. return $version >= 0;
  31. }
  32. private function walk_versions(int $version, callable $callback, callable $predicate): mixed {
  33. $value = $callback($version);
  34. if($predicate($value) || !$this->has_previous_version($version)) {
  35. return $value;
  36. }
  37. return $this->walk_versions($this->get_previous_version($version), $callback, $predicate);
  38. }
  39. private function get_versioned_key(string $key, int $version) {
  40. return $this->has_previous_version($version) ? $version.$key : $key;
  41. }
  42. public function __get(string $key): mixed {
  43. return $this->walk_versions(
  44. self::VERSION,
  45. fn($version) => parent::__get($this->get_versioned_key($key, $version)),
  46. fn($value) => isset($value)
  47. );
  48. }
  49. public function __isset(string $key): bool {
  50. return $this->walk_versions(
  51. self::VERSION,
  52. fn($version) => parent::__isset($this->get_versioned_key($key, $version)),
  53. fn($isset) => $isset
  54. );
  55. }
  56. public function __set(string $key,mixed $value): void {
  57. // optional: you can unset previous versions of the key
  58. parent::__set($this->get_versioned_key($key, self::VERSION), $value);
  59. }
  60. public function __unset(string $key) : void {
  61. // unset this version and all previous versions
  62. $this->walk_versions(
  63. self::VERSION,
  64. fn($version) => parent::__unset($this->get_versioned_key($key, $version)),
  65. fn() => false
  66. );
  67. }
  68. }

There’s a lot to be optimized, and it wouldn’t be a good idea to use this verbatim in production, but you can get the gist of how it would work. A lot of it will depend on your use case which is why there’s not something like this in the SDK. For instance, in this example implementation, the previous value is kept for where there may be a bug during an upgrade; keeping the previous value allows for running the upgrade again, but you may wish to delete the previous value.

Last modified January 1, 0001