From 269a9345145437cfa44605eaec650492734d44e8 Mon Sep 17 00:00:00 2001 From: michalsn Date: Fri, 25 Apr 2025 20:18:09 +0200 Subject: [PATCH] feat: add queue task type to scheduler --- composer.json | 3 ++- docs/basic-usage.md | 42 +++++++++++++++++++++++++++++ phpstan-baseline.neon | 2 +- src/Scheduler.php | 12 +++++++++ src/Task.php | 23 +++++++++++++++- src/Test/MockTask.php | 1 + tests/_support/Config/Registrar.php | 26 ++++++++++++++++++ tests/_support/Jobs/Example.php | 25 +++++++++++++++++ tests/unit/SchedulerTest.php | 8 ++++++ tests/unit/TaskTest.php | 27 +++++++++++++++++++ 10 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 tests/_support/Config/Registrar.php create mode 100644 tests/_support/Jobs/Example.php diff --git a/composer.json b/composer.json index de0c9bc..e1c0ca1 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "require": { "php": "^8.1", "ext-json": "*", - "codeigniter4/settings": "^2.0" + "codeigniter4/settings": "^2.0", + "codeigniter4/queue": "dev-develop" }, "require-dev": { "codeigniter4/devkit": "^1.3", diff --git a/docs/basic-usage.md b/docs/basic-usage.md index 5bf7843..e844045 100644 --- a/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -81,6 +81,48 @@ a simple URL string, you can use a closure or command instead. $schedule->url('https://my-status-cloud.com?site=foo.com')->everyFiveMinutes(); ``` +### Scheduling Queue Jobs + +If you want to schedule a Queue Job, you can use the `queue()` method and specify the queue name, job name and data your job needs: + +```php +$schedule->queue('queue-name', 'jobName', ['data' => 'array'])->hourly(); +``` + +!!! note + + To learn more about the [Queue package](https://github.com/codeigniter4/queue) you can visit a project page. + + +The `singleInstance()` option, described in the next section, works a bit differently than with other scheduling methods. +Since queue jobs are added quickly and processed later in the background, the lock is applied as soon as the job is queued - not when it actually runs. + +```php +$schedule->queue('queue-name', 'jobName', ['data' => 'array']) + ->hourly() + ->singleInstance(); +``` + +This means: + +- The lock is created immediately when the job is queued. +- The lock is released only after the job is processed (whether it succeeds or fails). + +We can optionally pass a TTL to `singleInstance()` to limit how long the job lock should last: + +```php +$schedule->queue('queue-name', 'jobName', ['data' => 'array']) + ->hourly() + ->singleInstance(30 * MINUTE); +``` + +How it works: + +- The lock is set immediately when the job is queued. +- The job must start processing before the TTL expires (in this case, within 30 minutes). +- Once the job starts, the lock is renewed for the same TTL. +- So, effectively, you have 30 minutes to start, and another 30 minutes to complete the job. + ## Single Instance Tasks Some tasks can run longer than their scheduled interval. To prevent multiple instances of the same task running simultaneously, you can use the `singleInstance()` method: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f275e39..45e53a9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -39,7 +39,7 @@ parameters: - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''CodeIgniter\\\\Tasks\\\\Task'' and CodeIgniter\\Tasks\\Task will always evaluate to true\.$#' identifier: method.alreadyNarrowedType - count: 3 + count: 4 path: tests/unit/SchedulerTest.php - diff --git a/src/Scheduler.php b/src/Scheduler.php index b7d2b5e..a430813 100644 --- a/src/Scheduler.php +++ b/src/Scheduler.php @@ -14,6 +14,8 @@ namespace CodeIgniter\Tasks; use Closure; +use CodeIgniter\Queue\Queue; +use CodeIgniter\Tasks\Exceptions\TasksException; class Scheduler { @@ -73,6 +75,16 @@ public function url(string $url): Task return $this->createTask('url', $url); } + /** + * Schedule a queue job. + * + * @throws TasksException + */ + public function queue(string $queue, string $job, array $data): Task + { + return $this->createTask('queue', [$queue, $job, $data]); + } + // -------------------------------------------------------------------- /** diff --git a/src/Task.php b/src/Task.php index a5659c6..e121fb1 100644 --- a/src/Task.php +++ b/src/Task.php @@ -15,6 +15,7 @@ use CodeIgniter\Events\Events; use CodeIgniter\I18n\Time; +use CodeIgniter\Queue\Payloads\PayloadMetadata; use CodeIgniter\Tasks\Exceptions\TasksException; use InvalidArgumentException; use ReflectionException; @@ -48,6 +49,7 @@ class Task 'closure', 'event', 'url', + 'queue', ]; /** @@ -142,7 +144,7 @@ public function run() return $this->{$method}(); } finally { - if ($this->singleInstance) { + if ($this->singleInstance && $this->getType() !== 'queue') { cache()->delete($lockKey); } } @@ -297,6 +299,25 @@ protected function runUrl() return $response->getBody(); } + /** + * Sends a job to the queue. + */ + protected function runQueue() + { + $queueAction = $this->getAction(); + + if ($this->singleInstance) { + // Create PayloadMetadata instance with the task lock key + $queueAction[] = new PayloadMetadata([ + 'queue' => $queueAction[0], + 'taskLockTTL' => $this->singleInstanceTTL, + 'taskLockKey' => $this->getLockKey(), + ]); + } + + return service('queue')->push(...$queueAction); + } + /** * Builds a unique name for the task. * Used when an existing name doesn't exist. diff --git a/src/Test/MockTask.php b/src/Test/MockTask.php index fe1d54a..0a685b9 100644 --- a/src/Test/MockTask.php +++ b/src/Test/MockTask.php @@ -46,6 +46,7 @@ public function run() 'closure' => 42, 'event' => true, 'url' => 'body', + 'queue' => true, ][$this->type]; } } diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php new file mode 100644 index 0000000..dd7e7e4 --- /dev/null +++ b/tests/_support/Config/Registrar.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Config; + +class Registrar +{ + public static function Queue(): array + { + return [ + 'jobHandlers' => [ + 'job-example' => 'Tests\Jobs\Example', + ], + ]; + } +} diff --git a/tests/_support/Jobs/Example.php b/tests/_support/Jobs/Example.php new file mode 100644 index 0000000..d8115bd --- /dev/null +++ b/tests/_support/Jobs/Example.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Jobs; + +use CodeIgniter\Queue\BaseJob; +use CodeIgniter\Queue\Interfaces\JobInterface; + +class Example extends BaseJob implements JobInterface +{ + public function process(): bool + { + return true; + } +} diff --git a/tests/unit/SchedulerTest.php b/tests/unit/SchedulerTest.php index ed1c5cd..a332886 100644 --- a/tests/unit/SchedulerTest.php +++ b/tests/unit/SchedulerTest.php @@ -56,4 +56,12 @@ public function testShellSavesTask() $this->assertInstanceOf(Task::class, $task); $this->assertSame('foo:bar', $task->getAction()); } + + public function testQueueSavesTask() + { + $task = $this->scheduler->queue('example', 'job-example', ['data' => 'array']); + + $this->assertInstanceOf(Task::class, $task); + $this->assertSame(['example', 'job-example', ['data' => 'array']], $task->getAction()); + } } diff --git a/tests/unit/TaskTest.php b/tests/unit/TaskTest.php index 55862fe..e61b3f5 100644 --- a/tests/unit/TaskTest.php +++ b/tests/unit/TaskTest.php @@ -297,4 +297,31 @@ public function testSingleInstanceWithCustomTTL() $this->assertNull($this->getPrivateProperty($task2, 'singleInstanceTTL')); } + + public function testRunQueue() + { + $task = new Task('queue', ['example', 'job-example', []]); + $task->named('test_run_queue'); + + $result = $task->run(); + $this->assertTrue($result); + + // No lock + $lockKey = $this->getPrivateMethodInvoker($task, 'getLockKey')(); + $this->assertNull(cache()->get($lockKey)); + } + + public function testRunQueueWithSingleInstance() + { + $task = new Task('queue', ['example', 'job-example', []]); + $task->named('test_run_queue_single'); + $task->singleInstance(); + + $result = $task->run(); + $this->assertTrue($result); + + // Lock is still present + $lockKey = $this->getPrivateMethodInvoker($task, 'getLockKey')(); + $this->assertNotNull(cache()->get($lockKey)); + } }