Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
42 changes: 42 additions & 0 deletions docs/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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

-
Expand Down
12 changes: 12 additions & 0 deletions src/Scheduler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
namespace CodeIgniter\Tasks;

use Closure;
use CodeIgniter\Queue\Queue;
use CodeIgniter\Tasks\Exceptions\TasksException;

class Scheduler
{
Expand Down Expand Up @@ -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]);
}

// --------------------------------------------------------------------

/**
Expand Down
23 changes: 22 additions & 1 deletion src/Task.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,6 +49,7 @@
'closure',
'event',
'url',
'queue',
];

/**
Expand Down Expand Up @@ -142,7 +144,7 @@

return $this->{$method}();
} finally {
if ($this->singleInstance) {
if ($this->singleInstance && $this->getType() !== 'queue') {
cache()->delete($lockKey);
}
}
Expand Down Expand Up @@ -297,6 +299,25 @@
return $response->getBody();
}

/**
* Sends a job to the queue.
*/
protected function runQueue()

Check warning on line 305 in src/Task.php

View workflow job for this annotation

GitHub Actions / infection / Mutation Testing

Escaped Mutant for Mutator "ProtectedVisibility": @@ @@ /** * Sends a job to the queue. */ - protected function runQueue() + private function runQueue() { $queueAction = $this->getAction(); if ($this->singleInstance) {
{
$queueAction = $this->getAction();

if ($this->singleInstance) {
// Create PayloadMetadata instance with the task lock key
$queueAction[] = new PayloadMetadata([

Check warning on line 311 in src/Task.php

View workflow job for this annotation

GitHub Actions / infection / Mutation Testing

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ $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()]); + $queueAction[] = new PayloadMetadata(['taskLockTTL' => $this->singleInstanceTTL, 'taskLockKey' => $this->getLockKey()]); } return service('queue')->push(...$queueAction); }
'queue' => $queueAction[0],

Check warning on line 312 in src/Task.php

View workflow job for this annotation

GitHub Actions / infection / Mutation Testing

Escaped Mutant for Mutator "IncrementInteger": @@ @@ $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()]); + $queueAction[] = new PayloadMetadata(['queue' => $queueAction[1], 'taskLockTTL' => $this->singleInstanceTTL, 'taskLockKey' => $this->getLockKey()]); } return service('queue')->push(...$queueAction); }
'taskLockTTL' => $this->singleInstanceTTL,

Check warning on line 313 in src/Task.php

View workflow job for this annotation

GitHub Actions / infection / Mutation Testing

Escaped Mutant for Mutator "ArrayItem": @@ @@ $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()]); + $queueAction[] = new PayloadMetadata(['queue' => $queueAction[0], 'taskLockTTL' > $this->singleInstanceTTL, 'taskLockKey' => $this->getLockKey()]); } return service('queue')->push(...$queueAction); }
'taskLockKey' => $this->getLockKey(),

Check warning on line 314 in src/Task.php

View workflow job for this annotation

GitHub Actions / infection / Mutation Testing

Escaped Mutant for Mutator "ArrayItem": @@ @@ $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()]); + $queueAction[] = new PayloadMetadata(['queue' => $queueAction[0], 'taskLockTTL' => $this->singleInstanceTTL, 'taskLockKey' > $this->getLockKey()]); } return service('queue')->push(...$queueAction); }
]);
}

return service('queue')->push(...$queueAction);
}

/**
* Builds a unique name for the task.
* Used when an existing name doesn't exist.
Expand Down
1 change: 1 addition & 0 deletions src/Test/MockTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
'closure' => 42,
'event' => true,
'url' => 'body',
'queue' => true,

Check warning on line 49 in src/Test/MockTask.php

View workflow job for this annotation

GitHub Actions / infection / Mutation Testing

Escaped Mutant for Mutator "TrueValue": @@ @@ throw TasksException::forInvalidTaskType($this->type); } $_SESSION['tasks_cache'] = [$this->type, $this->action]; - return ['command' => 'success', 'shell' => [], 'closure' => 42, 'event' => true, 'url' => 'body', 'queue' => true][$this->type]; + return ['command' => 'success', 'shell' => [], 'closure' => 42, 'event' => true, 'url' => 'body', 'queue' => false][$this->type]; } }
][$this->type];
}
}
26 changes: 26 additions & 0 deletions tests/_support/Config/Registrar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter Tasks.
*
* (c) CodeIgniter Foundation <[email protected]>
*
* 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',
],
];
}
}
25 changes: 25 additions & 0 deletions tests/_support/Jobs/Example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter Tasks.
*
* (c) CodeIgniter Foundation <[email protected]>
*
* 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;
}
}
8 changes: 8 additions & 0 deletions tests/unit/SchedulerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
27 changes: 27 additions & 0 deletions tests/unit/TaskTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}