From bf98321f1f1d04f4b998e4e63abcdd94be968f45 Mon Sep 17 00:00:00 2001 From: Mikael Hedin Date: Thu, 14 Sep 2023 15:17:59 +0200 Subject: [PATCH 01/14] Add support for json credential file --- README.md | 30 ++++++++++++++++++++++------- src/CloudTasksServiceProvider.php | 10 ++++++++-- src/TaskHandler.php | 32 +++++++++++++++++++++---------- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 23b072e..3240d09 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,21 @@ Please check the [Laravel support policy](https://laravel.com/docs/master/releas // is marked as a DEADLINE_EXCEEDED failure. 'dispatch_deadline' => null, 'backoff' => 0, + + 'queue' => env('STACKKIT_CLOUD_TASKS_QUEUE', 'default'), + 'credential_file' => env('STACKKIT_CLOUD_TASKS_CREDENTIAL_FILE', ''), + // Either you have an AppEngine task, and use these + 'app_engine' => env('STACKKIT_APP_ENGINE_TASK', false), + 'app_engine_service' => env('STACKKIT_APP_ENGINE_SERVICE', ''), + // Else you get a HttpTask, and use these + 'handler' => env('STACKKIT_CLOUD_TASKS_HANDLER', ''), + 'service_account_email' => env('STACKKIT_CLOUD_TASKS_SERVICE_EMAIL', ''), + 'signed_audience' => env('STACKKIT_CLOUD_TASKS_SIGNED_AUDIENCE', true), + // Optional: The deadline in seconds for requests sent to the worker. If the worker + // does not respond by this deadline then the request is cancelled and the attempt + // is marked as a DEADLINE_EXCEEDED failure. + 'dispatch_deadline' => null, + 'backoff' => 0, ], ``` @@ -64,13 +79,14 @@ Now that the package is installed, the final step is to set the correct environm Please check the table below on what the values mean and what their value should be. -| Environment variable | Description |Example ---------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--- -| `STACKKIT_CLOUD_TASKS_PROJECT` | The project your queue belongs to. |`my-project` -| `STACKKIT_CLOUD_TASKS_LOCATION` | The region where the project is hosted |`europe-west6` -| `STACKKIT_CLOUD_TASKS_QUEUE` | The default queue a job will be added to |`emails` -| `STACKKIT_CLOUD_TASKS_SERVICE_EMAIL` | The email address of the service account. Important, it should have the correct roles. See the section below which roles. |`my-service-account@appspot.gserviceaccount.com` -| `STACKKIT_CLOUD_TASKS_HANDLER` (optional) | The URL that Cloud Tasks will call to process a job. This should be the URL to your Laravel app. By default we will use the URL that dispatched the job. |`https://.com` +| Environment variable | Description |Example +---------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|--- +| `STACKKIT_CLOUD_TASKS_PROJECT` | The project your queue belongs to. |`my-project` +| `STACKKIT_CLOUD_TASKS_LOCATION` | The region where the project is hosted. |`europe-west6` +| `STACKKIT_CLOUD_TASKS_QUEUE` | The default queue a job will be added to. |`emails` +| `STACKKIT_CLOUD_TASKS_CREDENTIAL_FILE` (optional) | A json credential file to authenticate the connection (from outside AppEngine) |`project-123.json` +| `STACKKIT_CLOUD_TASKS_SERVICE_EMAIL` | The email address of the service account. Important, it should have the correct roles. See the section below which roles. |`my-service-account@appspot.gserviceaccount.com` +| `STACKKIT_CLOUD_TASKS_HANDLER` (optional) | The URL that Cloud Tasks will call to process a job. This should be the URL to your Laravel app. By default we will use the URL that dispatched the job. |`https://.com` | `STACKKIT_CLOUD_TASKS_SIGNED_AUDIENCE` (optional) | True or false depending if you want extra security by signing the audience of your tasks. May misbehave in certain Cloud Run setups. Defaults to true. | `true`
diff --git a/src/CloudTasksServiceProvider.php b/src/CloudTasksServiceProvider.php index d22a281..1c9aaa9 100644 --- a/src/CloudTasksServiceProvider.php +++ b/src/CloudTasksServiceProvider.php @@ -29,8 +29,14 @@ public function boot(): void private function registerClient(): void { - $this->app->singleton(CloudTasksClient::class, function () { - return new CloudTasksClient(); + $this->app->singleton(CloudTasksClient::class, function ($app) { + $config = config('queue.connections.cloudtasks'); + $options = []; + $x = $config['credential_file']; + if (!empty($x)) { + $options['credentials'] = $x; + } + return new CloudTasksClient($options); }); $this->app->bind('open-id-verificator', OpenIdVerificatorConcrete::class); diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 434e8f8..8df5607 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -10,10 +10,12 @@ use Illuminate\Queue\Jobs\Job; use Illuminate\Queue\QueueManager; use Illuminate\Queue\WorkerOptions; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Safe\Exceptions\JsonException; use UnexpectedValueException; + use function Safe\json_decode; class TaskHandler @@ -45,11 +47,18 @@ public function __construct(CloudTasksClient $client) public function handle(?string $task = null): void { + Log::debug(json_encode(request()->headers->all())); + Log::debug(json_encode(request()->bearerToken())); + Log::debug(json_encode(request()->getContent())); + $task = $this->captureTask($task); + Log::debug(json_encode($task)); $this->loadQueueConnectionConfiguration($task); + Log::debug(json_encode($this->config)); $this->setQueue(); + Log::debug(json_encode($this->queue)); OpenIdVerificator::verify(request()->bearerToken(), $this->config); @@ -63,7 +72,7 @@ public function handle(?string $task = null): void */ private function captureTask($task): array { - $task = $task ?: (string) (request()->getContent()); + $task = $task ?: (string)(request()->getContent()); try { $array = json_decode($task, true); @@ -72,13 +81,13 @@ private function captureTask($task): array } $validator = validator([ - 'json' => $task, - 'task' => $array, + 'json' => $task, + 'task' => $array, 'name_header' => request()->header('X-CloudTasks-Taskname'), ], [ - 'json' => 'required|json', - 'task' => 'required|array', - 'task.data' => 'required|array', + 'json' => 'required|json', + 'task' => 'required|array', + 'task.data' => 'required|array', 'name_header' => 'required|string', ]); @@ -141,8 +150,8 @@ private function handleTask(array $task): void // we know the job was created using an earlier version of the package. This // job does not have the attempts tracked internally yet. $taskRetryCountHeader = request()->header('X-CloudTasks-TaskRetryCount'); - if ($taskRetryCountHeader && (int) $taskRetryCountHeader > 0) { - $job->setAttempts((int) $taskRetryCountHeader); + if ($taskRetryCountHeader && (int)$taskRetryCountHeader > 0) { + $job->setAttempts((int)$taskRetryCountHeader); } else { $job->setAttempts($task['internal']['attempts']); } @@ -173,11 +182,14 @@ private function loadQueueRetryConfig(CloudTasksJob $job): void public static function getCommandProperties(string $command): array { if (Str::startsWith($command, 'O:')) { - return (array) unserialize($command, ['allowed_classes' => false]); + return (array)unserialize($command, ['allowed_classes' => false]); } if (app()->bound(Encrypter::class)) { - return (array) unserialize(app(Encrypter::class)->decrypt($command), ['allowed_classes' => ['Illuminate\Support\Carbon']]); + return (array)unserialize( + app(Encrypter::class)->decrypt($command), + ['allowed_classes' => ['Illuminate\Support\Carbon']] + ); } return []; From 297c31f9387c60007f2dedb8ec3d6c212220e637 Mon Sep 17 00:00:00 2001 From: Mikael Hedin Date: Thu, 14 Sep 2023 15:24:32 +0200 Subject: [PATCH 02/14] Add support for App Engine task --- README.md | 5 ++++- src/CloudTasksQueue.php | 46 ++++++++++++++++++++++++++++------------- src/Config.php | 34 ++++++++++++++++-------------- src/Errors.php | 5 +++++ 4 files changed, 60 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 3240d09..dabb813 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,9 @@ Please check the table below on what the values mean and what their value should | `STACKKIT_CLOUD_TASKS_LOCATION` | The region where the project is hosted. |`europe-west6` | `STACKKIT_CLOUD_TASKS_QUEUE` | The default queue a job will be added to. |`emails` | `STACKKIT_CLOUD_TASKS_CREDENTIAL_FILE` (optional) | A json credential file to authenticate the connection (from outside AppEngine) |`project-123.json` -| `STACKKIT_CLOUD_TASKS_SERVICE_EMAIL` | The email address of the service account. Important, it should have the correct roles. See the section below which roles. |`my-service-account@appspot.gserviceaccount.com` +| `STACKKIT_APP_ENGINE_TASK` (optional) | Set to true to use App Engine task (else a Http task will be used). Defaults to false. |`true` +| `STACKKIT_APP_ENGINE_SERVICE` (optional) | The App Engine service to handle the task (only if using App Engine task). |`api` +| `STACKKIT_CLOUD_TASKS_SERVICE_EMAIL` (optional) | The email address of the service account. Important, it should have the correct roles. See the section below which roles. |`my-service-account@appspot.gserviceaccount.com` | `STACKKIT_CLOUD_TASKS_HANDLER` (optional) | The URL that Cloud Tasks will call to process a job. This should be the URL to your Laravel app. By default we will use the URL that dispatched the job. |`https://.com` | `STACKKIT_CLOUD_TASKS_SIGNED_AUDIENCE` (optional) | True or false depending if you want extra security by signing the audience of your tasks. May misbehave in certain Cloud Run setups. Defaults to true. | `true`
@@ -101,6 +103,7 @@ With Cloud Tasks, this is not the case. Instead, Cloud Tasks will schedule the j #### Good to know +- If `STACKKIT_APP_ENGINE_TASK` is true, `STACKKIT_CLOUD_TASKS_SERVICE_EMAIL` and `STACKKIT_CLOUD_TASKS_HANDLER` will be ignored. - The "Min backoff" and "Max backoff" options in Cloud Tasks are ignored. This is intentional: Laravel has its own backoff feature (which is more powerful than what Cloud Tasks offers) and therefore I have chosen that over the Cloud Tasks one. - Similarly to the backoff feature, I have also chosen to let the package do job retries the 'Laravel way'. In Cloud Tasks, when a task throws an exception, Cloud Tasks will decide for itself when to retry the task (based on the backoff values). It will also manage its own state and knows how many times a task has been retried. This is different from Laravel. In typical Laravel queues, when a job throws an exception, the job is deleted and released back onto the queue. In order to support Laravel's backoff feature, this package must behave the same way about job retries. diff --git a/src/CloudTasksQueue.php b/src/CloudTasksQueue.php index b07242b..19072da 100644 --- a/src/CloudTasksQueue.php +++ b/src/CloudTasksQueue.php @@ -2,6 +2,8 @@ namespace Stackkit\LaravelGoogleCloudTasksQueue; +use Google\Cloud\Tasks\V2\AppEngineHttpRequest; +use Google\Cloud\Tasks\V2\AppEngineRouting; use Google\Cloud\Tasks\V2\CloudTasksClient; use Google\Cloud\Tasks\V2\HttpMethod; use Google\Cloud\Tasks\V2\HttpRequest; @@ -136,10 +138,6 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) $queueName = $this->client->queueName($this->config['project'], $this->config['location'], $queue); $availableAt = $this->availableAt($delay); - $httpRequest = $this->createHttpRequest(); - $httpRequest->setUrl($this->getHandler()); - $httpRequest->setHttpMethod(HttpMethod::POST); - $payload = json_decode($payload, true); // Laravel 7+ jobs have a uuid, but Laravel 6 doesn't have it. @@ -152,11 +150,38 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) // value and need to manually set and update the number of times a task has been attempted. $payload = $this->withAttempts($payload); - $httpRequest->setBody(json_encode($payload)); - $task = $this->createTask(); $task->setName($this->taskName($queue, $payload)); - $task->setHttpRequest($httpRequest); + + if ($this->config['app_engine']) { + $path = \Safe\parse_url(route('cloud-tasks.handle-task'), PHP_URL_PATH); + + $appEngineRequest = new AppEngineHttpRequest(); + $appEngineRequest->setRelativeUri($path); + $appEngineRequest->setHttpMethod(HttpMethod::POST); + $appEngineRequest->setBody(json_encode($payload)); + if (!empty($service = $this->config['app_engine_service'])) { + $routing = new AppEngineRouting(); + $routing->setService($service); + $appEngineRequest->setAppEngineRouting($routing); + } + $task->setAppEngineHttpRequest($appEngineRequest); + } else { + $httpRequest = $this->createHttpRequest(); + $httpRequest->setUrl($this->getHandler()); + $httpRequest->setHttpMethod(HttpMethod::POST); + + $httpRequest->setBody(json_encode($payload)); + + $token = new OidcToken; + $token->setServiceAccountEmail($this->config['service_account_email']); + if ($audience = $this->getAudience()) { + $token->setAudience($audience); + } + $httpRequest->setOidcToken($token); + $task->setHttpRequest($httpRequest); + } + // The deadline for requests sent to the app. If the app does not respond by // this deadline then the request is cancelled and the attempt is marked as @@ -165,13 +190,6 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) $task->setDispatchDeadline(new Duration(['seconds' => $this->config['dispatch_deadline']])); } - $token = new OidcToken; - $token->setServiceAccountEmail($this->config['service_account_email']); - if ($audience = $this->getAudience()) { - $token->setAudience($audience); - } - $httpRequest->setOidcToken($token); - if ($availableAt > time()) { $task->setScheduleTime(new Timestamp(['seconds' => $availableAt])); } diff --git a/src/Config.php b/src/Config.php index 422ce44..5102570 100644 --- a/src/Config.php +++ b/src/Config.php @@ -19,8 +19,8 @@ public static function validate(array $config): void throw new Error(Errors::invalidLocation()); } - if (empty($config['service_account_email'])) { - throw new Error(Errors::invalidServiceAccountEmail()); + if (empty($config['service_account_email']) && empty($config['app-engine'])) { + throw new Error(Errors::serviceAccountOrAppEngine()); } } @@ -42,25 +42,29 @@ public static function getHandler($handler): string // (still) set to localhost. That will never work because Cloud Tasks // should always call a public address / hostname to process tasks. if (in_array($parse['host'], ['localhost', '127.0.0.1', '::1'])) { - throw new Exception(sprintf( - 'Unable to push task to Cloud Tasks because the handler URL is set to a local host: %s. ' . - 'This does not work because Google is not able to call the given local URL. ' . - 'If you are developing on locally, consider using Ngrok or Expose for Laravel to expose your local ' . - 'application to the internet.', - $handler - )); + throw new Exception( + sprintf( + 'Unable to push task to Cloud Tasks because the handler URL is set to a local host: %s. ' . + 'This does not work because Google is not able to call the given local URL. ' . + 'If you are developing on locally, consider using Ngrok or Expose for Laravel to expose your local ' . + 'application to the internet.', + $handler + ) + ); } // When the application is running behind a proxy and the TrustedProxy middleware has not been set up yet, // an error like [HttpRequest.url must start with 'https'] could be thrown. Since the handler URL must // always be https, we will provide a little extra information on how to fix this. if ($parse['scheme'] !== 'https') { - throw new Exception(sprintf( - 'Unable to push task to Cloud Tasks because the hander URL is not https. Google Cloud Tasks ' . - 'will only call safe (https) URLs. If you are running Laravel behind a proxy (e.g. Ngrok, Expose), make sure it is ' . - 'as a trusted proxy. To quickly fix this, add the following to the [app/Http/Middleware/TrustProxies] middleware: ' . - 'protected $proxies = \'*\';' - )); + throw new Exception( + sprintf( + 'Unable to push task to Cloud Tasks because the hander URL is not https. Google Cloud Tasks ' . + 'will only call safe (https) URLs. If you are running Laravel behind a proxy (e.g. Ngrok, Expose), make sure it is ' . + 'as a trusted proxy. To quickly fix this, add the following to the [app/Http/Middleware/TrustProxies] middleware: ' . + 'protected $proxies = \'*\';' + ) + ); } $trimmedHandlerUrl = rtrim($handler, '/'); diff --git a/src/Errors.php b/src/Errors.php index 58c648c..1d73f64 100644 --- a/src/Errors.php +++ b/src/Errors.php @@ -18,4 +18,9 @@ public static function invalidServiceAccountEmail(): string { return 'Google Service Account email address not provided. This is needed to secure the handler so it is only accessible by Google. To fix this, set the STACKKIT_CLOUD_TASKS_SERVICE_EMAIL environment variable'; } + + public static function serviceAccountOrAppEngine(): string + { + return 'A Google Service Account email or App Engine Request must be set. Set STACKKIT_CLOUD_TASKS_SERVICE_EMAIL or STACKKIT_APP_ENGINE_TASK'; + } } From b699e98054701364a5475eede0f5a5b42b47f85c Mon Sep 17 00:00:00 2001 From: Mikael Hedin Date: Thu, 14 Sep 2023 16:00:18 +0200 Subject: [PATCH 03/14] Use request header according to request type --- src/TaskHandler.php | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 8df5607..196cfcb 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -5,16 +5,14 @@ use Google\ApiCore\ApiException; use Google\Cloud\Tasks\V2\CloudTasksClient; use Google\Cloud\Tasks\V2\RetryConfig; -use Illuminate\Bus\Queueable; use Illuminate\Contracts\Encryption\Encrypter; -use Illuminate\Queue\Jobs\Job; -use Illuminate\Queue\QueueManager; use Illuminate\Queue\WorkerOptions; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Safe\Exceptions\JsonException; use UnexpectedValueException; +use stdClass; use function Safe\json_decode; @@ -69,6 +67,7 @@ public function handle(?string $task = null): void * @param string|array|null $task * @return array * @throws JsonException + * @throws ValidationException */ private function captureTask($task): array { @@ -80,10 +79,14 @@ private function captureTask($task): array $array = []; } + $nameHeader = config('queue.connections.cloudtasks.app_engine') + ? 'X-AppEngine-TaskName' + : 'X-CloudTasks-TaskName'; + Log::debug($nameHeader); $validator = validator([ 'json' => $task, 'task' => $array, - 'name_header' => request()->header('X-CloudTasks-Taskname'), + 'name_header' => request()->header($nameHeader), ], [ 'json' => 'required|json', 'task' => 'required|array', @@ -91,15 +94,16 @@ private function captureTask($task): array 'name_header' => 'required|string', ]); - try { - $validator->validate(); - } catch (ValidationException $e) { - if (config('app.debug')) { - throw $e; - } else { - abort(404); - } - } + $validator->validate(); +// try { +// $validator->validate(); +// } catch (ValidationException $e) { +// if (config('app.debug')) { +// throw $e; +// } else { +// abort(404); +// } +// } return json_decode($task, true); } From a6cdf93b5ca10ccb55f46c363572ec0d54f25022 Mon Sep 17 00:00:00 2001 From: Mikael Hedin Date: Thu, 14 Sep 2023 16:23:04 +0200 Subject: [PATCH 04/14] Correct config name --- src/Config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.php b/src/Config.php index 5102570..075fc3a 100644 --- a/src/Config.php +++ b/src/Config.php @@ -19,7 +19,7 @@ public static function validate(array $config): void throw new Error(Errors::invalidLocation()); } - if (empty($config['service_account_email']) && empty($config['app-engine'])) { + if (empty($config['service_account_email']) && empty($config['app_engine'])) { throw new Error(Errors::serviceAccountOrAppEngine()); } } From d471f95b10f73b890d238e7101b35decb564c465 Mon Sep 17 00:00:00 2001 From: Mikael Hedin Date: Thu, 14 Sep 2023 17:08:21 +0200 Subject: [PATCH 05/14] Don't verify Id on App Engine tasks --- src/TaskHandler.php | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 196cfcb..ef23f11 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -58,7 +58,9 @@ public function handle(?string $task = null): void $this->setQueue(); Log::debug(json_encode($this->queue)); - OpenIdVerificator::verify(request()->bearerToken(), $this->config); + if (!$this->config['app_engine']) { + OpenIdVerificator::verify(request()->bearerToken(), $this->config); + } $this->handleTask($task); } @@ -67,7 +69,6 @@ public function handle(?string $task = null): void * @param string|array|null $task * @return array * @throws JsonException - * @throws ValidationException */ private function captureTask($task): array { @@ -79,14 +80,13 @@ private function captureTask($task): array $array = []; } - $nameHeader = config('queue.connections.cloudtasks.app_engine') - ? 'X-AppEngine-TaskName' - : 'X-CloudTasks-TaskName'; - Log::debug($nameHeader); +// $nameHeader = config('queue.connections.cloudtasks.app_engine') +// ? 'X-AppEngine-TaskName' +// : 'X-CloudTasks-TaskName'; $validator = validator([ 'json' => $task, 'task' => $array, - 'name_header' => request()->header($nameHeader), + 'name_header' => request()->header('X-CloudTasks-TaskName') ?? request()->header('X-AppEngine-TaskName'), ], [ 'json' => 'required|json', 'task' => 'required|array', @@ -94,16 +94,15 @@ private function captureTask($task): array 'name_header' => 'required|string', ]); - $validator->validate(); -// try { -// $validator->validate(); -// } catch (ValidationException $e) { -// if (config('app.debug')) { -// throw $e; -// } else { -// abort(404); -// } -// } + try { + $validator->validate(); + } catch (ValidationException $e) { + if (config('app.debug')) { + throw $e; + } else { + abort(404); + } + } return json_decode($task, true); } From a75bab98b015f7d85cf9b1259975d44dadac7eab Mon Sep 17 00:00:00 2001 From: Mikael Hedin Date: Thu, 14 Sep 2023 17:21:35 +0200 Subject: [PATCH 06/14] Get task name from both kind of tasks --- src/TaskHandler.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/TaskHandler.php b/src/TaskHandler.php index ef23f11..84eba1a 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -37,6 +37,10 @@ class TaskHandler * @var RetryConfig */ private $retryConfig = null; + /** + * @var string + */ + private $taskName; public function __construct(CloudTasksClient $client) { @@ -83,10 +87,11 @@ private function captureTask($task): array // $nameHeader = config('queue.connections.cloudtasks.app_engine') // ? 'X-AppEngine-TaskName' // : 'X-CloudTasks-TaskName'; + $taskName = request()->header('X-CloudTasks-TaskName') ?? request()->header('X-AppEngine-TaskName'); $validator = validator([ 'json' => $task, 'task' => $array, - 'name_header' => request()->header('X-CloudTasks-TaskName') ?? request()->header('X-AppEngine-TaskName'), + 'name_header' => $taskName, ], [ 'json' => 'required|json', 'task' => 'required|array', @@ -96,6 +101,7 @@ private function captureTask($task): array try { $validator->validate(); + $this->taskName = $taskName; } catch (ValidationException $e) { if (config('app.debug')) { throw $e; @@ -131,12 +137,11 @@ private function handleTask(array $task): void $this->loadQueueRetryConfig($job); - $taskName = request()->header('X-Cloudtasks-Taskname'); $fullTaskName = $this->client->taskName( $this->config['project'], $this->config['location'], $job->getQueue() ?: $this->config['queue'], - $taskName, + $this->taskName, ); try { From a78c5799365539ffb33e96d3812e6e1c0272a156 Mon Sep 17 00:00:00 2001 From: Mikael Hedin Date: Thu, 14 Sep 2023 17:28:33 +0200 Subject: [PATCH 07/14] Remove excessive logging --- src/TaskHandler.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 84eba1a..232d9b2 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -7,7 +7,6 @@ use Google\Cloud\Tasks\V2\RetryConfig; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Queue\WorkerOptions; -use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Safe\Exceptions\JsonException; @@ -49,18 +48,11 @@ public function __construct(CloudTasksClient $client) public function handle(?string $task = null): void { - Log::debug(json_encode(request()->headers->all())); - Log::debug(json_encode(request()->bearerToken())); - Log::debug(json_encode(request()->getContent())); - $task = $this->captureTask($task); - Log::debug(json_encode($task)); $this->loadQueueConnectionConfiguration($task); - Log::debug(json_encode($this->config)); $this->setQueue(); - Log::debug(json_encode($this->queue)); if (!$this->config['app_engine']) { OpenIdVerificator::verify(request()->bearerToken(), $this->config); @@ -84,9 +76,6 @@ private function captureTask($task): array $array = []; } -// $nameHeader = config('queue.connections.cloudtasks.app_engine') -// ? 'X-AppEngine-TaskName' -// : 'X-CloudTasks-TaskName'; $taskName = request()->header('X-CloudTasks-TaskName') ?? request()->header('X-AppEngine-TaskName'); $validator = validator([ 'json' => $task, From cddb9b2c97f3ddb3b0b604dff97ca35280aa0acd Mon Sep 17 00:00:00 2001 From: Mikael Hedin Date: Thu, 14 Sep 2023 18:53:44 +0200 Subject: [PATCH 08/14] Get name from header in delete too --- src/CloudTasksQueue.php | 47 ++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/CloudTasksQueue.php b/src/CloudTasksQueue.php index 19072da..2adfa30 100644 --- a/src/CloudTasksQueue.php +++ b/src/CloudTasksQueue.php @@ -16,6 +16,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Str; use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated; + use function Safe\json_decode; use function Safe\json_encode; @@ -38,7 +39,7 @@ public function __construct(array $config, CloudTasksClient $client, $dispatchAf /** * Get the size of the queue. * - * @param string|null $queue + * @param string|null $queue * @return int */ public function size($queue = null) @@ -50,11 +51,11 @@ public function size($queue = null) /** * Fallback method for Laravel 6x and 7x * - * @param \Closure|string|object $job - * @param string $payload - * @param string $queue - * @param \DateTimeInterface|\DateInterval|int|null $delay - * @param callable $callback + * @param \Closure|string|object $job + * @param string $payload + * @param string $queue + * @param \DateTimeInterface|\DateInterval|int|null $delay + * @param callable $callback * @return mixed */ protected function enqueueUsing($job, $payload, $queue, $delay, $callback) @@ -69,9 +70,9 @@ protected function enqueueUsing($job, $payload, $queue, $delay, $callback) /** * Push a new job onto the queue. * - * @param string|object $job - * @param mixed $data - * @param string|null $queue + * @param string|object $job + * @param mixed $data + * @param string|null $queue * @return void */ public function push($job, $data = '', $queue = null) @@ -90,14 +91,14 @@ function ($payload, $queue) { /** * Push a raw payload onto the queue. * - * @param string $payload - * @param string|null $queue - * @param array $options + * @param string $payload + * @param string|null $queue + * @param array $options * @return string */ public function pushRaw($payload, $queue = null, array $options = []) { - $delay = ! empty($options['delay']) ? $options['delay'] : 0; + $delay = !empty($options['delay']) ? $options['delay'] : 0; return $this->pushToCloudTasks($queue, $payload, $delay); } @@ -105,10 +106,10 @@ public function pushRaw($payload, $queue = null, array $options = []) /** * Push a new job onto the queue after a delay. * - * @param \DateTimeInterface|\DateInterval|int $delay - * @param string|object $job - * @param mixed $data - * @param string|null $queue + * @param \DateTimeInterface|\DateInterval|int $delay + * @param string|object $job + * @param mixed $data + * @param string|null $queue * @return void */ public function later($delay, $job, $data = '', $queue = null) @@ -127,8 +128,8 @@ function ($payload, $queue, $delay) { /** * Push a job to Cloud Tasks. * - * @param string|null $queue - * @param string $payload + * @param string|null $queue + * @param string $payload * @param \DateTimeInterface|\DateInterval|int $delay * @return string */ @@ -204,7 +205,7 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) private function withUuid(array $payload): array { if (!isset($payload['uuid'])) { - $payload['uuid'] = (string) Str::uuid(); + $payload['uuid'] = (string)Str::uuid(); } return $payload; @@ -245,7 +246,7 @@ private function withAttempts(array $payload): array /** * Pop the next job off of the queue. * - * @param string|null $queue + * @param string|null $queue * @return \Illuminate\Contracts\Queue\Job|null */ public function pop($queue = null) @@ -269,11 +270,13 @@ public function delete(CloudTasksJob $job): void $queue = $job->getQueue() ?: $this->config['queue']; // @todo: make this a helper method somewhere. + $headerTaskName = request()->headers->get('X-Cloudtasks-Taskname') + ?? request()->headers->get('X-AppEngine-TaskName'); $taskName = $this->client->taskName( $config['project'], $config['location'], $queue, - (string) request()->headers->get('X-Cloudtasks-Taskname') + (string)$headerTaskName ); CloudTasksApi::deleteTask($taskName); From 3b7b87a42ef607fb1e76c7315fec6ae7eab1c8dd Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Fri, 19 Jan 2024 22:32:13 +0100 Subject: [PATCH 09/14] Cleanup README.md --- README.md | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index dabb813..ca5346d 100644 --- a/README.md +++ b/README.md @@ -46,26 +46,24 @@ Please check the [Laravel support policy](https://laravel.com/docs/master/releas 'queue' => env('STACKKIT_CLOUD_TASKS_QUEUE', 'default'), 'service_account_email' => env('STACKKIT_CLOUD_TASKS_SERVICE_EMAIL', ''), 'signed_audience' => env('STACKKIT_CLOUD_TASKS_SIGNED_AUDIENCE', true), + + // Optional + 'credential_file' => env('STACKKIT_CLOUD_TASKS_CREDENTIAL_FILE', ''), + + // Required when using AppEngine + 'app_engine' => env('STACKKIT_APP_ENGINE_TASK', false), + 'app_engine_service' => env('STACKKIT_APP_ENGINE_SERVICE', ''), + + // Required when not using AppEngine + 'handler' => env('STACKKIT_CLOUD_TASKS_HANDLER', ''), + 'service_account_email' => env('STACKKIT_CLOUD_TASKS_SERVICE_EMAIL', ''), + 'signed_audience' => env('STACKKIT_CLOUD_TASKS_SIGNED_AUDIENCE', true), + // Optional: The deadline in seconds for requests sent to the worker. If the worker // does not respond by this deadline then the request is cancelled and the attempt // is marked as a DEADLINE_EXCEEDED failure. 'dispatch_deadline' => null, 'backoff' => 0, - - 'queue' => env('STACKKIT_CLOUD_TASKS_QUEUE', 'default'), - 'credential_file' => env('STACKKIT_CLOUD_TASKS_CREDENTIAL_FILE', ''), - // Either you have an AppEngine task, and use these - 'app_engine' => env('STACKKIT_APP_ENGINE_TASK', false), - 'app_engine_service' => env('STACKKIT_APP_ENGINE_SERVICE', ''), - // Else you get a HttpTask, and use these - 'handler' => env('STACKKIT_CLOUD_TASKS_HANDLER', ''), - 'service_account_email' => env('STACKKIT_CLOUD_TASKS_SERVICE_EMAIL', ''), - 'signed_audience' => env('STACKKIT_CLOUD_TASKS_SIGNED_AUDIENCE', true), - // Optional: The deadline in seconds for requests sent to the worker. If the worker - // does not respond by this deadline then the request is cancelled and the attempt - // is marked as a DEADLINE_EXCEEDED failure. - 'dispatch_deadline' => null, - 'backoff' => 0, ], ``` @@ -85,8 +83,10 @@ Please check the table below on what the values mean and what their value should | `STACKKIT_CLOUD_TASKS_LOCATION` | The region where the project is hosted. |`europe-west6` | `STACKKIT_CLOUD_TASKS_QUEUE` | The default queue a job will be added to. |`emails` | `STACKKIT_CLOUD_TASKS_CREDENTIAL_FILE` (optional) | A json credential file to authenticate the connection (from outside AppEngine) |`project-123.json` +| **App Engine** | `STACKKIT_APP_ENGINE_TASK` (optional) | Set to true to use App Engine task (else a Http task will be used). Defaults to false. |`true` | `STACKKIT_APP_ENGINE_SERVICE` (optional) | The App Engine service to handle the task (only if using App Engine task). |`api` +| **Non- App Engine apps** | `STACKKIT_CLOUD_TASKS_SERVICE_EMAIL` (optional) | The email address of the service account. Important, it should have the correct roles. See the section below which roles. |`my-service-account@appspot.gserviceaccount.com` | `STACKKIT_CLOUD_TASKS_HANDLER` (optional) | The URL that Cloud Tasks will call to process a job. This should be the URL to your Laravel app. By default we will use the URL that dispatched the job. |`https://.com` | `STACKKIT_CLOUD_TASKS_SIGNED_AUDIENCE` (optional) | True or false depending if you want extra security by signing the audience of your tasks. May misbehave in certain Cloud Run setups. Defaults to true. | `true` @@ -103,7 +103,6 @@ With Cloud Tasks, this is not the case. Instead, Cloud Tasks will schedule the j #### Good to know -- If `STACKKIT_APP_ENGINE_TASK` is true, `STACKKIT_CLOUD_TASKS_SERVICE_EMAIL` and `STACKKIT_CLOUD_TASKS_HANDLER` will be ignored. - The "Min backoff" and "Max backoff" options in Cloud Tasks are ignored. This is intentional: Laravel has its own backoff feature (which is more powerful than what Cloud Tasks offers) and therefore I have chosen that over the Cloud Tasks one. - Similarly to the backoff feature, I have also chosen to let the package do job retries the 'Laravel way'. In Cloud Tasks, when a task throws an exception, Cloud Tasks will decide for itself when to retry the task (based on the backoff values). It will also manage its own state and knows how many times a task has been retried. This is different from Laravel. In typical Laravel queues, when a job throws an exception, the job is deleted and released back onto the queue. In order to support Laravel's backoff feature, this package must behave the same way about job retries. @@ -151,7 +150,7 @@ The dashboard is accessible at the URI: /cloud-tasks Authentication
-Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable with a path to the credentials file. +Set the `STACKKIT_CLOUD_TASKS_CREDENTIAL_FILE` environment variable with a path to the credentials file. More info: https://cloud.google.com/docs/authentication/production From cecf786dff25c815c5f2ce4464b10157ecfad064 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Fri, 19 Jan 2024 23:17:21 +0100 Subject: [PATCH 10/14] wip --- src/CloudTasksQueue.php | 2 +- src/CloudTasksServiceProvider.php | 5 ++--- src/TaskHandler.php | 14 ++++---------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/CloudTasksQueue.php b/src/CloudTasksQueue.php index 2adfa30..ccb7798 100644 --- a/src/CloudTasksQueue.php +++ b/src/CloudTasksQueue.php @@ -154,7 +154,7 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) $task = $this->createTask(); $task->setName($this->taskName($queue, $payload)); - if ($this->config['app_engine']) { + if (!empty($this->config['app_engine'])) { $path = \Safe\parse_url(route('cloud-tasks.handle-task'), PHP_URL_PATH); $appEngineRequest = new AppEngineHttpRequest(); diff --git a/src/CloudTasksServiceProvider.php b/src/CloudTasksServiceProvider.php index 1c9aaa9..dec7ab7 100644 --- a/src/CloudTasksServiceProvider.php +++ b/src/CloudTasksServiceProvider.php @@ -32,9 +32,8 @@ private function registerClient(): void $this->app->singleton(CloudTasksClient::class, function ($app) { $config = config('queue.connections.cloudtasks'); $options = []; - $x = $config['credential_file']; - if (!empty($x)) { - $options['credentials'] = $x; + if (!empty($config['credential_file'])) { + $options['credentials'] = $config['credential_file']; } return new CloudTasksClient($options); }); diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 232d9b2..d8878df 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -36,10 +36,6 @@ class TaskHandler * @var RetryConfig */ private $retryConfig = null; - /** - * @var string - */ - private $taskName; public function __construct(CloudTasksClient $client) { @@ -54,7 +50,7 @@ public function handle(?string $task = null): void $this->setQueue(); - if (!$this->config['app_engine']) { + if (!empty($this->config['app_engine'])) { OpenIdVerificator::verify(request()->bearerToken(), $this->config); } @@ -76,11 +72,10 @@ private function captureTask($task): array $array = []; } - $taskName = request()->header('X-CloudTasks-TaskName') ?? request()->header('X-AppEngine-TaskName'); $validator = validator([ 'json' => $task, 'task' => $array, - 'name_header' => $taskName, + 'name_header' => request()->header('X-CloudTasks-TaskName') ?? request()->header('X-AppEngine-TaskName'), ], [ 'json' => 'required|json', 'task' => 'required|array', @@ -90,7 +85,6 @@ private function captureTask($task): array try { $validator->validate(); - $this->taskName = $taskName; } catch (ValidationException $e) { if (config('app.debug')) { throw $e; @@ -130,7 +124,7 @@ private function handleTask(array $task): void $this->config['project'], $this->config['location'], $job->getQueue() ?: $this->config['queue'], - $this->taskName, + request()->header('X-CloudTasks-TaskName') ?? request()->header('X-AppEngine-TaskName'), ); try { @@ -146,7 +140,7 @@ private function handleTask(array $task): void // If the task has a [X-CloudTasks-TaskRetryCount] header higher than 0, then // we know the job was created using an earlier version of the package. This // job does not have the attempts tracked internally yet. - $taskRetryCountHeader = request()->header('X-CloudTasks-TaskRetryCount'); + $taskRetryCountHeader = request()->header('X-CloudTasks-TaskRetryCount') ?? request()->header('X-AppEngine-TaskRetryCount'); if ($taskRetryCountHeader && (int)$taskRetryCountHeader > 0) { $job->setAttempts((int)$taskRetryCountHeader); } else { From f4c4b6c79f257e32f094f2b8403f042a0b8c523f Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Tue, 30 Jan 2024 22:09:56 +0100 Subject: [PATCH 11/14] Remove config validation --- src/CloudTasksConnector.php | 2 -- src/Config.php | 15 ----------- tests/ConfigTest.php | 54 ------------------------------------- 3 files changed, 71 deletions(-) delete mode 100644 tests/ConfigTest.php diff --git a/src/CloudTasksConnector.php b/src/CloudTasksConnector.php index db81cd6..8ff23e9 100644 --- a/src/CloudTasksConnector.php +++ b/src/CloudTasksConnector.php @@ -9,8 +9,6 @@ class CloudTasksConnector implements ConnectorInterface { public function connect(array $config) { - Config::validate($config); - // The handler is the URL which Cloud Tasks will call with the job payload. This // URL of the handler can be manually set through an environment variable, but // if it is not then we will choose a sensible default (the current app url) diff --git a/src/Config.php b/src/Config.php index 075fc3a..9819d6e 100644 --- a/src/Config.php +++ b/src/Config.php @@ -9,21 +9,6 @@ class Config { - public static function validate(array $config): void - { - if (empty($config['project'])) { - throw new Error(Errors::invalidProject()); - } - - if (empty($config['location'])) { - throw new Error(Errors::invalidLocation()); - } - - if (empty($config['service_account_email']) && empty($config['app_engine'])) { - throw new Error(Errors::serviceAccountOrAppEngine()); - } - } - /** * @param Closure|string $handler */ diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php deleted file mode 100644 index 317a801..0000000 --- a/tests/ConfigTest.php +++ /dev/null @@ -1,54 +0,0 @@ -setConfigValue('project', ''); - - $this->expectException(Error::class); - $this->expectExceptionMessage(Errors::invalidProject()); - - SimpleJob::dispatch(); - } - - /** @test */ - public function location_is_required() - { - $this->setConfigValue('location', ''); - - $this->expectException(Error::class); - $this->expectExceptionMessage(Errors::invalidLocation()); - - SimpleJob::dispatch(); - } - - /** @test */ - public function there_is_a_sensible_handler_default() - { - $this->setConfigValue('handler', ''); - - $this->expectException(Error::class); - $this->expectExceptionMessage(Errors::invalidHandler()); - - SimpleJob::dispatch(); - } - - /** @test */ - public function service_email_is_required() - { - $this->setConfigValue('service_account_email', ''); - - $this->expectException(Error::class); - $this->expectExceptionMessage(Errors::invalidServiceAccountEmail()); - - SimpleJob::dispatch(); - } -} From ac072a756f63a45142986d18a1425e748382f95e Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Tue, 30 Jan 2024 22:10:03 +0100 Subject: [PATCH 12/14] Fix incorrect empty check --- src/TaskHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TaskHandler.php b/src/TaskHandler.php index d8878df..8d3a40e 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -50,7 +50,7 @@ public function handle(?string $task = null): void $this->setQueue(); - if (!empty($this->config['app_engine'])) { + if (empty($this->config['app_engine'])) { OpenIdVerificator::verify(request()->bearerToken(), $this->config); } From 74c96adf4ba9e62101f35471dcf828f8672e3628 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Tue, 30 Jan 2024 22:22:24 +0100 Subject: [PATCH 13/14] Remove STACKKIT_CLOUD_TASKS_CREDENTIAL_FILE --- README.md | 6 +----- src/CloudTasksServiceProvider.php | 9 ++------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ca5346d..4c15678 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,6 @@ Please check the [Laravel support policy](https://laravel.com/docs/master/releas 'service_account_email' => env('STACKKIT_CLOUD_TASKS_SERVICE_EMAIL', ''), 'signed_audience' => env('STACKKIT_CLOUD_TASKS_SIGNED_AUDIENCE', true), - // Optional - 'credential_file' => env('STACKKIT_CLOUD_TASKS_CREDENTIAL_FILE', ''), - // Required when using AppEngine 'app_engine' => env('STACKKIT_APP_ENGINE_TASK', false), 'app_engine_service' => env('STACKKIT_APP_ENGINE_SERVICE', ''), @@ -82,7 +79,6 @@ Please check the table below on what the values mean and what their value should | `STACKKIT_CLOUD_TASKS_PROJECT` | The project your queue belongs to. |`my-project` | `STACKKIT_CLOUD_TASKS_LOCATION` | The region where the project is hosted. |`europe-west6` | `STACKKIT_CLOUD_TASKS_QUEUE` | The default queue a job will be added to. |`emails` -| `STACKKIT_CLOUD_TASKS_CREDENTIAL_FILE` (optional) | A json credential file to authenticate the connection (from outside AppEngine) |`project-123.json` | **App Engine** | `STACKKIT_APP_ENGINE_TASK` (optional) | Set to true to use App Engine task (else a Http task will be used). Defaults to false. |`true` | `STACKKIT_APP_ENGINE_SERVICE` (optional) | The App Engine service to handle the task (only if using App Engine task). |`api` @@ -150,7 +146,7 @@ The dashboard is accessible at the URI: /cloud-tasks Authentication
-Set the `STACKKIT_CLOUD_TASKS_CREDENTIAL_FILE` environment variable with a path to the credentials file. +Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable with a path to the credentials file. More info: https://cloud.google.com/docs/authentication/production diff --git a/src/CloudTasksServiceProvider.php b/src/CloudTasksServiceProvider.php index dec7ab7..d22a281 100644 --- a/src/CloudTasksServiceProvider.php +++ b/src/CloudTasksServiceProvider.php @@ -29,13 +29,8 @@ public function boot(): void private function registerClient(): void { - $this->app->singleton(CloudTasksClient::class, function ($app) { - $config = config('queue.connections.cloudtasks'); - $options = []; - if (!empty($config['credential_file'])) { - $options['credentials'] = $config['credential_file']; - } - return new CloudTasksClient($options); + $this->app->singleton(CloudTasksClient::class, function () { + return new CloudTasksClient(); }); $this->app->bind('open-id-verificator', OpenIdVerificatorConcrete::class); From a4a30986f32c90784a72131b89e237f33b909929 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Tue, 13 Feb 2024 19:32:30 +0100 Subject: [PATCH 14/14] Add tests --- src/DashboardService.php | 4 +- tests/CloudTasksDashboardTest.php | 56 +++++++++++++++++++---- tests/ConfigHandlerTest.php | 2 +- tests/QueueAppEngineTest.php | 74 +++++++++++++++++++++++++++++++ tests/TestCase.php | 34 +++++++++++++- 5 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 tests/QueueAppEngineTest.php diff --git a/src/DashboardService.php b/src/DashboardService.php index 8800e3d..4dc5cec 100644 --- a/src/DashboardService.php +++ b/src/DashboardService.php @@ -21,9 +21,9 @@ public static function make(): DashboardService private function getTaskBody(Task $task): string { - $httpRequest = $task->getHttpRequest(); + $httpRequest = $task->getHttpRequest() ?: $task->getAppEngineHttpRequest(); - if (! $httpRequest instanceof HttpRequest) { + if (! $httpRequest) { throw new Exception('Task does not have a HTTP request.'); } diff --git a/tests/CloudTasksDashboardTest.php b/tests/CloudTasksDashboardTest.php index 6cb7b99..c8820c9 100644 --- a/tests/CloudTasksDashboardTest.php +++ b/tests/CloudTasksDashboardTest.php @@ -241,10 +241,15 @@ public function it_returns_info_about_a_specific_task() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_is_dispatched_it_will_be_added_to_the_dashboard() + public function when_a_job_is_dispatched_it_will_be_added_to_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + CloudTasksApi::fake(); $tasksBefore = StackkitCloudTask::count(); $job = $this->dispatch(new SimpleJob()); @@ -280,10 +285,15 @@ public function when_dashboard_is_disabled_jobs_will_not_be_added_to_the_dashboa /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_is_scheduled_it_will_be_added_as_such() + public function when_a_job_is_scheduled_it_will_be_added_as_such(array $test) { // Arrange + $this->withTaskType($test['task_type']); + CloudTasksApi::fake(); Carbon::setTestNow(now()); $tasksBefore = StackkitCloudTask::count(); @@ -305,10 +315,15 @@ public function when_a_job_is_scheduled_it_will_be_added_as_such() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_is_running_it_will_be_updated_in_the_dashboard() + public function when_a_job_is_running_it_will_be_updated_in_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); @@ -331,10 +346,15 @@ public function when_a_job_is_running_it_will_be_updated_in_the_dashboard() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_is_successful_it_will_be_updated_in_the_dashboard() + public function when_a_job_is_successful_it_will_be_updated_in_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); @@ -357,10 +377,15 @@ public function when_a_job_is_successful_it_will_be_updated_in_the_dashboard() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_errors_it_will_be_updated_in_the_dashboard() + public function when_a_job_errors_it_will_be_updated_in_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); @@ -384,10 +409,15 @@ public function when_a_job_errors_it_will_be_updated_in_the_dashboard() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_fails_it_will_be_updated_in_the_dashboard() + public function when_a_job_fails_it_will_be_updated_in_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); @@ -416,10 +446,15 @@ public function when_a_job_fails_it_will_be_updated_in_the_dashboard() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_is_released_it_will_be_updated_in_the_dashboard() + public function when_a_job_is_released_it_will_be_updated_in_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); @@ -447,10 +482,15 @@ public function when_a_job_is_released_it_will_be_updated_in_the_dashboard() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function job_release_delay_is_added_to_the_metadata() + public function job_release_delay_is_added_to_the_metadata(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); diff --git a/tests/ConfigHandlerTest.php b/tests/ConfigHandlerTest.php index 523f6a4..6c30e3c 100644 --- a/tests/ConfigHandlerTest.php +++ b/tests/ConfigHandlerTest.php @@ -14,7 +14,7 @@ public function test_it_allows_a_handler_url_to_contain_path(string $handler, st self::assertSame($expectedHandler, Config::getHandler($handler)); } - public function handlerDataProvider(): array + public static function handlerDataProvider(): array { return [ ['https://example.com', 'https://example.com/handle-task'], diff --git a/tests/QueueAppEngineTest.php b/tests/QueueAppEngineTest.php new file mode 100644 index 0000000..994444f --- /dev/null +++ b/tests/QueueAppEngineTest.php @@ -0,0 +1,74 @@ +withTaskType('appengine'); + } + + /** + * @test + */ + public function an_app_engine_http_request_with_the_handler_url_is_made() + { + // Arrange + CloudTasksApi::fake(); + + // Act + $this->dispatch(new SimpleJob()); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task): bool { + return $task->getAppEngineHttpRequest()->getRelativeUri() === '/handle-task'; + }); + } + + /** + * @test + */ + public function it_routes_to_the_service() + { + // Arrange + CloudTasksApi::fake(); + + // Act + $this->dispatch(new SimpleJob()); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task): bool { + return $task->getAppEngineHttpRequest()->getAppEngineRouting()->getService() === 'api'; + }); + } + + /** + * @test + */ + public function it_contains_the_payload() + { + // Arrange + CloudTasksApi::fake(); + + // Act + $this->dispatch($job = new SimpleJob()); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) use ($job): bool { + $decoded = json_decode($task->getAppEngineHttpRequest()->getBody(), true); + + return $decoded['displayName'] === SimpleJob::class + && $decoded['job'] === 'Illuminate\Queue\CallQueuedHandler@call' + && $decoded['data']['command'] === serialize($job); + }); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index bbdd1e1..048d6a9 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -130,12 +130,20 @@ public function dispatch($job) $task = null; Event::listen(TaskCreated::class, function (TaskCreated $event) use (&$payload, &$payloadAsArray, &$task) { - $payload = $event->task->getHttpRequest()->getBody(); + $request = $event->task->getHttpRequest() ?? $event->task->getAppEngineHttpRequest(); + $payload = $request->getBody(); $payloadAsArray = json_decode($payload, true); $task = $event->task; [,,,,,,,$taskName] = explode('/', $task->getName()); - request()->headers->set('X-Cloudtasks-Taskname', $taskName); + + if ($task->hasHttpRequest()) { + request()->headers->set('X-Cloudtasks-Taskname', $taskName); + } + + if ($task->hasAppEngineHttpRequest()) { + request()->headers->set('X-AppEngine-TaskName', $taskName); + } }); dispatch($job); @@ -248,4 +256,26 @@ public function getJobReleasedAfterExceptionEvent(): string ? PackageJobReleasedAfterException::class : JobReleasedAfterException::class; } + + public function withTaskType(string $taskType): void + { + switch ($taskType) { + case 'appengine': + $this->setConfigValue('handler', null); + $this->setConfigValue('service_account_email', null); + $this->setConfigValue('signed_audience', null); + + $this->setConfigValue('app_engine', true); + $this->setConfigValue('app_engine_service', 'api'); + break; + case 'http': + $this->setConfigValue('app_engine', false); + $this->setConfigValue('app_engine_service', null); + + $this->setConfigValue('handler', 'https://docker.for.mac.localhost:8080'); + $this->setConfigValue('service_account_email', 'info@stackkit.io'); + $this->setConfigValue('signed_audience', true); + break; + } + } }