This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

虚拟 Actor

如何构建 Actor

如果你不熟悉 Actor 模式,了解 Actor 模式的最佳位置是 Actor 概述。

在 PHP SDK 中,Actor 有两个方面,即客户端和 Actor(也称为运行时)。作为 Actor 的客户端, 你将通过 ActorProxy 类与远程 Actor 交互。该类使用若干配置策略之一动态地生成代理类。

在编写 Actor 时,状态可以为你管理。你可以钩入 Actor 生命周期,并定义提醒和定时器。 这为你处理适合 Actor 模式的各类问题提供了相当大的能力。

Actor 代理

每当您需要与 Actor 通信时,都需要获取一个代理对象来执行此操作。代理负责 序列化您的请求、反序列化响应并将其返回给您,同时遵守指定接口定义的契约。

为了创建代理,首先需要一个接口来定义您与 Actor 发送和接收的内容和方式。 例如,如果您想与一个仅跟踪计数的计数 Actor 通信,您可以将接口 定义如下:

<?php
#[\Dapr\Actors\Attributes\DaprType('Counter')]
interface ICount {
    function increment(int $amount = 1): void;
    function get_count(): int;
}

将此接口放在 Actor 和客户端都可以访问的共享库中(如果两者都用 PHP 编写),这是一个好主意。DaprType 属性告诉 DaprClient 要发送到的 Actor 名称。它应该与实现的 DaprType 匹配,尽管 如果需要,您可以覆盖该类型。

<?php
$app->run(function(\Dapr\Actors\ActorProxy $actorProxy) {
    $actor = $actorProxy->get(ICount::class, 'actor-id');
    $actor->increment(10);
});

编写 Actor

要创建 Actor,您需要实现之前定义的接口,并添加 DaprType 属性。所有 Actor 必须实现 IActor,不过有一个 Actor 基类实现了样板代码,使您的实现 更加简单。

以下是计数 Actor:

<?php
#[\Dapr\Actors\Attributes\DaprType('Count')]
class Counter extends \Dapr\Actors\Actor implements ICount {
    function __construct(string $id, private CountState $state) {
        parent::__construct($id);
    }
    
    function increment(int $amount = 1): void {
        $this->state->count += $amount;
    }
    
    function get_count(): int {
        return $this->state->count;
    }
}

最重要的是构造函数。它至少接受一个名为 id 的参数,该参数是 Actor 的 id。 任何其他参数都由 DI 容器注入,包括您想要使用的任何 ActorState

Actor 生命周期

Actor 通过构造函数在每个针对该 Actor 类型的请求上进行实例化。您可以使用它来计算 临时状态或处理您所需的任何特定于请求的启动,例如设置其他客户端或 连接。

实例化 Actor 后,可能会调用 on_activation() 方法。on_activation() 方法在 Actor “唤醒” 或首次创建时调用。它不会在每次请求时调用。

接下来,调用 Actor 方法。这可能来自定时器、提醒或客户端。您可以执行任何需要 完成的工作和/或抛出异常。

最后,工作结果返回给调用者。一段时间后(取决于您如何配置 服务),Actor 将被停用,并将调用 on_deactivation()。如果主机宕机、 daprd 崩溃或发生其他一些阻止其成功调用的错误,则可能不会调用此方法。

Actor 状态

Actor 状态是扩展 ActorState 的"普通旧 PHP 对象"(POPO)。ActorState 基类提供了一些 有用的方法。以下是示例实现:

<?php
class CountState extends \Dapr\Actors\ActorState {
    public int $count = 0;
}

注册 Actor

Dapr 期望在启动时知道服务可以托管哪些 Actor。您需要将其添加到配置中:

如果你想利用预编译的依赖注入,你需要使用一个工厂:

<?php
// in config.php

return [
    'dapr.actors' => fn() => [Counter::class],
];

启动应用程序所需的全部内容:

<?php

require_once __DIR__ . '/vendor/autoload.php';

$app = \Dapr\App::create(
    configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php')->enableCompilation(__DIR__)
);
$app->start();
<?php
// in config.php

