php72 な App Engine で Cloud Tasks を使ってタスクを実行するまで

投稿日:

「gae/php72 で Cloud Tasks にタスクを登録する」って流れを試そうと思ったら、思った以上に良く分からなかったので、備忘録かねてメモっておきます。

最初にこんなこと書くとアレですが、内容的には大した内容じゃないです。

分かってしまえば何てことない。

が、そこにたどり着くまでが激しくめんどい。

GCP は毎回これだよなぁ...

やりたい事

  • BigQueryの大量データの集計などを月別に実行
  • 管理画面などからオンデマンドでも実行したい
  • 重複実行させたくない
  • 定期実行させたい

「集計バッチを管理画面から叩く」とか「集計バッチを週一で実行する」みたいなイメージです。

BigQuery の集計は物によっては 1分以上待たされることもあるので、その間に同じバッチを実行されないように重複実行を防止したい。

と言う感じ。

元々 CentOS 上に php で作成してたバッチを移植する予定で、そっちでは重複実行を防止するために lock ファイルなどを使っていましたが、AppEngine の場合は Cloud Tasks で Task ID を指定して行った方が良いとの事なので、そちらを検討。

Cloud Tasks を使った場合の流れ

イメージとしては以下のような感じ。

  1. タスクの登録 (/cretate_task)
  2. Cloud Tasks からバッチ処理用のスクリプトが叩かれる
  3. バッチ処理を実行 (/task_handler)

が、Cloud Tasks のイメージが全くわかないので、クイックスタートをやってみました。

Quickstart for Cloud Tasks Queues | Cloud Tasks Documentation | Google Cloud

Whether your business is early in its journey or well on its way to digital transformation, Google Cloud's solutions and technologies help chart a path to success.

が、このままやっても上手く動かないと言うか、意図通りのサンプルでは無いので手を入れて検証しました。

そもそもクイックスタートだと、タスク登録がローカルスクリプトのようなので、GAE で動くように改修していきました。

サンプルソースを期待通りに動かす方法

まず、サービスアカウントのキーファイルの環境変数を登録しておかないとエラーになります。

Fatal error: Uncaught GuzzleHttp\Exception\ClientException: Client error: `POST https://oauth2.googleapis.com/token` resulted in a `400 Bad Request` response: { "error": "invalid_grant", "error_description": "Bad Request" } in C:\work\tmp\cloud-tasks\php-docs-samples\appengine\php72\tasks\apps\handler\vendor\guzzlehttp\guzzle\src\Exception\RequestException.php:113 Stack trace: #0 C:\work\tmp\cloud-tasks\php-docs-samples\appengine\php72\tasks\apps\handler\vendor\guzzlehttp\guzzle\src\Middleware.php(66): GuzzleHttp\Exception\RequestException::create(Object(GuzzleHttp\Psr7\Request), Object(GuzzleHttp\Psr7\Response)) #1 C:\work\tmp\cloud-tasks\php-docs-samples\appengine\php72\tasks\apps\handler\vendor\guzzlehttp\promises\src\Promise.php(203): GuzzleHttp\Middleware::GuzzleHttp\{closure}(Object(GuzzleHttp\Psr7\Response)) #2 C:\work\tmp\cloud-tasks\php-docs-samples\appengine\php72\tasks\apps\handler\vendor\guzzlehttp\promises\src\Promise.php(156): GuzzleHttp\Promise\Promise::callHandler(1, Object(GuzzleHttp\Psr7\Response), in C:\work\tmp\cloud-tasks\php-docs-samples\appengine\php72\tasks\apps\handler\vendor\guzzlehttp\guzzle\src\Exception\RequestException.php on line 113

この辺りはドキュメントにもありますが、Powershell の場合のパスの通し方は以下のような感じ。

> $env:GOOGLE_APPLICATION_CREDENTIALS="[PATH]"

これを打ってから、ビルトインウェブサーバーを起動します。

> php -S localhost:8080

が、クイックスタートで clone したサンプルソースはローカルでの実行を前提としているので、GAE で動かす場合、composer.json の中身が足りません

{
    "require": {
        "google/cloud-logging": "^1.14",
        "google/cloud-tasks": "0.10.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^5",
        "google/cloud-tools": "^0.8.5"
    }
}

google/cloud-tasks が足りないので追加する必要があるのですが、composer でパッケージを既にインストール済みの場合、composer.json の編集ではなく下記のコマンドで追加。

> composer require google/cloud-tasks:^0.10.0

一応ここまでで、タスクが作成され、タスクから task_handler が呼ばれてログが出力されるサンプルは実行できると思います。

が、肝心の Task ID は毎回ランダムなものが発行されてしまうので、このままでは重複実行を防止することは無理。

と言う訳で、Task ID を指定してタスクを追加します。

Task ID を指定してタスクを追加する方法

