diff --git a/UPGRADE.md b/UPGRADE.md index cb91a2d..5b84bad 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -3,6 +3,7 @@ * `TaskLog::getTaskObject()` was removed. Use `TaskDetailsNormalizer::deserializeTask($log)` instead. * `TaskLog::$ulid` was removed. +* `Task` has no constructor anymore, so you need to remove your `parent::__construct()` calls. 1.x to 2.0 diff --git a/src/Director/RunDirector.php b/src/Director/RunDirector.php index 0692c29..83c6f57 100644 --- a/src/Director/RunDirector.php +++ b/src/Director/RunDirector.php @@ -17,7 +17,7 @@ final class RunDirector */ public function __construct ( private readonly TaskLogModel $logModel, - private readonly TaskRun $run, + private readonly ?TaskRun $run, ) { $this->output = new ChainOutput(); @@ -40,6 +40,11 @@ public function getIo () : TorrStyle */ public function finish (bool $success) : void { + if (null === $this->run) + { + return; + } + $this->run->finish($success, $this->output->getBufferedOutput()); $this->logModel->flush(); } diff --git a/src/Director/TaskDirector.php b/src/Director/TaskDirector.php index 2d21622..7ea8213 100644 --- a/src/Director/TaskDirector.php +++ b/src/Director/TaskDirector.php @@ -2,6 +2,8 @@ namespace Torr\TaskManager\Director; +use Psr\Log\LoggerInterface; +use Torr\TaskManager\Identification\TaskIdentifier; use Torr\TaskManager\Model\TaskLogModel; use Torr\TaskManager\Task\Task; @@ -11,6 +13,8 @@ */ public function __construct ( private TaskLogModel $logModel, + private LoggerInterface $logger, + private TaskIdentifier $taskIdentifier, ) {} /** @@ -18,7 +22,19 @@ public function __construct ( */ public function startRun (Task $task) : RunDirector { - $log = $this->logModel->getLogForTask($task); + $uuid = $this->taskIdentifier->getUuid($task); + + if (null === $uuid) + { + $this->logger->critical("Could not identify task of type {type}", [ + "type" => get_debug_type($task), + "task" => $task, + ]); + + return new RunDirector($this->logModel, null); + } + + $log = $this->logModel->getLogForUuid($task, $uuid); // create run $run = $this->logModel->createRunForTask($log); diff --git a/src/Entity/TaskLog.php b/src/Entity/TaskLog.php index e446d43..ab90918 100644 --- a/src/Entity/TaskLog.php +++ b/src/Entity/TaskLog.php @@ -69,12 +69,12 @@ class TaskLog /** */ public function __construct ( - Task $task, + object $task, + string $uuid, ) { $this->taskClass = $task::class; - /** @phpstan-ignore-next-line property.deprecated (The uuid integration will be refactored in v4) */ - $this->taskId = $task->ulid; + $this->taskId = $uuid; $this->runs = new ArrayCollection(); $this->timeQueued = now(); } diff --git a/src/Identification/TaskIdStamp.php b/src/Identification/TaskIdStamp.php new file mode 100644 index 0000000..96ccab7 --- /dev/null +++ b/src/Identification/TaskIdStamp.php @@ -0,0 +1,21 @@ +taskId = new UuidV7()->toRfc4122(); + } +} diff --git a/src/Identification/TaskIdentificationMiddleware.php b/src/Identification/TaskIdentificationMiddleware.php new file mode 100644 index 0000000..76b52b4 --- /dev/null +++ b/src/Identification/TaskIdentificationMiddleware.php @@ -0,0 +1,122 @@ +last(TaskIdStamp::class); + + if (null === $stamp) + { + $stamp = new TaskIdStamp(); + $envelope = $envelope->with($stamp); + } + + $this->taskIdentifier->setUuid($envelope->getMessage(), $stamp->taskId); + + // create task run before + $logEntry = $this->logModel->getLogForUuid($envelope->getMessage(), $stamp->taskId); + $logEntry->setTaskDetails($this->detailsNormalizer->normalizeTaskDetails($envelope)); + $this->logModel->flush(); + + try + { + // push message through the stack + $envelope = $stack->next()->handle($envelope, $stack); + } + catch (ExceptionInterface $exception) + { + $run = $this->getTaskRun($logEntry, $envelope->getMessage()); + + if (null !== $run) + { + $run->abort(false, $exception->getMessage()); + $this->logModel->flush(); + } + + throw $exception; + } + + $handledStamp = $envelope->last(HandledStamp::class); + + if (null !== $handledStamp) + { + $run = $this->getTaskRun($logEntry, $envelope->getMessage()); + + if (null !== $run) + { + $run->abort( + success: true, + output: $this->extractResultContent($handledStamp), + ); + } + } + + // update log with most up-to-date details + $logEntry->setTaskDetails($this->detailsNormalizer->normalizeTaskDetails($envelope)); + $this->logModel->flush(); + + return $envelope; + } + + /** + * + */ + private function getTaskRun (TaskLog $log, object $message) : ?TaskRun + { + // if the message is not a task, no task director will be called. + // so we can create a run here + if (!$message instanceof Task) + { + return $this->logModel->createRunForTask($log); + } + + return $log->getLastUnfinishedRun(); + } + + /** + */ + private function extractResultContent (HandledStamp $handledStamp) : ?string + { + $result = $handledStamp->getResult(); + + if (\is_string($result)) + { + return $result; + } + + if ($result instanceof RunCommandContext) + { + return $result->output; + } + + return null; + } +} diff --git a/src/Identification/TaskIdentifier.php b/src/Identification/TaskIdentifier.php new file mode 100644 index 0000000..3324e03 --- /dev/null +++ b/src/Identification/TaskIdentifier.php @@ -0,0 +1,46 @@ + */ + private \WeakMap $uuidMap; + + /** + */ + public function __construct () + { + $this->uuidMap = new \WeakMap(); + } + + /** + * + */ + public function setUuid (object $message, string $uuid) : void + { + $this->uuidMap[$message] = $uuid; + } + + /** + * + */ + public function getUuid (object $message) : ?string + { + return $this->uuidMap[$message] + ?? null; + } + + /** + * + */ + public function reset () : void + { + $this->uuidMap = new \WeakMap(); + } +} diff --git a/src/Listener/MessengerEventListener.php b/src/Listener/MessengerEventListener.php deleted file mode 100644 index bbacb18..0000000 --- a/src/Listener/MessengerEventListener.php +++ /dev/null @@ -1,102 +0,0 @@ -getEnvelope(); - $taskLog = $this->getLogForEvent($envelope); - - // make sure that the log entry is created and flushed - if (null !== $taskLog) - { - // update envelope with current version - $taskLog->setTaskDetails($this->detailsNormalizer->normalizeTaskDetails($envelope)); - - $this->logModel->flush(); - } - } - - /** - * Automatically integrate - */ - #[AsEventListener] - public function onWorkerMessageHandled (WorkerMessageHandledEvent $event) : void - { - $envelope = $event->getEnvelope(); - $taskLog = $this->getLogForEvent($envelope); - - if (null === $taskLog) - { - return; - } - - // update envelope with current version - $taskLog->setTaskDetails($this->detailsNormalizer->normalizeTaskDetails($envelope)); - - // abort run as success. It wasn't marked as finished manually, but it succeeded nonetheless. - $run = $taskLog->getLastUnfinishedRun(); - $run?->abort(true); - - $this->logModel->flush(); - } - - #[AsEventListener] - public function onWorkerMessageFailed (WorkerMessageFailedEvent $event) : void - { - $envelope = $event->getEnvelope(); - $taskLog = $this->getLogForEvent($envelope); - - if (null === $taskLog) - { - return; - } - - // update envelope with current version - $taskLog->setTaskDetails($this->detailsNormalizer->normalizeTaskDetails($envelope)); - - // abort run as failure. It wasn't marked as finished manually and it failed. - $run = $taskLog->getLastUnfinishedRun(); - $run?->abort(false, $event->getThrowable()->getMessage()); - - $this->logModel->flush(); - } - - /** - * - */ - private function getLogForEvent (Envelope $envelope) : ?TaskLog - { - $message = $envelope->getMessage(); - - // filter out task manager internal tasks - return $message instanceof Task && !$message instanceof TaskManagerInternalTask - ? $this->logModel->getLogForTask($message) - : null; - } -} diff --git a/src/Manager/TaskManager.php b/src/Manager/TaskManager.php index c53414f..627e0fc 100644 --- a/src/Manager/TaskManager.php +++ b/src/Manager/TaskManager.php @@ -6,6 +6,7 @@ use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Stamp\DeduplicateStamp; use Symfony\Component\Messenger\Stamp\StampInterface; +use Torr\TaskManager\Identification\TaskIdStamp; use Torr\TaskManager\Task\Task; use Torr\TaskManager\Transport\TransportsHelper; @@ -27,21 +28,25 @@ public function __construct ( * * @api */ - public function enqueue (Task $task, array $stamps = []) : string + public function enqueue (object $task, array $stamps = []) : string { - $uniqueTaskId = $task->getMetaData()->uniqueTaskId; + $uniqueTaskId = $task instanceof Task + ? $task->getMetaData()->uniqueTaskId + : null; if (null !== $uniqueTaskId) { $stamps[] = new DeduplicateStamp($uniqueTaskId); } + $id = new TaskIdStamp(); + $stamps[] = $id; + $this->messageBus->dispatch( new Envelope($task, $stamps), ); - /** @phpstan-ignore-next-line property.deprecated (The uuid integration will be refactored in v4) */ - return $task->ulid; + return $id->taskId; } /** diff --git a/src/Model/TaskLogModel.php b/src/Model/TaskLogModel.php index 58cea57..17013a3 100644 --- a/src/Model/TaskLogModel.php +++ b/src/Model/TaskLogModel.php @@ -8,7 +8,6 @@ use Torr\TaskManager\Entity\TaskLog; use Torr\TaskManager\Entity\TaskRun; use Torr\TaskManager\Task\DispatchAfterRunTask\DispatchAfterRunTask; -use Torr\TaskManager\Task\Task; final class TaskLogModel { @@ -47,12 +46,11 @@ public function findByTaskId (string $taskId) : ?TaskLog } /** - * Gets or creates the log entry for the given task + * Gets or creates the log entry for the given task uuid */ - public function getLogForTask (Task $task) : TaskLog + public function getLogForUuid (object $task, string $uuid) : TaskLog { - /** @phpstan-ignore-next-line property.deprecated (The uuid integration will be refactored in v4) */ - $log = $this->findByTaskId($task->ulid); + $log = $this->findByTaskId($uuid); if (null !== $log) { @@ -60,7 +58,7 @@ public function getLogForTask (Task $task) : TaskLog } // if it isn't created yet, create a new one - $log = new TaskLog($task); + $log = new TaskLog($task, $uuid); $this->entityManager->persist($log); return $log; diff --git a/src/Normalizer/TaskDetailsNormalizer.php b/src/Normalizer/TaskDetailsNormalizer.php index 4d3466f..21f7acc 100644 --- a/src/Normalizer/TaskDetailsNormalizer.php +++ b/src/Normalizer/TaskDetailsNormalizer.php @@ -40,6 +40,11 @@ public function normalizeTaskDetails (Envelope $envelope) : array $details["label"] = $task->getMetaData()->label; $details["task"] = $this->serializer->serialize($task->prepareForTaskLog(), JsonEncoder::FORMAT); } + else + { + $details["label"] = get_debug_type($task); + $details["task"] = $this->serializer->serialize($task, JsonEncoder::FORMAT); + } return $details; } diff --git a/src/Schedule/WrappedSchedule.php b/src/Schedule/WrappedSchedule.php index 0f0c368..9a62602 100644 --- a/src/Schedule/WrappedSchedule.php +++ b/src/Schedule/WrappedSchedule.php @@ -42,7 +42,7 @@ public function __construct ( */ public function cron ( string $cronExpression, - Task $task, + object $task, \DateTimeZone|string|null $timezone = null, ) : static { @@ -60,7 +60,7 @@ public function cron ( */ public function every ( string|int|\DateInterval $frequency, - Task $task, + object $task, string|\DateTimeImmutable|null $from = null, string|\DateTimeImmutable $until = new \DateTimeImmutable('3000-01-01'), ) : static diff --git a/src/Task/DispatchAfterRunTask/DispatchAfterRunTask.php b/src/Task/DispatchAfterRunTask/DispatchAfterRunTask.php index 345ae7b..733cae9 100644 --- a/src/Task/DispatchAfterRunTask/DispatchAfterRunTask.php +++ b/src/Task/DispatchAfterRunTask/DispatchAfterRunTask.php @@ -18,12 +18,9 @@ readonly class DispatchAfterRunTask extends Task implements TaskManagerInternalTask { public function __construct ( - public Task $task, + public object $task, public array|string $transportNames = [], - ) - { - parent::__construct(); - } + ) {} /** * @@ -31,8 +28,12 @@ public function __construct ( #[\Override] public function getMetaData () : TaskMetaData { + $label = $this->task instanceof Task + ? $this->task->getMetaData()->label + : get_debug_type($this->task); + return new TaskMetaData( - \sprintf("Redispatch task '%s' after the current run", $this->task->getMetaData()->label), + \sprintf("Redispatch task '%s' after the current run", $label), ); } } diff --git a/src/Task/DispatchAfterRunTask/DispatchAfterRunTaskHandler.php b/src/Task/DispatchAfterRunTask/DispatchAfterRunTaskHandler.php index 21611cd..fbca4e8 100644 --- a/src/Task/DispatchAfterRunTask/DispatchAfterRunTaskHandler.php +++ b/src/Task/DispatchAfterRunTask/DispatchAfterRunTaskHandler.php @@ -38,11 +38,13 @@ public function onDispatchAfterRunTask (DispatchAfterRunTask $task) : void $task->task::class, )); - $stamps = !empty($task->transportNames) - ? [new TransportNamesStamp($task->transportNames)] - : []; + $stamps = []; - $wrappedTask = $task->task->withNewTaskUlid(); - $this->taskManager->enqueue($wrappedTask, $stamps); + if (!empty($task->transportNames)) + { + $stamps[] = new TransportNamesStamp($task->transportNames); + } + + $this->taskManager->enqueue($task->task, $stamps); } } diff --git a/src/Task/Task.php b/src/Task/Task.php index 1fca471..c10ec5a 100644 --- a/src/Task/Task.php +++ b/src/Task/Task.php @@ -2,31 +2,11 @@ namespace Torr\TaskManager\Task; -use Symfony\Component\Uid\UuidV7; - /** * A runnable task */ abstract readonly class Task { - /** - * @deprecated use $taskId instead - * - * @todo remove in 4.0 - * - * @phpstan-ignore-next-line property.deprecated (The uuid integration will be refactored in v4) - */ - public string $ulid; - - /** - */ - public function __construct () - { - $uuid = new UuidV7()->toString(); - /** @phpstan-ignore-next-line property.deprecated (The uuid integration will be refactored in v4) */ - $this->ulid = $uuid; - } - /** * Defines the metadata for this task. * @@ -35,18 +15,6 @@ public function __construct () */ abstract public function getMetaData () : TaskMetaData; - /** - * - */ - public function withNewTaskUlid () : static - { - $uuid = new UuidV7()->toString(); - - return clone($this, [ - "ulid" => $uuid, - ]); - } - /** * Is called before the task is stored in the task log entry. * You can clone the task here to remove / redact / truncate fields in the normalized task. diff --git a/tests/Entity/TaskLogTest.php b/tests/Entity/TaskLogTest.php index 0a6da1b..6eeebb1 100644 --- a/tests/Entity/TaskLogTest.php +++ b/tests/Entity/TaskLogTest.php @@ -3,6 +3,7 @@ namespace Tests\Torr\TaskManager\Entity; use PHPUnit\Framework\TestCase; +use Symfony\Component\Clock\Test\ClockSensitiveTrait; use Torr\TaskManager\Entity\TaskLog; use Torr\TaskManager\Exception\Log\InvalidLogActionException; use Torr\TaskManager\Task\Task; @@ -13,8 +14,9 @@ */ final class TaskLogTest extends TestCase { - // region Helpers + use ClockSensitiveTrait; + // region Helpers private function createTask () : Task { // @phpstan-ignore-next-line 21torr.custom.task.suffix @@ -29,7 +31,7 @@ public function getMetaData () : TaskMetaData private function createLog () : TaskLog { - return new TaskLog($this->createTask()); + return new TaskLog($this->createTask(), "my-uuid"); } // endregion @@ -144,17 +146,20 @@ public function testGetTotalDurationSumsAllRuns () : void { $log = $this->createLog(); + self::mockTime("2026-06-18 12:00:00"); $run1 = $log->createRun(); + self::mockTime("2026-06-18 12:00:05"); $run1->finish(false, null); $run2 = $log->createRun(); + self::mockTime("2026-06-18 12:00:10"); $run2->finish(true, null); $total = $log->getTotalDuration(); self::assertGreaterThan(0, $total); self::assertEqualsWithDelta( - ($run1->duration ?? 0) + ($run2->duration ?? 0), + 10e9, $total, 0.001, ); @@ -182,16 +187,15 @@ public function testTaskDetailsDefaultsToNull () : void public function testTaskIdMatchesTaskUlid () : void { $task = $this->createTask(); - $log = new TaskLog($task); + $log = new TaskLog($task, "my-uuid"); - /** @phpstan-ignore-next-line property.deprecated (The uuid integration will be refactored in v4) */ - self::assertSame($task->ulid, $log->taskId); + self::assertSame("my-uuid", $log->taskId); } public function testTaskClassMatchesTaskClass () : void { $task = $this->createTask(); - $log = new TaskLog($task); + $log = new TaskLog($task, "my-uuid"); self::assertSame($task::class, $log->taskClass); } diff --git a/tests/Entity/TaskRunTest.php b/tests/Entity/TaskRunTest.php index 15c75ab..469a874 100644 --- a/tests/Entity/TaskRunTest.php +++ b/tests/Entity/TaskRunTest.php @@ -29,7 +29,7 @@ public function getMetaData () : TaskMetaData } }; - return new TaskLog($task); + return new TaskLog($task, "my-uuid"); } // endregion diff --git a/tests/Event/RegisterTasksEventTest.php b/tests/Event/RegisterTasksEventTest.php index 46b34ad..266738b 100644 --- a/tests/Event/RegisterTasksEventTest.php +++ b/tests/Event/RegisterTasksEventTest.php @@ -22,10 +22,7 @@ private function createTask (string $label, ?string $group = null) : Task public function __construct ( private string $label, private ?string $group, - ) - { - parent::__construct(); - } + ) {} #[\Override] public function getMetaData () : TaskMetaData diff --git a/tests/Log/LogCleanerTest.php b/tests/Log/LogCleanerTest.php index 6d99792..f684fc9 100644 --- a/tests/Log/LogCleanerTest.php +++ b/tests/Log/LogCleanerTest.php @@ -163,7 +163,7 @@ public function testCutoffEntryOverridesTtlWhenNewer () : void // The TaskLog created below has timeQueued ≈ now (real system clock), // which is newer than TTL purge date, so the cutoff entry should override. $clock = new MockClock(); - $cutoffEntry = new TaskLog($this->createTask()); + $cutoffEntry = new TaskLog($this->createTask(), "my-uuid"); $capturedOldestTimestamp = null; diff --git a/tests/Manager/TaskManagerTest.php b/tests/Manager/TaskManagerTest.php index 159bd8e..6c3dc64 100644 --- a/tests/Manager/TaskManagerTest.php +++ b/tests/Manager/TaskManagerTest.php @@ -28,10 +28,7 @@ private function createTask (?string $uniqueTaskId = null) : Task return new readonly class($uniqueTaskId) extends Task { public function __construct ( private ?string $uniqueTaskId, - ) - { - parent::__construct(); - } + ) {} #[\Override] public function getMetaData () : TaskMetaData @@ -142,7 +139,7 @@ static function (Envelope $envelope) use (&$capturedEnvelope) : Envelope $stamp = new class() implements StampInterface {}; $manager = $this->createManager(["queue" => $this->createListableTransport()], $bus); - $manager->enqueue($this->createTask(null), [$stamp]); + $manager->enqueue($this->createTask(), [$stamp]); self::assertNotNull($capturedEnvelope); self::assertNotEmpty($capturedEnvelope->all($stamp::class)); diff --git a/tests/Normalizer/TaskDetailsNormalizerTest.php b/tests/Normalizer/TaskDetailsNormalizerTest.php index 6eb2b46..8e61e67 100644 --- a/tests/Normalizer/TaskDetailsNormalizerTest.php +++ b/tests/Normalizer/TaskDetailsNormalizerTest.php @@ -28,10 +28,7 @@ private function createTask (string $label = "Test Task") : Task return new readonly class($label) extends Task { public function __construct ( private string $label, - ) - { - parent::__construct(); - } + ) {} #[\Override] public function getMetaData () : TaskMetaData @@ -103,19 +100,6 @@ public function testNormalizeWithoutStampsHasNullTransportAndHandledBy () : void self::assertArrayHasKey("handledBy", $details); self::assertNull($details["handledBy"]); } - - public function testNormalizeNonTaskMessageSkipsLabelAndTask () : void - { - $message = new \stdClass(); - $serializer = $this->createMock(SerializerInterface::class); - $serializer->expects(self::never())->method("serialize"); - - $details = $this->createNormalizer($serializer)->normalizeTaskDetails(new Envelope($message)); - - self::assertArrayNotHasKey("label", $details); - self::assertArrayNotHasKey("task", $details); - } - // endregion // region deserializeTask @@ -123,7 +107,7 @@ public function testNormalizeNonTaskMessageSkipsLabelAndTask () : void public function testDeserializeReturnsNullWhenNoTaskStored () : void { $task = $this->createTask(); - $log = new TaskLog($task); + $log = new TaskLog($task, "my-uuid"); // taskDetails is empty by default $result = $this->createNormalizer()->deserializeTask($log); @@ -134,7 +118,7 @@ public function testDeserializeReturnsNullWhenNoTaskStored () : void public function testDeserializeReturnsTask () : void { $originalTask = $this->createTask(); - $log = new TaskLog($originalTask); + $log = new TaskLog($originalTask, "my-uuid"); $log->setTaskDetails(["task" => '{"ulid":"abc"}']); $serializer = $this->createMock(SerializerInterface::class); @@ -152,7 +136,7 @@ public function testDeserializeReturnsTask () : void public function testDeserializeReturnsNullWhenDeserializedObjectIsNotATask () : void { $task = $this->createTask(); - $log = new TaskLog($task); + $log = new TaskLog($task, "my-uuid"); $log->setTaskDetails(["task" => "{}'"]); $serializer = self::createStub(SerializerInterface::class); @@ -166,7 +150,7 @@ public function testDeserializeReturnsNullWhenDeserializedObjectIsNotATask () : public function testDeserializeLogsErrorAndReturnsNullOnSerializerException () : void { $task = $this->createTask(); - $log = new TaskLog($task); + $log = new TaskLog($task, "my-uuid"); $log->setTaskDetails(["task" => "invalid-json"]); $serializer = self::createStub(SerializerInterface::class); diff --git a/tests/Registry/TaskRegistryTest.php b/tests/Registry/TaskRegistryTest.php index 3d75cf9..f82f622 100644 --- a/tests/Registry/TaskRegistryTest.php +++ b/tests/Registry/TaskRegistryTest.php @@ -23,10 +23,7 @@ private function createTask (string $label, ?string $group = null) : Task public function __construct ( private string $label, private ?string $group, - ) - { - parent::__construct(); - } + ) {} #[\Override] public function getMetaData () : TaskMetaData diff --git a/tests/Task/TaskTest.php b/tests/Task/TaskTest.php deleted file mode 100644 index 22ab5da..0000000 --- a/tests/Task/TaskTest.php +++ /dev/null @@ -1,37 +0,0 @@ -ulid; - $newTask = $task->withNewTaskUlid(); - /** @phpstan-ignore-next-line property.deprecated (The uuid integration will be refactored in v4) */ - self::assertNotSame($initialUlid, $newTask->ulid, "Task ULID should change on PHP 8.4"); - } -}