return [
    'dapr.actors' => [Counter::class]
];

启动应用程序所需的全部内容:

<?php

require_once __DIR__ . '/vendor/autoload.php';

$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php'));
$app->start();

1 - 生产环境参考:Actor

在生产环境中运行 PHP Actor

代理模式

Actor 代理有四种不同的处理模式。每种模式呈现不同的权衡,你需要在开发和生产环境中进行权衡。

<?php
\Dapr\Actors\Generators\ProxyFactory::GENERATED;
\Dapr\Actors\Generators\ProxyFactory::GENERATED_CACHED;
\Dapr\Actors\Generators\ProxyFactory::ONLY_EXISTING;
\Dapr\Actors\Generators\ProxyFactory::DYNAMIC;

可以通过 dapr.actors.proxy.generation 配置键进行设置。

这是默认模式。在此模式下,每个请求都会生成一个类并通过 eval 执行。它主要用于开发,不应在生产环境中使用。

这与 ProxyModes::GENERATED 相同,只是类存储在临时文件中,因此不需要在每次请求时重新生成。它不知道何时更新缓存的类,因此不建议在开发中使用,但在无法手动生成文件时提供此选项。

在此模式下,如果代理类不存在,将抛出异常。当你不想在生产环境中生成代码时,这很有用。你需要确保生成并预加载/自动加载该类。

生成代理

你可以创建一个 composer 脚本按需生成代理,以利用 ONLY_EXISTING 模式。

创建一个 ProxyCompiler.php

<?php

class ProxyCompiler {
    private const PROXIES = [
        MyActorInterface::class,
        MyOtherActorInterface::class,
    ];
    
    private const PROXY_LOCATION = __DIR__.'/proxies/';
    
    public static function compile() {
        try {
            $app = \Dapr\App::create();
            foreach(self::PROXIES as $interface) {
                $output = $app->run(function(\DI\FactoryInterface $factory) use ($interface) {
                    return \Dapr\Actors\Generators\FileGenerator::generate($interface, $factory);
                });
                $reflection = new ReflectionClass($interface);
                $dapr_type = $reflection->getAttributes(\Dapr\Actors\Attributes\DaprType::class)[0]->newInstance()->type;
                $filename = 'dapr_proxy_'.$dapr_type.'.php';
                file_put_contents(self::PROXY_LOCATION.$filename, $output);
                echo "Compiled: $interface";
            }
        } catch (Exception $ex) {
            echo "Failed to generate proxy for $interface\n{$ex->getMessage()} on line {$ex->getLine()} in {$ex->getFile()}\n";
        }
    }
}

然后为生成的代理添加一个 psr-4 自动加载器,并在 composer.json 中添加一个脚本:

{
  "autoload": {
    "psr-4": {
      "Dapr\\Proxies\\": "path/to/proxies"
    }
  },
  "scripts": {
    "compile-proxies": "ProxyCompiler::compile"
  }
}

最后,配置 dapr 仅使用生成的代理:

<?php
// in config.php

return [
    'dapr.actors.proxy.generation' => ProxyFactory::ONLY_EXISTING,
];

在此模式下,代理满足接口约定,但实际上并不实现接口本身(意味着 instanceof 将返回 false)。此模式利用 PHP 的一些特性来工作,适用于代码无法被 eval 或生成的场景。

请求

对于任何模式,创建 actor 代理的开销都非常小。创建 actor 代理对象时不会发起任何请求。

当你在代理对象上调用方法时,只有你实现的方法由你的 actor 实现提供服务。get_id() 在本地处理,而 get_reminder()delete_reminder() 等由 daprd 处理。

Actor 实现

PHP 中的每个 actor 实现都必须实现 \Dapr\Actors\IActor 并使用 \Dapr\Actors\ActorTrait trait。这允许快速反射并提供一些快捷方式。使用 \Dapr\Actors\Actor 抽象基类会自动为你完成这些,但如果你需要覆盖默认行为,可以通过实现接口并使用该 trait 来实现。

