Waiter

はじめに

SDK によって提供されている高次の抽象化のひとつに、 waiter の概念があります。 Waiter によって 結果整合性 (eventually consistent) システムを簡単に利用することができます。 それは、 Waiter がリソースが特定の状態になるまでリソースをポーリングすることが容易にしてくれるためです。 クライアントの docblock を見ることで、クライアントによってサポートされている waiter のリストを知ることができます。 "waitUntil" で始まっている @method タグは waiter を使うことができます。

$client->waitUntilBucketExists(array('Bucket' => 'my-bucket'));

先のメソッド呼び出しで waiter オブジェクトをインスタンス化し、 bucket が存在するまでポーリングすることができます。

Waiterがバケットに対して過剰にポーリングを行うと、 Aws\Common\Exception\RuntimeException 例外が発生します。

"waitUntil[…]" 型のメソッドはいずれも __call マジックメソッドを使って実装されています。 実際に存在するのは waitUntil() メソッドだけですが、 @method 記法で 定義されたメソッドは多くのIDEが自動補完できるので、 waitUntil[…]" 型のメソッドは目につきやすいのです。 次のコードは waitUntil() メソッドを使っていますが、先のコード例と同等です。

$client->waitUntil('BucketExists', array('Bucket' => 'my-bucket'));

基本設定

Waiterが実行を試みるポーリングの回数や、各ポーリング間の遅延秒数は、 "waiter" というプレフィックスがついたキーの値を渡して調整できます。

$client->waitUntilBucketExists(array(
    'Bucket'              => 'my-bucket',
    'waiter.interval'     => 10,
    'waiter.max_attempts' => 3
));

Waiterオブジェクト

Waiterオブジェクトと直接やり取りするためには、 getWaiter() メソッドを使います。 次のコードは前節に示した例と同等です。

$bucketExistsWaiter = $client->getWaiter('BucketExists')
    ->setConfig(array('Bucket' => 'my-bucket'))
    ->setInterval(10)
    ->setMaxAttempts(3);
$bucketExistsWaiter->wait();

Waiterイベント

Waiterオブジェクトを直接操作する方法には、イベントリスナをアタッチできる、という利点があります。 Waiterは ウェイトサイクル ごとに、2つのイベントを送出します。 ウェイトサイクルでは次のような処理を行います。

  1. waiter.before_attempt イベントをディスパッチする。
  2. ウェイト条件を解決するため、サービスに要求を送り、結果を調べる。
  3. ウェイト条件が解決できた場合、ウェイトサイクルは終了する。回数が max_attempts に達すれば例外を投げる。
  4. waiter.before_wait イベントをディスパッチする。
  5. interval 秒間スリープする。

Waiterオブジェクトは Guzzle\Common\AbstractHasDispatcher クラスを拡張したものなので、 addSubscriber() メソッド、 getEventDispatcher() メソッドがあります。 リスナをアタッチするコード例を示します。 これは先に示した例を修正したものです。

// Get and configure the waiter object
$waiter = $client->getWaiter('BucketExists')
    ->setConfig(array('Bucket' => 'my-bucket'))
    ->setInterval(10)
    ->setMaxAttempts(3);

// Get the event dispatcher and register listeners for both events emitted by the waiter
$dispatcher = $waiter->getEventDispatcher();
$dispatcher->addListener('waiter.before_attempt', function () {
    echo "Checking if the wait condition has been met…\n";
});
$dispatcher->addListener('waiter.before_wait', function () use ($waiter) {
    $interval = $waiter->getInterval();
    echo "Sleeping for {$interval} seconds…\n";
});

$waiter->wait();

独自のWaiter

ユースケースを考慮すれば、アプリケーション特有のWaiterロジック、 あるいはSDKに組み込まれていないWaiterが必要である場合、 独自のWaiterオブジェクトを実装するとよいでしょう。 クライアントの getWaiterFactory() メソッド、 setWaiterFactory() メソッドで、 クライアントが使うWaiterファクトリに手を加え、独自のWaiterが生成されるようにします。 サービスクライアントは通常、 Aws\Common\Waiter\CompositeWaiterFactory を使うので、必要ならばファクトリを追加することも可能です。 独自のWaiterクラスを実装するとともに、クライアントのWaiterファクトリに手を加えて、 独自のWaiterが生成されるようにした例を示します。

namespace MyApp\FakeWaiters
{
    use Aws\Common\Waiter\AbstractResourceWaiter;

    class SleptThreeTimes extends AbstractResourceWaiter
    {
        public function doWait()
        {
            if ($this->attempts < 3) {
                echo "Need to sleep…\n";
                return false;
            } else {
                echo "Now I've slept 3 times.\n";
                return true;
            }
        }
    }
}

namespace
{
    use Aws\S3\S3Client;
    use Aws\Common\Waiter\WaiterClassFactory;

    $client = S3Client::factory();

    $compositeFactory = $client->getWaiterFactory();
    $compositeFactory->addFactory(new WaiterClassFactory('MyApp\FakeWaiters'));

    $waiter = $client->waitUntilSleptThreeTimes();
}

これを実行すると次のような出力が得られます。

Need to sleep… Need to sleep… Need to sleep… Now I've slept 3 times.

Waiterの定義

SDKに付属のWaiterは、クライアント向けのサービス記述にその定義があります。 設定DSL (ドメイン固有の言語) を使って、ウェイト間隔、ウェイト条件、 リソースを検査またはポーリングして条件を解決する方法を定義しています。

このデータは、クライアントのインスタンスを生成する際、 Aws\Common\Waiter\WaiterConfigFactory クラスが自動的に取り込んで使うようになります。 サービス記述で定義されたWaiterを、クライアントが使えるようにするためです。

Amazon Glacierのサービス記述 (Aws\Glacier\GlacierClient に付属しているWaiterを定義) の 抜粋を示します。

return array(
    // ...

    'waiters' => array(
        '__default__' => array(
            'interval' => 3,
            'max_attempts' => 15,
        ),
        '__VaultState' => array(
            'operation' => 'DescribeVault',
        ),
        'VaultExists' => array(
            'extends' => '__VaultState',
            'success.type' => 'output',
            'description' => 'Wait until a vault can be accessed.',
            'ignore_errors' => array(
                'ResourceNotFoundException',
            ),
        ),
        'VaultNotExists' => array(
            'extends' => '__VaultState',
            'description' => 'Wait until a vault is deleted.',
            'success.type' => 'error',
            'success.value' => 'ResourceNotFoundException',
        ),
    ),

    // ...
);

Waiterを開発して、SDK に対して貢献するためには、 waiters DSL を使って実装する必要があります。 DSL はまだドキュメント化されていません。現在のテーマを変更し、waiter の実装を手助けしたい場合は、 GitHub 経由で問い合わせてください。