From 8b0fd98ac1691ef89c80d5808bec4a635c156709 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 16 Jun 2026 16:41:59 +0200 Subject: [PATCH 1/5] Bump to Symfony 8.1+ --- CHANGELOG.md | 6 ++++++ composer.json | 36 ++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21a5f1d..ea11440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +3.4.0 (unreleased) +===== + +* (improvement) Require Symfony 8.1+ + + 3.3.2 ===== diff --git a/composer.json b/composer.json index 64e09f8..a975ad1 100644 --- a/composer.json +++ b/composer.json @@ -13,33 +13,33 @@ "require": { "php": ">= 8.5", "21torr/bundle-helpers": "^2.3.2", - "21torr/cli": "^1.2.3", - "21torr/hosting": "^4.1.1", - "21torr/snail": "^1.0.1", + "21torr/cli": "^1.3.0", + "21torr/hosting": "^4.1.2", + "21torr/snail": "^1.0.2", "doctrine/collections": "^2.6", "doctrine/doctrine-bundle": "^3.2.2", "doctrine/orm": "^3.6", "dragonmantank/cron-expression": "^3.6", "psr/log": "^3.0", - "symfony/clock": "^7.4 || ^8.0", - "symfony/config": "^7.4 || ^8.0", - "symfony/console": "^7.4 || ^8.0", - "symfony/dependency-injection": "^7.4 || ^8.0", - "symfony/event-dispatcher": "^7.4 || ^8.0", - "symfony/http-kernel": "^7.4 || ^8.0", - "symfony/lock": "^7.4 || ^8.0", - "symfony/messenger": "^7.4 || ^8.0", - "symfony/scheduler": "^7.4 || ^8.0", - "symfony/serializer": "^7.4 || ^8.0", - "symfony/string": "^7.4 || ^8.0", - "symfony/uid": "^7.4 || ^8.0" + "symfony/clock": "^8.1", + "symfony/config": "^8.1", + "symfony/console": "^8.1", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/http-kernel": "^8.1", + "symfony/lock": "^8.1", + "symfony/messenger": "^8.1", + "symfony/scheduler": "^8.1", + "symfony/serializer": "^8.1", + "symfony/string": "^8.1", + "symfony/uid": "^8.1" }, "require-dev": { - "21torr/janus": "^2.0.3", + "21torr/janus": "^2.1.1", "bamarni/composer-bin-plugin": "^1.9.1", - "phpunit/phpunit": "^13.0.5", + "phpunit/phpunit": "^13.2.1", "roave/security-advisories": "dev-latest", - "symfony/translation-contracts": "^3.6.1" + "symfony/translation-contracts": "^3.7.0" }, "autoload": { "psr-4": { From 700c3d1c7a8fae286f21f3b533f5af6680286f7e Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 16 Jun 2026 16:42:41 +0200 Subject: [PATCH 2/5] Use Symfonys native message deduplication --- CHANGELOG.md | 1 + src/Manager/TaskManager.php | 79 ++++--------------------------- tests/Manager/TaskManagerTest.php | 48 ++++--------------- 3 files changed, 19 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea11440..7d6d9d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ===== * (improvement) Require Symfony 8.1+ +* (feature) Use [Symfonys native message deduplication](https://symfony.com/doc/current/messenger.html#message-deduplication) 3.3.2 diff --git a/src/Manager/TaskManager.php b/src/Manager/TaskManager.php index b1be0f3..59e2023 100644 --- a/src/Manager/TaskManager.php +++ b/src/Manager/TaskManager.php @@ -4,11 +4,8 @@ use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Stamp\DeduplicateStamp; use Symfony\Component\Messenger\Stamp\StampInterface; -use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; -use Symfony\Component\Messenger\Transport\Sync\SyncTransport; -use Symfony\Component\Scheduler\Messenger\SchedulerTransport; -use Torr\TaskManager\Exception\Transport\InvalidMessageTransportException; use Torr\TaskManager\Task\Task; use Torr\TaskManager\Transport\TransportsHelper; @@ -32,78 +29,18 @@ public function __construct ( */ public function enqueue (Task $task, array $stamps = []) : bool { - // if we find a message with the same unique task id, we don't queue it again - if ($this->isTaskWithSameTaskIdAlreadyQueued($task->getMetaData()->uniqueTaskId)) - { - return false; - } - - $envelope = new Envelope($task, $stamps); - - $this->messageBus->dispatch($envelope); - - return true; - } - - /** - * Finds a queued message with the given job id - */ - private function isTaskWithSameTaskIdAlreadyQueued (?string $uniqueTaskId) : bool - { - // no task id, so this task is not deduplicated. No need to check anything, just enqueue it. - if (null === $uniqueTaskId) - { - return false; - } - - foreach ($this->transportsHelper->getOrderedQueueNames() as $queueName) - { - foreach ($this->fetchTasksInQueue($queueName) as $envelope) - { - $message = $envelope->getMessage(); - - if ($message instanceof Task && $message->getMetaData()->uniqueTaskId === $uniqueTaskId) - { - return true; - } - } - } - - return false; - } - - /** - * Fetches all tasks for the given priority - * - * @return iterable - * - * @api - */ - public function fetchTasksInQueue (string $queueName) : iterable - { - $receiver = $this->transportsHelper->getTransport($queueName); - - // ignore schedulers - if ($receiver instanceof SchedulerTransport) - { - return []; - } + $uniqueTaskId = $task->getMetaData()->uniqueTaskId; - // skip, as sync transports can't queue messages like regular transports - if ($receiver instanceof SyncTransport) + if (null !== $uniqueTaskId) { - return []; + $stamps[] = new DeduplicateStamp($uniqueTaskId); } - if (!$receiver instanceof ListableReceiverInterface) - { - throw new InvalidMessageTransportException(\sprintf( - "Transport for queue '%s' must implement ListableReceiverInterface", - $queueName, - )); - } + $this->messageBus->dispatch( + new Envelope($task, $stamps), + ); - return $receiver->all(); + return true; } /** diff --git a/tests/Manager/TaskManagerTest.php b/tests/Manager/TaskManagerTest.php index fba744c..f165094 100644 --- a/tests/Manager/TaskManagerTest.php +++ b/tests/Manager/TaskManagerTest.php @@ -6,6 +6,7 @@ use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Stamp\DeduplicateStamp; use Symfony\Component\Messenger\Stamp\StampInterface; use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; use Symfony\Component\Messenger\Transport\TransportInterface; @@ -112,46 +113,17 @@ public function testEnqueueReturnsTrueWhenNoConflict () : void public function testEnqueueReturnsFalseWhenDuplicateInQueue () : void { - $existingTask = $this->createTask("test.task"); - $transport = $this->createListableTransport([new Envelope($existingTask)]); + $bus = self::createMock(MessageBusInterface::class); - $bus = $this->createMock(MessageBusInterface::class); - $bus->expects(self::never())->method("dispatch"); - - $manager = $this->createManager(["queue" => $transport], $bus); - $newTask = $this->createTask("test.task"); - - self::assertFalse($manager->enqueue($newTask)); - } - - public function testEnqueueScansMultipleQueuesForDuplicate () : void - { - $existingTask = $this->createTask("test.task"); - $emptyTransport = $this->createListableTransport([]); - $fullTransport = $this->createListableTransport([new Envelope($existingTask)]); - - $bus = $this->createMock(MessageBusInterface::class); - $bus->expects(self::never())->method("dispatch"); + $bus->expects(self::once()) + ->method("dispatch") + ->with(self::callback( + static fn (Envelope $envelope) : bool => "unique.key" === $envelope->last(DeduplicateStamp::class)->getKey()->__toString(), + )) + ->willReturnArgument(0); - $manager = $this->createManager([ - "queue_a" => $emptyTransport, - "queue_b" => $fullTransport, - ], $bus); - - self::assertFalse($manager->enqueue($this->createTask("test.task"))); - } - - public function testEnqueueDispatchesWhenDifferentUniqueTaskIdInQueue () : void - { - $otherTask = $this->createTask("other.task"); - $transport = $this->createListableTransport([new Envelope($otherTask)]); - - $bus = $this->createMock(MessageBusInterface::class); - $bus->expects(self::once())->method("dispatch")->willReturnArgument(0); - - $manager = $this->createManager(["queue" => $transport], $bus); - - self::assertTrue($manager->enqueue($this->createTask("test.task"))); + $manager = $this->createManager(["queue" => $this->createListableTransport()], $bus); + $manager->enqueue($this->createTask("unique.key")); } public function testEnqueueForwardsStampsToDispatchedEnvelope () : void From 41af415caaf67fba06b1d5acf62572f4c049aef6 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 16 Jun 2026 16:59:49 +0200 Subject: [PATCH 3/5] Fix tests --- tests/Entity/TaskRunTest.php | 8 ++++++-- tests/Manager/TaskManagerTest.php | 5 ++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/Entity/TaskRunTest.php b/tests/Entity/TaskRunTest.php index a9d1aef..15c75ab 100644 --- a/tests/Entity/TaskRunTest.php +++ b/tests/Entity/TaskRunTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Symfony\Component\Clock\Test\ClockSensitiveTrait; use Torr\TaskManager\Entity\TaskLog; use Torr\TaskManager\Entity\TaskRun; use Torr\TaskManager\Task\Task; @@ -14,8 +15,9 @@ */ final class TaskRunTest extends TestCase { - // region Helpers + use ClockSensitiveTrait; + // region Helpers private function createLog () : TaskLog { // @phpstan-ignore-next-line 21torr.custom.task.suffix @@ -45,14 +47,16 @@ public function testInitialStateIsUnfinished () : void public function testFinishMarksAsSuccessful () : void { + self::mockTime("2026-06-16 12:00:00"); $run = new TaskRun($this->createLog()); + self::mockTime("2026-06-16 12:00:05"); $run->finish(true, "some output"); self::assertTrue($run->isFinished); self::assertTrue($run->success); self::assertSame("some output", $run->output); self::assertTrue($run->finishedProperly); - self::assertGreaterThan(0, $run->duration); + self::assertEquals(5e9, $run->duration); } public function testFinishMarksAsFailure () : void diff --git a/tests/Manager/TaskManagerTest.php b/tests/Manager/TaskManagerTest.php index f165094..b29c292 100644 --- a/tests/Manager/TaskManagerTest.php +++ b/tests/Manager/TaskManagerTest.php @@ -22,7 +22,6 @@ final class TaskManagerTest extends TestCase { // region Helpers - private function createTask (?string $uniqueTaskId = null) : Task { // @phpstan-ignore-next-line 21torr.custom.task.suffix @@ -47,7 +46,7 @@ public function getMetaData () : TaskMetaData * * @param Envelope[] $envelopes */ - private function createListableTransport (array $envelopes = []) : TransportInterface&ListableReceiverInterface + private function createListableTransport (array $envelopes = []) : ListableReceiverInterface|TransportInterface { return new class($envelopes) implements TransportInterface, ListableReceiverInterface { /** @param Envelope[] $envelopes */ @@ -118,7 +117,7 @@ public function testEnqueueReturnsFalseWhenDuplicateInQueue () : void $bus->expects(self::once()) ->method("dispatch") ->with(self::callback( - static fn (Envelope $envelope) : bool => "unique.key" === $envelope->last(DeduplicateStamp::class)->getKey()->__toString(), + static fn (Envelope $envelope) : bool => "unique.key" === $envelope->last(DeduplicateStamp::class)?->getKey()->__toString(), )) ->willReturnArgument(0); From 5ddddba6708541de7ddf8b21cc7215b77f365aec Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 16 Jun 2026 17:53:34 +0200 Subject: [PATCH 4/5] Use UUIDv7 instead of ULIDs --- CHANGELOG.md | 2 ++ UPGRADE.md | 1 + src/Entity/TaskLog.php | 4 ++-- src/Model/TaskLogModel.php | 2 +- src/Task/Task.php | 19 ++++++++++++++++--- tests/Entity/TaskLogTest.php | 2 +- tests/Task/TaskTest.php | 4 ++-- 7 files changed, 25 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d6d9d3..e38e107 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ * (improvement) Require Symfony 8.1+ * (feature) Use [Symfonys native message deduplication](https://symfony.com/doc/current/messenger.html#message-deduplication) +* (improvement) Use UUIDv7 instead of ULID for task ids +* (deprecation) Deprecate `TaskLog::$ulid`, use `TaskLog::$uuid` instead. 3.3.2 diff --git a/UPGRADE.md b/UPGRADE.md index 2a6e347..92374b3 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -2,6 +2,7 @@ ========== * `TaskLog::getTaskObject()` was removed. Use `TaskDetailsNormalizer::deserializeTask($log)` instead. +* `TaskLog::$ulid` was removed, use `TaskLog::$uuid` instead. 1.x to 2.0 diff --git a/src/Entity/TaskLog.php b/src/Entity/TaskLog.php index 340c7b2..a0857d2 100644 --- a/src/Entity/TaskLog.php +++ b/src/Entity/TaskLog.php @@ -36,7 +36,7 @@ class TaskLog public private(set) ?int $id = null; /** - * ULIDs have only 22 characters, but just to be sure + * UUIDv7s have only 36 characters, but just to be sure we use 50 characters */ #[ORM\Column(type: Types::STRING, length: 50, unique: true)] public private(set) string $taskId; @@ -73,7 +73,7 @@ public function __construct ( ) { $this->taskClass = $task::class; - $this->taskId = $task->ulid; + $this->taskId = $task->uuid; $this->runs = new ArrayCollection(); $this->timeQueued = now(); } diff --git a/src/Model/TaskLogModel.php b/src/Model/TaskLogModel.php index 64de570..14da2c3 100644 --- a/src/Model/TaskLogModel.php +++ b/src/Model/TaskLogModel.php @@ -51,7 +51,7 @@ public function findByTaskId (string $taskId) : ?TaskLog */ public function getLogForTask (Task $task) : TaskLog { - $log = $this->findByTaskId($task->ulid); + $log = $this->findByTaskId($task->uuid); if (null !== $log) { diff --git a/src/Task/Task.php b/src/Task/Task.php index 46aa8d8..07c756e 100644 --- a/src/Task/Task.php +++ b/src/Task/Task.php @@ -2,20 +2,30 @@ namespace Torr\TaskManager\Task; -use Symfony\Component\Uid\Ulid; +use Symfony\Component\Uid\UuidV7; /** * A runnable task */ abstract readonly class Task { + public string $uuid; + + /** + * @deprecated use $taskId instead + * + * @todo remove in 4.0 + */ public string $ulid; /** */ public function __construct () { - $this->ulid = new Ulid()->toBase58(); + $uuid = new UuidV7()->toString(); + $this->uuid = $uuid; + /** @phpstan-ignore-next-line property.deprecated (We still need to support the deprecated property) */ + $this->ulid = $uuid; } /** @@ -31,8 +41,11 @@ abstract public function getMetaData () : TaskMetaData; */ public function withNewTaskUlid () : static { + $uuid = new UuidV7()->toString(); + return clone($this, [ - "ulid" => new Ulid()->toBase58(), + "uuid" => $uuid, + "ulid" => $uuid, ]); } } diff --git a/tests/Entity/TaskLogTest.php b/tests/Entity/TaskLogTest.php index 76eda6d..212beba 100644 --- a/tests/Entity/TaskLogTest.php +++ b/tests/Entity/TaskLogTest.php @@ -184,7 +184,7 @@ public function testTaskIdMatchesTaskUlid () : void $task = $this->createTask(); $log = new TaskLog($task); - self::assertSame($task->ulid, $log->taskId); + self::assertSame($task->uuid, $log->taskId); } public function testTaskClassMatchesTaskClass () : void diff --git a/tests/Task/TaskTest.php b/tests/Task/TaskTest.php index 276dc0b..32a3533 100644 --- a/tests/Task/TaskTest.php +++ b/tests/Task/TaskTest.php @@ -28,8 +28,8 @@ public function getMetaData () : TaskMetaData } }; - $initialUlid = $task->ulid; + $initialUlid = $task->uuid; $newTask = $task->withNewTaskUlid(); - self::assertNotSame($initialUlid, $newTask->ulid, "Task ULID should change on PHP 8.4"); + self::assertNotSame($initialUlid, $newTask->uuid, "Task ULID should change on PHP 8.4"); } } From 7eefcdbc581e1a999157c8866815ec6e3546d725 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 16 Jun 2026 17:55:41 +0200 Subject: [PATCH 5/5] Add `Task::prepareForTaskLog()` --- CHANGELOG.md | 1 + src/Normalizer/TaskDetailsNormalizer.php | 2 +- src/Task/Task.php | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e38e107..f80bf6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * (feature) Use [Symfonys native message deduplication](https://symfony.com/doc/current/messenger.html#message-deduplication) * (improvement) Use UUIDv7 instead of ULID for task ids * (deprecation) Deprecate `TaskLog::$ulid`, use `TaskLog::$uuid` instead. +* (feature) Add `Task::prepareForTaskLog()` to be able to modify the payload before storing it in the database. 3.3.2 diff --git a/src/Normalizer/TaskDetailsNormalizer.php b/src/Normalizer/TaskDetailsNormalizer.php index d61ebc3..4d3466f 100644 --- a/src/Normalizer/TaskDetailsNormalizer.php +++ b/src/Normalizer/TaskDetailsNormalizer.php @@ -38,7 +38,7 @@ public function normalizeTaskDetails (Envelope $envelope) : array if ($task instanceof Task) { $details["label"] = $task->getMetaData()->label; - $details["task"] = $this->serializer->serialize($task, JsonEncoder::FORMAT); + $details["task"] = $this->serializer->serialize($task->prepareForTaskLog(), JsonEncoder::FORMAT); } return $details; diff --git a/src/Task/Task.php b/src/Task/Task.php index 07c756e..71913ba 100644 --- a/src/Task/Task.php +++ b/src/Task/Task.php @@ -48,4 +48,13 @@ public function withNewTaskUlid () : static "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. + */ + public function prepareForTaskLog () : static + { + return $this; + } }