サンプルでは下記のように、何もせずに Task オブジェクトを作成しています。

$task = new Task();

このように作成すると毎回新しいタスクIDが振られるのですが、下記のように Taskオブジェクトの初期化時に name を渡すと、nameで指定した Task ID でタスクを作成できます。

$task_id= '201906';
$task = new Task(['name' => "$queueName/tasks/$task_id"]);

今回の要件のように、月別のタスクを作成して重複実行させたくない場合には、タスクIDにターゲットの月を指定して実行すれば良いかなと。

タスクオブジェクトのドキュメントの場所が分かりづらくて苦労しましたが...

google-cloud-php

Google Cloud Platform's client library documentation

Task ID が重複した場合

で、タスク ID を指定して作成した場合には、同じ Task ID のタスクが作成されると、エラーになります。

サンプルスクリプトの場合は下記のような盛大なエラーが。

Fatal error: Uncaught Google\ApiCore\ApiException: { "message": "The task cannot be created because a task with this name existed too recently. For more information about task de-duplication see https:\/\/cloud.google.com\/tasks\/docs\/reference\/rest\/v2\/projects.locations.queues.tasks\/create#body.request_body.FIELDS.task.", "code": 6, "status": "ALREADY_EXISTS", "details": [ { "@type": "grpc-server-stats-bin", "data": "" } ] } thrown in C:\work\tmp\cloud-tasks\php-docs-samples\appengine\php72\tasks\apps\handler\vendor\google\gax\src\ApiException.php on line 139

実際に実装する場合には、このエラーを拾って "status": "ALREADY_EXISTS" 辺りを見つつ、もうちょいまともなエラーを返した方が良いかもしれないですね。

で、この ALREADY_EXISTS ですが、タスクの実行が終わって 1時間くらいは同じタスクIDのリクエストは出来ないようです。

Package google.cloud.tasks.v2 | Cloud Tasks | Google Cloud

Whether your business is early in its journey or well on its way to digital transformation, Google Cloud's solutions and technologies help chart a path to success.

「とにかく急いでるので無視して実行したい!」みたいなときには、ランダムな Task ID で実行する感じでしょうか?

っとなると、集計対象のパラメータとしては Task ID は使えないかもしれませんね。

パラメータを渡す方法

と言う訳で、パラメータ。

サンプルにもある通り、下記のような感じで Cloud Tasks からたたかれた時には HTTP_X_APPENGINE_TASKNAME と HTTP_X_APPENGINE_QUEUENAME でタスクIDとキューIDを取得できます。

$taskName = $_SERVER['HTTP_X_APPENGINE_TASKNAME'] ?? '';
$queueName = $_SERVER['HTTP_X_APPENGINE_QUEUENAME'] ?? '';

HTTP_X_APPENGINE_QUEUENAME は固定なので パラメータとして使うなら HTTP_X_APPENGINE_TASKNAME ですが、先ほど書いたようなケースを想定すると、Task ID をパラメーターとして使うのは柔軟性が無いかなと。

と言う感じで Cloud Tasks 経由で パラメーターを渡す場合には payload を使う形になります。

// $payload = 'The payload your task should carry to the task handler. Optional';

サンプルだと、こんな感じでただの文字列っぽい感じになってたり、他の言語のサンプルでも hello とかなんだから良く分からない内容になってますが...

// Setting a body value is only compatible with HTTP POST and PUT requests.
if (isset($payload)) {
    $httpRequest->setBody($payload);
}

// Create a Cloud Task object.
$task = new Task();
$task->setAppEngineHttpRequest($httpRequest);

// Send request and print the task name.
$response = $client->createTask($queueName, $task);

サンプルのこの流れで setBody でセットされた文字列は生のPOSTデータとして送信されます。

で、task_handler 側で file_get_contents('php://input') で取得できます。

サンプルのままだとただの文字列なので、複数パラメーターを渡す用途では使えませんが、例えば下記のように JSON データを $payload にセットして task_handler で取得すれば、簡単に複数パラメーターを渡せそうです。

{"key1": "value1", "key2": "value2"}

引数一つなら value そのまま payload に突っ込んでも良いけど、将来的な拡張性を考えると、最初から json とかで渡しておいた方がスマートかも。

定期実行は cron で

オンデマンド実行は /create_task を叩いてもらう感じで良いと思いますが、定期的に自動実行するような場合は App Engine の cron ジョブで実行されるようにすれば OK。

具体的には cron.yaml に下記のように設定を書いてデプロイする。

cron:
- description: "every 2"
  url: /create_task
  schedule: every 2 minutes
> gcloud app deploy cron.yaml

と言う感じでとりあえず、Cloud Tasks にタスク登録して実行するところまでは出来たので、こんな感じで設計してみます。

GCP、楽しいけど、もっとサンプルが分かりやすいと良いよなぁ...

更新日: