Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 4 additions & 5 deletions src/Controller/Admin/QueueController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Queue\Queue\AddFromBackendInterface;
use Queue\Queue\AddInterface;
use Queue\Queue\TaskFinder;
use Queue\Queue\TaskMetadata;

/**
* @property \Queue\Model\Table\QueuedJobsTable $QueuedJobs
Expand Down Expand Up @@ -66,9 +67,7 @@ public function index() {

$taskDescriptions = [];
foreach ($tasks as $task => $className) {
/** @var \Queue\Queue\Task $taskObject */
$taskObject = new $className();
$taskDescriptions[$task] = $taskObject->description();
$taskDescriptions[$task] = TaskMetadata::fromClass($className)->description;
}

$servers = $QueueProcesses->serverList();
Expand Down Expand Up @@ -132,8 +131,8 @@ public function addJob() {
throw new NotFoundException('Class not found for job `' . $job . '`');
}

$object = new $className();
if ($object instanceof AddInterface) {
if (is_subclass_of($className, AddInterface::class)) {
$object = new $className();
$object->add(null);
} else {
$this->QueuedJobs->createJob($job);
Expand Down
17 changes: 7 additions & 10 deletions src/Queue/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public static function ignoredTasks(): array {
}

/**
* @param array<string> $tasks
* @param array<string, class-string<\Queue\Queue\Task>> $tasks
*
* @throws \RuntimeException
*
Expand All @@ -134,13 +134,12 @@ public static function taskConfig(array $tasks): array {
foreach ($tasks as $task => $className) {
[$pluginName, $taskName] = pluginSplit($task);

/** @var \Queue\Queue\Task $taskObject */
$taskObject = new $className();
$taskMeta = TaskMetadata::fromClass($className);

// Get task-specific config overrides from Configure
$taskConfig = $taskOverrides[$task] ?? [];

$taskTimeout = $taskConfig['timeout'] ?? $taskObject->timeout ?? $defaultTimeout;
$taskTimeout = $taskConfig['timeout'] ?? $taskMeta->timeout ?? $defaultTimeout;

// Auto-cap task timeout to defaultRequeueTimeout to prevent duplicate execution
if ($taskTimeout > $defaultTimeout) {
Expand All @@ -151,12 +150,10 @@ public static function taskConfig(array $tasks): array {
$config[$task]['name'] = $taskName;
$config[$task]['plugin'] = $pluginName;
$config[$task]['timeout'] = $taskTimeout;
$config[$task]['retries'] = $taskConfig['retries'] ?? $taskObject->retries ?? static::defaultworkerretries();
$config[$task]['rate'] = $taskConfig['rate'] ?? $taskObject->rate;
$config[$task]['costs'] = $taskConfig['costs'] ?? $taskObject->costs;
$config[$task]['unique'] = $taskConfig['unique'] ?? $taskObject->unique;

unset($taskObject);
$config[$task]['retries'] = $taskConfig['retries'] ?? $taskMeta->retries ?? static::defaultworkerretries();
$config[$task]['rate'] = $taskConfig['rate'] ?? $taskMeta->rate;
$config[$task]['costs'] = $taskConfig['costs'] ?? $taskMeta->costs;
$config[$task]['unique'] = $taskConfig['unique'] ?? $taskMeta->unique;
}

return $config;
Expand Down
78 changes: 78 additions & 0 deletions src/Queue/TaskMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);

namespace Queue\Queue;

use ReflectionClass;
use Throwable;

/**
* Reads task metadata (timeout, retries, etc.) from a task class
* without instantiating it.
*
* This allows introspection of tasks that have required constructor
* parameters (e.g. DI dependencies) where `new $className()` would fail.
*/
class TaskMetadata {

public function __construct(
public readonly string $class,
public readonly ?int $timeout,
public readonly ?int $retries,
public readonly int $rate,
public readonly int $costs,
public readonly bool $unique,
public readonly ?string $description,
) {
}

/**
* Build metadata for a task class by reading declared property defaults
* and calling description() without a full constructor.
*
* @phpstan-param class-string<\Queue\Queue\Task> $className
*
* @param string $className
*
* @return self
*/
public static function fromClass(string $className): self {
$reflection = new ReflectionClass($className);

$timeout = static::propertyDefault($reflection, 'timeout');
$retries = static::propertyDefault($reflection, 'retries');
$rate = static::propertyDefault($reflection, 'rate') ?? 0;
$costs = static::propertyDefault($reflection, 'costs') ?? 0;
$unique = static::propertyDefault($reflection, 'unique') ?? false;

$description = null;
try {
/** @var \Queue\Queue\Task $instance */
$instance = $reflection->newInstanceWithoutConstructor();
$description = $instance->description();
} catch (Throwable) {
}

return new self($className, $timeout, $retries, $rate, $costs, $unique, $description);
}

/**
* @param \ReflectionClass<\Queue\Queue\Task> $reflection
* @param string $property
*
* @return mixed
*/
protected static function propertyDefault(ReflectionClass $reflection, string $property): mixed {
if (!$reflection->hasProperty($property)) {
return null;
}

$prop = $reflection->getProperty($property);
if (!$prop->hasDefaultValue()) {
return null;
}

return $prop->getDefaultValue();
}

}
2 changes: 1 addition & 1 deletion tests/TestCase/Command/InfoCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function testExecute(): void {
$this->exec('queue info');

$output = $this->_out->output();
$this->assertStringContainsString('16 tasks available:', $output);
$this->assertStringContainsString('17 tasks available:', $output);
$this->assertExitCode(0);
}

Expand Down
57 changes: 57 additions & 0 deletions tests/TestCase/Queue/TaskMetadataTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);

namespace Queue\Test\TestCase\Queue;

use Cake\TestSuite\TestCase;
use Queue\Queue\TaskMetadata;
use TestApp\Queue\Task\InjectedTask;
use TestApp\Queue\Task\MetadataTask;

class TaskMetadataTest extends TestCase {

/**
* Test that fromClass reads declared property defaults correctly.
*
* @return void
*/
public function testFromClassReadsPropertyDefaults(): void {
$meta = TaskMetadata::fromClass(MetadataTask::class);

$this->assertSame(MetadataTask::class, $meta->class);
$this->assertSame(42, $meta->timeout);
$this->assertSame(3, $meta->retries);
$this->assertSame(5, $meta->rate);
$this->assertSame(75, $meta->costs);
$this->assertTrue($meta->unique);
}

/**
* Test that fromClass reads description() from tasks that override it.
*
* @return void
*/
public function testFromClassReadsDescription(): void {
$meta = TaskMetadata::fromClass(MetadataTask::class);

$this->assertSame('A task for testing metadata introspection', $meta->description);
}

/**
* Test that fromClass works for tasks with required constructor DI params
* that cannot be instantiated with new $className().
*
* @return void
*/
public function testFromClassWithDiConstructor(): void {
$meta = TaskMetadata::fromClass(InjectedTask::class);

$this->assertSame(InjectedTask::class, $meta->class);
$this->assertSame(10, $meta->timeout);
$this->assertNull($meta->retries);
$this->assertSame(0, $meta->rate);
$this->assertSame(0, $meta->costs);
$this->assertFalse($meta->unique);
}

}
33 changes: 33 additions & 0 deletions tests/test_app/src/Queue/Task/MetadataTask.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);

namespace TestApp\Queue\Task;

use Queue\Queue\Task;

/**
* Test task with explicit values for all metadata properties.
*
* Used to verify TaskMetadata reads the correct declared defaults
* without instantiating the task.
*/
class MetadataTask extends Task {

public ?int $timeout = 42;

public ?int $retries = 3;

public int $rate = 5;

public int $costs = 75;

public bool $unique = true;

public function run(array $data, int $jobId): void {
}

public function description(): ?string {
return 'A task for testing metadata introspection';
}

}
Loading