激活和停用

当 actor 激活时,会将一个令牌文件写入临时目录(在 Linux 上默认为 '/tmp/dapr_' + sha256(concat(Dapr type, id)),在 Windows 上为 '%temp%/dapr_' + sha256(concat(Dapr type, id)))。该文件会一直保留,直到 actor 停用或主机关闭。这确保了当 Dapr 在主机上激活 actor 时,on_activation 只被调用一次。

性能

在使用 php-fpmnginx 或 Windows 上的 IIS 的生产环境中,actor 方法调用非常快。虽然 actor 在每个请求上都会被构造,但 actor 状态键仅在需要时加载,而不是在每个请求期间加载。但是,单独加载每个键会有一些开销。可以通过在状态中存储数据数组来缓解这个问题,以可用性换取速度。不建议从一开始就这样做,而是作为需要时的优化手段。

状态版本控制

ActorState 对象中的变量名直接对应存储中的键名。这意味着如果你更改变量的类型或名称,可能会遇到错误。为了解决这个问题,你可能需要对状态对象进行版本控制。为此,你需要覆盖状态的加载和存储方式。有很多方法可以解决这个问题,其中一种解决方案可能如下所示:

<?php

class VersionedState extends \Dapr\Actors\ActorState {
    /**
     * @var int 存储中状态的当前版本。我们给出当前版本的默认值。 
     * 但是,它在存储中可能有不同的值。 
     */
    public int $state_version = self::VERSION;
    
    /**
     * @var int 数据的当前版本
     */
    private const VERSION = 3;
    
    /**
     * 在你的 actor 激活时调用。
     */
    public function upgrade() {
        if($this->state_version < self::VERSION) {
            $value = parent::__get($this->get_versioned_key('key', $this->state_version));
            // 更新数据结构后更新值
            parent::__set($this->get_versioned_key('key', self::VERSION), $value);
            $this->state_version = self::VERSION;
            $this->save_state();
        }
    }
    
    // 如果你在上面的方法中根据需要升级所有键,则不需要在加载/保存时遍历以前的
    // 键,你只需获取键的当前版本。
    
    private function get_previous_version(int $version): int {
        return $this->has_previous_version($version) ? $version - 1 : $version;
    }
    
    private function has_previous_version(int $version): bool {
        return $version >= 0;
    }
    
    private function walk_versions(int $version, callable $callback, callable $predicate): mixed {
        $value = $callback($version);
        if($predicate($value) || !$this->has_previous_version($version)) {
            return $value;
        }
        return $this->walk_versions($this->get_previous_version($version), $callback, $predicate);
    }
    
    private function get_versioned_key(string $key, int $version) {
        return $this->has_previous_version($version) ? $version.$key : $key;
    }
    
    public function __get(string $key): mixed {
        return $this->walk_versions(
            self::VERSION, 
            fn($version) => parent::__get($this->get_versioned_key($key, $version)),
            fn($value) => isset($value)
        );
    }
    
    public function __isset(string $key): bool {
        return $this->walk_versions(
            self::VERSION,
            fn($version) => parent::__isset($this->get_versioned_key($key, $version)),
            fn($isset) => $isset
        );
    }
    
    public function __set(string $key,mixed $value): void {
        // 可选:你可以取消设置键的以前版本
        parent::__set($this->get_versioned_key($key, self::VERSION), $value);
    }
    
    public function __unset(string $key) : void {
        // 取消设置此版本和所有以前版本
        $this->walk_versions(
            self::VERSION, 
            fn($version) => parent::__unset($this->get_versioned_key($key, $version)), 
            fn() => false
        );
    }
}

有很多可以优化的地方,直接在生产环境中使用它并不是一个好主意,但你可以了解它的工作原理。其中大部分将取决于你的用例,这也是 SDK 中没有类似功能的原因。例如,在此示例实现中,保留以前的值是为了防止升级期间可能出现的错误;保留以前的值允许再次运行升级,但你可能希望删除以前的值。