diff --git a/databox/api/migrations/Version20241028171322.php b/databox/api/migrations/Version20241028171322.php new file mode 100644 index 000000000..caee96234 --- /dev/null +++ b/databox/api/migrations/Version20241028171322.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE job_state ADD number SMALLINT NOT NULL DEFAULT 0'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE job_state DROP number'); + } +} diff --git a/databox/api/src/Controller/Admin/JobStateCrudController.php b/databox/api/src/Controller/Admin/JobStateCrudController.php index 7d438798b..670071ddf 100644 --- a/databox/api/src/Controller/Admin/JobStateCrudController.php +++ b/databox/api/src/Controller/Admin/JobStateCrudController.php @@ -5,6 +5,7 @@ use Alchemy\AdminBundle\Controller\AbstractAdminCrudController; use Alchemy\AdminBundle\Field\ArrayObjectField; use Alchemy\AdminBundle\Field\IdField; +use Alchemy\AdminBundle\Filter\AssociationIdentifierFilter; use Alchemy\Workflow\Doctrine\Entity\JobState; use Alchemy\Workflow\State\JobState as JobStateModel; use Alchemy\Workflow\State\JobState as ModelJobState; @@ -18,9 +19,12 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; +use EasyCorp\Bundle\EasyAdminBundle\Field\NumberField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; use EasyCorp\Bundle\EasyAdminBundle\Filter\ChoiceFilter; use EasyCorp\Bundle\EasyAdminBundle\Filter\DateTimeFilter; +use EasyCorp\Bundle\EasyAdminBundle\Filter\NumericFilter; +use EasyCorp\Bundle\EasyAdminBundle\Filter\TextFilter; use Symfony\Component\HttpFoundation\RedirectResponse; class JobStateCrudController extends AbstractAdminCrudController @@ -89,6 +93,9 @@ public function configureCrud(Crud $crud): Crud return parent::configureCrud($crud) ->setEntityLabelInSingular('Job State') ->setEntityLabelInPlural('Job States') + ->setDefaultSort([ + 'triggeredAt' => 'DESC', + ]) ->setSearchFields(['id']); } @@ -102,8 +109,10 @@ public function configureFilters(Filters $filters): Filters 'SKIPPED' => ModelJobState::STATUS_SKIPPED, 'RUNNING' => ModelJobState::STATUS_RUNNING, ])) + ->add(AssociationIdentifierFilter::new('workflow')) ->add(DateTimeFilter::new('startedAt')) ->add(DateTimeFilter::new('endedAt')) + ->add(NumericFilter::new('number')) ; } @@ -114,6 +123,7 @@ public function configureFields(string $pageName): iterable yield TextField::new('jobId', 'Job ID'); yield DateTimeField::new('triggeredAt', 'Triggered At'); yield DateTimeField::new('startedAt', 'Started At'); + yield NumberField::new('number'); yield ChoiceField::new('status', 'Status') ->setChoices([ 'TRIGGERED' => ModelJobState::STATUS_TRIGGERED, diff --git a/databox/api/src/Controller/Admin/WorkflowStateCrudController.php b/databox/api/src/Controller/Admin/WorkflowStateCrudController.php index 39ad16a85..844c90fd6 100644 --- a/databox/api/src/Controller/Admin/WorkflowStateCrudController.php +++ b/databox/api/src/Controller/Admin/WorkflowStateCrudController.php @@ -47,7 +47,7 @@ public function configureActions(Actions $actions): Actions { $viewWorkflow = Action::new('viewWorkflow', 'View', 'fa fa-eye') ->setHtmlAttributes(['target' => '_blank']) - ->linkToUrl(fn (WorkflowState $entity): string => sprintf('%s/workflows/%s', $this->databoxClientBaseUrl, $entity->getId())); + ->linkToUrl(fn (WorkflowState $entity): string => sprintf('%s/?_m=%s', $this->databoxClientBaseUrl, urlencode(sprintf('/workflows/%s', $entity->getId())))); $cancel = Action::new('cancelWorkflow', 'Cancel Workflow', 'fas fa-ban') ->displayIf(fn (WorkflowState $entity) => ModelWorkflowState::STATUS_STARTED === $entity->getStatus()) @@ -66,6 +66,9 @@ public function configureCrud(Crud $crud): Crud return parent::configureCrud($crud) ->setEntityLabelInSingular('Workflow State') ->setEntityLabelInPlural('Workflow States') + ->setDefaultSort([ + 'startedAt' => 'DESC', + ]) ->setSearchFields(['id']); } diff --git a/databox/api/src/Entity/Core/Attribute.php b/databox/api/src/Entity/Core/Attribute.php index e3aaca32c..29b5acc07 100644 --- a/databox/api/src/Entity/Core/Attribute.php +++ b/databox/api/src/Entity/Core/Attribute.php @@ -63,10 +63,10 @@ class Attribute extends AbstractBaseAttribute implements ESIndexableDeleteDepend final public const string GROUP_READ = 'attr:read'; final public const string GROUP_LIST = 'attr:index'; - final public const ORIGIN_MACHINE = 0; - final public const ORIGIN_HUMAN = 1; - final public const ORIGIN_FALLBACK = 2; - final public const ORIGIN_INITIAL = 3; + final public const int ORIGIN_MACHINE = 0; + final public const int ORIGIN_HUMAN = 1; + final public const int ORIGIN_FALLBACK = 2; + final public const int ORIGIN_INITIAL = 3; final public const ORIGIN_LABELS = [ self::ORIGIN_MACHINE => 'machine', @@ -75,9 +75,9 @@ class Attribute extends AbstractBaseAttribute implements ESIndexableDeleteDepend self::ORIGIN_INITIAL => 'initial', ]; - final public const STATUS_VALID = 0; - final public const STATUS_REVIEW_PENDING = 1; - final public const STATUS_DECLINED = 2; + final public const int STATUS_VALID = 0; + final public const int STATUS_REVIEW_PENDING = 1; + final public const int STATUS_DECLINED = 2; final public const STATUS_LABELS = [ self::STATUS_VALID => 'valid', diff --git a/databox/api/src/Entity/Core/AttributeEntity.php b/databox/api/src/Entity/Core/AttributeEntity.php index a2d7fe91d..2b4ab60cf 100644 --- a/databox/api/src/Entity/Core/AttributeEntity.php +++ b/databox/api/src/Entity/Core/AttributeEntity.php @@ -55,7 +55,7 @@ class AttributeEntity extends AbstractUuidEntity use CreatedAtTrait; use UpdatedAtTrait; use WorkspaceTrait; - public const TYPE_LENGTH = 100; + public const int TYPE_LENGTH = 100; final public const string GROUP_READ = 'attr-entity:read'; final public const string GROUP_LIST = 'attr-entity:index'; diff --git a/databox/api/src/Entity/Core/RenditionRule.php b/databox/api/src/Entity/Core/RenditionRule.php index c190eb274..1148bb281 100644 --- a/databox/api/src/Entity/Core/RenditionRule.php +++ b/databox/api/src/Entity/Core/RenditionRule.php @@ -60,10 +60,10 @@ class RenditionRule extends AbstractUuidEntity final public const string GROUP_READ = 'rendrule:read'; final public const string GROUP_LIST = 'rendrule:index'; - final public const TYPE_USER = 0; - final public const TYPE_GROUP = 1; - final public const TYPE_WORKSPACE = 0; - final public const TYPE_COLLECTION = 1; + final public const int TYPE_USER = 0; + final public const int TYPE_GROUP = 1; + final public const int TYPE_WORKSPACE = 0; + final public const int TYPE_COLLECTION = 1; final public const OBJECT_CLASSES = [ self::TYPE_WORKSPACE => Workspace::class, diff --git a/databox/api/src/Entity/Core/TagFilterRule.php b/databox/api/src/Entity/Core/TagFilterRule.php index 6eb21469f..1c09968dc 100644 --- a/databox/api/src/Entity/Core/TagFilterRule.php +++ b/databox/api/src/Entity/Core/TagFilterRule.php @@ -56,10 +56,10 @@ class TagFilterRule extends AbstractUuidEntity final public const string GROUP_READ = 'tfr:read'; final public const string GROUP_LIST = 'tfr:index'; - final public const TYPE_USER = 0; - final public const TYPE_GROUP = 1; - final public const TYPE_WORKSPACE = 0; - final public const TYPE_COLLECTION = 1; + final public const int TYPE_USER = 0; + final public const int TYPE_GROUP = 1; + final public const int TYPE_WORKSPACE = 0; + final public const int TYPE_COLLECTION = 1; final public const OBJECT_CLASSES = [ self::TYPE_WORKSPACE => Workspace::class, diff --git a/databox/api/src/Entity/Core/WorkspaceItemPrivacyInterface.php b/databox/api/src/Entity/Core/WorkspaceItemPrivacyInterface.php index 4080a357d..1598cf4d2 100644 --- a/databox/api/src/Entity/Core/WorkspaceItemPrivacyInterface.php +++ b/databox/api/src/Entity/Core/WorkspaceItemPrivacyInterface.php @@ -7,22 +7,22 @@ interface WorkspaceItemPrivacyInterface { // Completely secret, only owner or granted users can view the item - public const SECRET = 0; + public const int SECRET = 0; // Item is listed for users allowed in the workspace but content is not accessible - public const PRIVATE_IN_WORKSPACE = 1; + public const int PRIVATE_IN_WORKSPACE = 1; // Open to users allowed in the workspace - public const PUBLIC_IN_WORKSPACE = 2; + public const int PUBLIC_IN_WORKSPACE = 2; // Item is listed to every user, but content is not accessible - public const PRIVATE = 3; + public const int PRIVATE = 3; // Public to every authenticated users - public const PUBLIC_FOR_USERS = 4; + public const int PUBLIC_FOR_USERS = 4; // Public to everyone - public const PUBLIC = 5; + public const int PUBLIC = 5; public const KEYS = [ WorkspaceItemPrivacyInterface::SECRET => 'secret', diff --git a/databox/api/src/Security/RenditionPermissionManager.php b/databox/api/src/Security/RenditionPermissionManager.php index a479820ea..90157d7c4 100644 --- a/databox/api/src/Security/RenditionPermissionManager.php +++ b/databox/api/src/Security/RenditionPermissionManager.php @@ -13,7 +13,7 @@ class RenditionPermissionManager { - private const IS_EMPTY = 0; + private const int IS_EMPTY = 0; private const string ANONYMOUS = '~'; /** diff --git a/lib/js/visual-workflow/src/Job/JobDetail.tsx b/lib/js/visual-workflow/src/Job/JobDetail.tsx index c437432b8..c9c3cb0e6 100644 --- a/lib/js/visual-workflow/src/Job/JobDetail.tsx +++ b/lib/js/visual-workflow/src/Job/JobDetail.tsx @@ -21,6 +21,7 @@ export default function JobDetail({ const values: Cells = [ [`Status`, undefined !== job.status ? jobStatuses[job.status] : '-'], [`Duration`, job.duration ?? '-'], + [`#`, (job.number ?? '-').toString()], [`Started At`, ], ]; @@ -33,7 +34,7 @@ export default function JobDetail({ loading={rerunning} onClick={() => { setRerunning(true); - job.onRerun!(job.id).finally(() => { + job.onRerun!(job.jobId).finally(() => { setRerunning(false); }); }} diff --git a/lib/js/visual-workflow/src/VisualWorkflow.tsx b/lib/js/visual-workflow/src/VisualWorkflow.tsx index 620947e52..a3a4ebb83 100644 --- a/lib/js/visual-workflow/src/VisualWorkflow.tsx +++ b/lib/js/visual-workflow/src/VisualWorkflow.tsx @@ -54,11 +54,11 @@ export default function VisualWorkflow({ ...j, onRerun: onRerunJob, }; - jobIndex[j.id] = nodeData; + jobIndex[j.jobId] = nodeData; nodes.push({ type: 'jobNode', - id: j.id, + id: j.jobId, position: { x: stageXPadding * (1 + sIndex * 2) + nodeWith * sIndex, y: nodeYPadding * (1 + jIndex * 2) + nodeHeight * jIndex, @@ -77,9 +77,9 @@ export default function VisualWorkflow({ jobIndex[n].isDependency = true; edges.push({ - id: `${j.id}-${n}`, + id: `${j.jobId}-${n}`, source: n, - target: j.id, + target: j.jobId, className: 'job-edge', }) }) diff --git a/lib/js/visual-workflow/src/stories/workflowSample.ts b/lib/js/visual-workflow/src/stories/workflowSample.ts index 60aa4dcda..8f07c1c19 100644 --- a/lib/js/visual-workflow/src/stories/workflowSample.ts +++ b/lib/js/visual-workflow/src/stories/workflowSample.ts @@ -10,7 +10,8 @@ export const workflowSample: Workflow = { jobs: [ { name: 'init', - id: 'init', + stateId: 'init-0', + jobId: 'init', status: JobStatus.Success, duration: '0.053s', triggeredAt: '2023-05-24T10:22:25.495639+00:00', @@ -30,7 +31,7 @@ export const workflowSample: Workflow = { }, { name: 'skipped', - id: 'skipped', + jobId: 'skipped', status: JobStatus.Skipped, }, ], diff --git a/lib/js/visual-workflow/src/types.ts b/lib/js/visual-workflow/src/types.ts index 64017081c..6fee4bc41 100644 --- a/lib/js/visual-workflow/src/types.ts +++ b/lib/js/visual-workflow/src/types.ts @@ -27,7 +27,9 @@ export type Inputs = Record; export type Outputs = Record; export type Job = { - id: string; + stateId?: string; + jobId: string; + number?: number; name: string; status?: JobStatus | undefined; errors?: JobError[] | undefined; diff --git a/lib/php/workflow-bundle/Resources/config/doctrine/JobState.orm.yml b/lib/php/workflow-bundle/Resources/config/doctrine/JobState.orm.yml index 59e624858..8e9b16386 100644 --- a/lib/php/workflow-bundle/Resources/config/doctrine/JobState.orm.yml +++ b/lib/php/workflow-bundle/Resources/config/doctrine/JobState.orm.yml @@ -37,3 +37,7 @@ Alchemy\Workflow\Doctrine\Entity\JobState: name: status type: smallint nullable: false + number: + name: status + type: smallint + nullable: false diff --git a/lib/php/workflow/composer.json b/lib/php/workflow/composer.json index c78c90f8c..bb8b1bd57 100644 --- a/lib/php/workflow/composer.json +++ b/lib/php/workflow/composer.json @@ -27,16 +27,17 @@ "require": { "php": "^8.3", "ext-json": "*", - "symfony/console": "^5.4 || ^6", - "symfony/yaml": "^6.2", + "symfony/console": "^5.4 || ^6 || ^7", + "symfony/yaml": "^6.2 || ^7", "ramsey/uuid": "^4.2", - "symfony/process": "^6.3", - "symfony/expression-language": "^5.2 || ^6.2", - "symfony/property-access": "^5.2 || ^6.2" + "symfony/process": "^6.3 || ^7", + "symfony/expression-language": "^5.2 || ^6.2 || ^7", + "symfony/property-access": "^5.2 || ^6.2 || ^7" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.17", "phpunit/phpunit": "^9.5", + "symfony/http-kernel": "^6 || ^7", "symfony/var-dumper": "^5.4", "doctrine/orm": "^2.14", "symfony/messenger": "^6.4", diff --git a/lib/php/workflow/composer.lock b/lib/php/workflow/composer.lock index ed001d79b..1e244d5e2 100644 --- a/lib/php/workflow/composer.lock +++ b/lib/php/workflow/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9180388da9ea6ef37dac9183a749b6e8", + "content-hash": "caa1343f3850fb8c05b2e114b1230026", "packages": [ { "name": "brick/math", @@ -401,16 +401,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.12", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "a463451b7f6ac4a47b98dbfc78ec2d3560c759d8" + "reference": "8079a3006f53805e7771d086b62428b7cac481dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/a463451b7f6ac4a47b98dbfc78ec2d3560c759d8", - "reference": "a463451b7f6ac4a47b98dbfc78ec2d3560c759d8", + "url": "https://api.github.com/repos/symfony/cache/zipball/8079a3006f53805e7771d086b62428b7cac481dd", + "reference": "8079a3006f53805e7771d086b62428b7cac481dd", "shasum": "" }, "require": { @@ -477,7 +477,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.12" + "source": "https://github.com/symfony/cache/tree/v6.4.13" }, "funding": [ { @@ -493,7 +493,7 @@ "type": "tidelift" } ], - "time": "2024-09-16T16:01:33+00:00" + "time": "2024-10-25T15:39:47+00:00" }, { "name": "symfony/cache-contracts", @@ -573,47 +573,46 @@ }, { "name": "symfony/console", - "version": "v6.4.12", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765" + "reference": "bb5192af6edc797cbab5c8e8ecfea2fe5f421e57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/72d080eb9edf80e36c19be61f72c98ed8273b765", - "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765", + "url": "https://api.github.com/repos/symfony/console/zipball/bb5192af6edc797cbab5c8e8ecfea2fe5f421e57", + "reference": "bb5192af6edc797cbab5c8e8ecfea2fe5f421e57", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/string": "^6.4|^7.0" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -647,7 +646,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.12" + "source": "https://github.com/symfony/console/tree/v7.1.6" }, "funding": [ { @@ -663,7 +662,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:15:52+00:00" + "time": "2024-10-09T08:46:59+00:00" }, { "name": "symfony/deprecation-contracts", @@ -734,21 +733,21 @@ }, { "name": "symfony/expression-language", - "version": "v6.4.11", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", - "reference": "564e109c40d3637053c942a29a58e9434592a8bf" + "reference": "c3a1224bc144b36cd79149b42c1aecd5f81395a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/expression-language/zipball/564e109c40d3637053c942a29a58e9434592a8bf", - "reference": "564e109c40d3637053c942a29a58e9434592a8bf", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/c3a1224bc144b36cd79149b42c1aecd5f81395a5", + "reference": "c3a1224bc144b36cd79149b42c1aecd5f81395a5", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/cache": "^5.4|^6.0|^7.0", + "php": ">=8.2", + "symfony/cache": "^6.4|^7.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3" }, @@ -778,7 +777,7 @@ "description": "Provides an engine that can compile and evaluate expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/expression-language/tree/v6.4.11" + "source": "https://github.com/symfony/expression-language/tree/v7.1.6" }, "funding": [ { @@ -794,7 +793,7 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:55:28+00:00" + "time": "2024-10-09T08:46:59+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1116,20 +1115,20 @@ }, { "name": "symfony/process", - "version": "v6.4.12", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3" + "reference": "6aaa189ddb4ff6b5de8fa3210f2fb42c87b4d12e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/3f94e5f13ff58df371a7ead461b6e8068900fbb3", - "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3", + "url": "https://api.github.com/repos/symfony/process/zipball/6aaa189ddb4ff6b5de8fa3210f2fb42c87b4d12e", + "reference": "6aaa189ddb4ff6b5de8fa3210f2fb42c87b4d12e", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -1157,7 +1156,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.12" + "source": "https://github.com/symfony/process/tree/v7.1.6" }, "funding": [ { @@ -1173,29 +1172,28 @@ "type": "tidelift" } ], - "time": "2024-09-17T12:47:12+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/property-access", - "version": "v6.4.11", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "866f6cd84f2094cbc6f66ce9752faf749916e2a9" + "reference": "975d7f7fd8fcb952364c6badc46d01a580532bf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/866f6cd84f2094cbc6f66ce9752faf749916e2a9", - "reference": "866f6cd84f2094cbc6f66ce9752faf749916e2a9", + "url": "https://api.github.com/repos/symfony/property-access/zipball/975d7f7fd8fcb952364c6badc46d01a580532bf9", + "reference": "975d7f7fd8fcb952364c6badc46d01a580532bf9", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/property-info": "^5.4|^6.0|^7.0" + "php": ">=8.2", + "symfony/property-info": "^6.4|^7.0" }, "require-dev": { - "symfony/cache": "^5.4|^6.0|^7.0" + "symfony/cache": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -1234,7 +1232,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v6.4.11" + "source": "https://github.com/symfony/property-access/tree/v7.1.6" }, "funding": [ { @@ -1250,20 +1248,20 @@ "type": "tidelift" } ], - "time": "2024-08-30T16:10:11+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/property-info", - "version": "v7.1.3", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "88a279df2db5b7919cac6f35d6a5d1d7147e6a9b" + "reference": "6b630ff585d9fdc72f50369885ad4364a849cf02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/88a279df2db5b7919cac6f35d6a5d1d7147e6a9b", - "reference": "88a279df2db5b7919cac6f35d6a5d1d7147e6a9b", + "url": "https://api.github.com/repos/symfony/property-info/zipball/6b630ff585d9fdc72f50369885ad4364a849cf02", + "reference": "6b630ff585d9fdc72f50369885ad4364a849cf02", "shasum": "" }, "require": { @@ -1318,7 +1316,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.1.3" + "source": "https://github.com/symfony/property-info/tree/v7.1.6" }, "funding": [ { @@ -1334,7 +1332,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T07:36:36+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/service-contracts", @@ -1421,16 +1419,16 @@ }, { "name": "symfony/string", - "version": "v7.1.5", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" + "reference": "61b72d66bf96c360a727ae6232df5ac83c71f626" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", - "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", + "url": "https://api.github.com/repos/symfony/string/zipball/61b72d66bf96c360a727ae6232df5ac83c71f626", + "reference": "61b72d66bf96c360a727ae6232df5ac83c71f626", "shasum": "" }, "require": { @@ -1488,7 +1486,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.5" + "source": "https://github.com/symfony/string/tree/v7.1.6" }, "funding": [ { @@ -1504,20 +1502,20 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/type-info", - "version": "v7.1.5", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "9f6094aa900d2c06bd61576a6f279d4ac441515f" + "reference": "a13032128c307470955c45c99201349b15cd7f4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/9f6094aa900d2c06bd61576a6f279d4ac441515f", - "reference": "9f6094aa900d2c06bd61576a6f279d4ac441515f", + "url": "https://api.github.com/repos/symfony/type-info/zipball/a13032128c307470955c45c99201349b15cd7f4a", + "reference": "a13032128c307470955c45c99201349b15cd7f4a", "shasum": "" }, "require": { @@ -1570,7 +1568,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.1.5" + "source": "https://github.com/symfony/type-info/tree/v7.1.6" }, "funding": [ { @@ -1586,20 +1584,20 @@ "type": "tidelift" } ], - "time": "2024-09-19T21:48:23+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.1.2", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "b80a669a2264609f07f1667f891dbfca25eba44c" + "reference": "90173ef89c40e7c8c616653241048705f84130ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/b80a669a2264609f07f1667f891dbfca25eba44c", - "reference": "b80a669a2264609f07f1667f891dbfca25eba44c", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/90173ef89c40e7c8c616653241048705f84130ef", + "reference": "90173ef89c40e7c8c616653241048705f84130ef", "shasum": "" }, "require": { @@ -1646,7 +1644,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.1.2" + "source": "https://github.com/symfony/var-exporter/tree/v7.1.6" }, "funding": [ { @@ -1662,32 +1660,31 @@ "type": "tidelift" } ], - "time": "2024-06-28T08:00:31+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.12", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "762ee56b2649659380e0ef4d592d807bc17b7971" + "reference": "3ced3f29e4f0d6bce2170ff26719f1fe9aacc671" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/762ee56b2649659380e0ef4d592d807bc17b7971", - "reference": "762ee56b2649659380e0ef4d592d807bc17b7971", + "url": "https://api.github.com/repos/symfony/yaml/zipball/3ced3f29e4f0d6bce2170ff26719f1fe9aacc671", + "reference": "3ced3f29e4f0d6bce2170ff26719f1fe9aacc671", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<5.4" + "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0" + "symfony/console": "^6.4|^7.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -1718,7 +1715,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.12" + "source": "https://github.com/symfony/yaml/tree/v7.1.6" }, "funding": [ { @@ -1734,7 +1731,7 @@ "type": "tidelift" } ], - "time": "2024-09-17T12:47:12+00:00" + "time": "2024-09-25T14:20:29+00:00" } ], "packages-dev": [ @@ -2288,16 +2285,16 @@ }, { "name": "doctrine/common", - "version": "3.4.4", + "version": "3.4.5", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a" + "reference": "6c8fef961f67b8bc802ce3e32e3ebd1022907286" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", - "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", + "url": "https://api.github.com/repos/doctrine/common/zipball/6c8fef961f67b8bc802ce3e32e3ebd1022907286", + "reference": "6c8fef961f67b8bc802ce3e32e3ebd1022907286", "shasum": "" }, "require": { @@ -2359,7 +2356,7 @@ ], "support": { "issues": "https://github.com/doctrine/common/issues", - "source": "https://github.com/doctrine/common/tree/3.4.4" + "source": "https://github.com/doctrine/common/tree/3.4.5" }, "funding": [ { @@ -2375,20 +2372,20 @@ "type": "tidelift" } ], - "time": "2024-04-16T13:35:33+00:00" + "time": "2024-10-08T15:53:43+00:00" }, { "name": "doctrine/dbal", - "version": "3.9.1", + "version": "3.9.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7" + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", - "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", "shasum": "" }, "require": { @@ -2404,7 +2401,7 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.12.0", + "phpstan/phpstan": "1.12.6", "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", @@ -2472,7 +2469,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.1" + "source": "https://github.com/doctrine/dbal/tree/3.9.3" }, "funding": [ { @@ -2488,7 +2485,7 @@ "type": "tidelift" } ], - "time": "2024-09-01T13:49:23+00:00" + "time": "2024-10-10T17:56:43+00:00" }, { "name": "doctrine/deprecations", @@ -2868,16 +2865,16 @@ }, { "name": "doctrine/orm", - "version": "2.19.7", + "version": "2.20.0", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "168ac31084226f94d42e7461a40ff5607a56bd35" + "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/168ac31084226f94d42e7461a40ff5607a56bd35", - "reference": "168ac31084226f94d42e7461a40ff5607a56bd35", + "url": "https://api.github.com/repos/doctrine/orm/zipball/8ed6c2234aba019f9737a6bcc9516438e62da27c", + "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c", "shasum": "" }, "require": { @@ -2906,7 +2903,9 @@ "doctrine/annotations": "^1.13 || ^2", "doctrine/coding-standard": "^9.0.2 || ^12.0", "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.11.1", + "phpstan/extension-installer": "~1.1.0 || ^1.4", + "phpstan/phpstan": "~1.4.10 || 1.12.6", + "phpstan/phpstan-deprecation-rules": "^1", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", @@ -2963,9 +2962,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.19.7" + "source": "https://github.com/doctrine/orm/tree/2.20.0" }, - "time": "2024-08-23T06:54:57+00:00" + "time": "2024-10-11T11:47:24+00:00" }, { "name": "doctrine/persistence", @@ -3337,16 +3336,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.3.0", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a", - "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { @@ -3389,9 +3388,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2024-09-29T13:56:26+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "phar-io/manifest", @@ -3513,16 +3512,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.6", + "version": "1.12.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae" + "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc4d2f145a88ea7141ae698effd64d9df46527ae", - "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", + "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", "shasum": "" }, "require": { @@ -3567,7 +3566,7 @@ "type": "github" } ], - "time": "2024-10-06T15:03:59+00:00" + "time": "2024-10-18T11:12:07+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4621,16 +4620,16 @@ }, { "name": "rector/rector", - "version": "1.2.6", + "version": "1.2.8", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99" + "reference": "05755bf43617449c08ee8e50fb840c85ad3b1240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/6ca85da28159dbd3bb36211c5104b7bc91278e99", - "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/05755bf43617449c08ee8e50fb840c85ad3b1240", + "reference": "05755bf43617449c08ee8e50fb840c85ad3b1240", "shasum": "" }, "require": { @@ -4668,7 +4667,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/1.2.6" + "source": "https://github.com/rectorphp/rector/tree/1.2.8" }, "funding": [ { @@ -4676,7 +4675,7 @@ "type": "github" } ], - "time": "2024-10-03T08:56:44+00:00" + "time": "2024-10-18T11:54:27+00:00" }, { "name": "sebastian/cli-parser", @@ -5643,16 +5642,16 @@ }, { "name": "symfony/clock", - "version": "v7.1.1", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7" + "reference": "97bebc53548684c17ed696bc8af016880f0f098d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/3dfc8b084853586de51dd1441c6242c76a28cbe7", - "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7", + "url": "https://api.github.com/repos/symfony/clock/zipball/97bebc53548684c17ed696bc8af016880f0f098d", + "reference": "97bebc53548684c17ed696bc8af016880f0f098d", "shasum": "" }, "require": { @@ -5697,7 +5696,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.1.1" + "source": "https://github.com/symfony/clock/tree/v7.1.6" }, "funding": [ { @@ -5713,28 +5712,102 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v6.3.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "93a8400a7eaaaf385b2d6f71e30e064baa639629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/93a8400a7eaaaf385b2d6f71e30e064baa639629", + "reference": "93a8400a7eaaaf385b2d6f71e30e064baa639629", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^5.4|^6.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v6.3.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T14:35:58+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.1.1", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7" + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", - "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.1", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/dependency-injection": "<5.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -5743,13 +5816,13 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -5777,7 +5850,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" }, "funding": [ { @@ -5793,7 +5866,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -5873,16 +5946,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.1.5", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a" + "reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/61fe0566189bf32e8cfee78335d8776f64a66f5a", - "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/c835867b3c62bb05c7fe3d637c871c7ae52024d4", + "reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4", "shasum": "" }, "require": { @@ -5919,7 +5992,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.1.5" + "source": "https://github.com/symfony/filesystem/tree/v7.1.6" }, "funding": [ { @@ -5935,20 +6008,20 @@ "type": "tidelift" } ], - "time": "2024-09-17T09:16:35+00:00" + "time": "2024-10-25T15:11:02+00:00" }, { "name": "symfony/finder", - "version": "v7.1.4", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "d95bbf319f7d052082fb7af147e0f835a695e823" + "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823", - "reference": "d95bbf319f7d052082fb7af147e0f835a695e823", + "url": "https://api.github.com/repos/symfony/finder/zipball/2cb89664897be33f78c65d3d2845954c8d7a43b8", + "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8", "shasum": "" }, "require": { @@ -5983,7 +6056,196 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.1.4" + "source": "https://github.com/symfony/finder/tree/v7.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-01T08:31:23+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "4c0341b3e0a7291e752c69d2a1ed9a84b68d604c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/4c0341b3e0a7291e752c69d2a1ed9a84b68d604c", + "reference": "4c0341b3e0a7291e752c69d2a1ed9a84b68d604c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.3" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.3|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-11T19:20:58+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v6.2.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "d05cebbc07478d37ff1e0f0079f06298a096b870" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/d05cebbc07478d37ff1e0f0079f06298a096b870", + "reference": "d05cebbc07478d37ff1e0f0079f06298a096b870", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/error-handler": "^6.1", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/http-foundation": "^5.4.21|^6.2.7", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/cache": "<5.4", + "symfony/config": "<6.1", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<6.2", + "symfony/doctrine-bridge": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/translation": "<5.4", + "symfony/twig-bridge": "<5.4", + "symfony/validator": "<5.4", + "twig/twig": "<2.13" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/config": "^6.1", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dependency-injection": "^6.2", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-client-contracts": "^1.1|^2|^3", + "symfony/process": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/translation-contracts": "^1.1|^2|^3", + "symfony/uid": "^5.4|^6.0", + "symfony/var-exporter": "^6.2", + "twig/twig": "^2.13|^3.0.4" + }, + "suggest": { + "symfony/browser-kit": "", + "symfony/config": "", + "symfony/console": "", + "symfony/dependency-injection": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v6.2.14" }, "funding": [ { @@ -5999,20 +6261,20 @@ "type": "tidelift" } ], - "time": "2024-08-13T14:28:19+00:00" + "time": "2023-07-31T10:40:35+00:00" }, { "name": "symfony/messenger", - "version": "v6.4.12", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/messenger.git", - "reference": "05035355ef94de2cb054f8697e65d82f67bf89d4" + "reference": "5cd75048611f7a86a7dca7f1cf7089d0d7ef90ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/messenger/zipball/05035355ef94de2cb054f8697e65d82f67bf89d4", - "reference": "05035355ef94de2cb054f8697e65d82f67bf89d4", + "url": "https://api.github.com/repos/symfony/messenger/zipball/5cd75048611f7a86a7dca7f1cf7089d0d7ef90ff", + "reference": "5cd75048611f7a86a7dca7f1cf7089d0d7ef90ff", "shasum": "" }, "require": { @@ -6070,7 +6332,7 @@ "description": "Helps applications send and receive messages to/from other applications or via message queues", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/messenger/tree/v6.4.12" + "source": "https://github.com/symfony/messenger/tree/v6.4.13" }, "funding": [ { @@ -6086,20 +6348,20 @@ "type": "tidelift" } ], - "time": "2024-09-08T12:31:10+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.1.1", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" + "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55", - "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/85e95eeede2d41cd146146e98c9c81d9214cae85", + "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85", "shasum": "" }, "require": { @@ -6137,7 +6399,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.1.1" + "source": "https://github.com/symfony/options-resolver/tree/v7.1.6" }, "funding": [ { @@ -6153,7 +6415,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/polyfill-php72", @@ -6454,16 +6716,16 @@ }, { "name": "symfony/stopwatch", - "version": "v7.1.1", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d" + "reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", - "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/8b4a434e6e7faf6adedffb48783a5c75409a1a05", + "reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05", "shasum": "" }, "require": { @@ -6496,7 +6758,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.1.1" + "source": "https://github.com/symfony/stopwatch/tree/v7.1.6" }, "funding": [ { @@ -6512,20 +6774,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/var-dumper", - "version": "v5.4.43", + "version": "v5.4.45", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "6be6a6a8af4818564e3726fc65cf936f34743cef" + "reference": "c4a5a08fe8d836a1aeec59eeee9697457fd28723" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6be6a6a8af4818564e3726fc65cf936f34743cef", - "reference": "6be6a6a8af4818564e3726fc65cf936f34743cef", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c4a5a08fe8d836a1aeec59eeee9697457fd28723", + "reference": "c4a5a08fe8d836a1aeec59eeee9697457fd28723", "shasum": "" }, "require": { @@ -6585,7 +6847,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v5.4.43" + "source": "https://github.com/symfony/var-dumper/tree/v5.4.45" }, "funding": [ { @@ -6601,7 +6863,7 @@ "type": "tidelift" } ], - "time": "2024-08-30T16:01:46+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { "name": "theseer/tokenizer", diff --git a/lib/php/workflow/src/Doctrine/Entity/JobState.php b/lib/php/workflow/src/Doctrine/Entity/JobState.php index 8e9edf6d9..80b79626c 100644 --- a/lib/php/workflow/src/Doctrine/Entity/JobState.php +++ b/lib/php/workflow/src/Doctrine/Entity/JobState.php @@ -9,14 +9,13 @@ use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping as ORM; -use Ramsey\Uuid\Uuid; class JobState { - protected string $id; - protected ?string $state = null; + protected int $number; + protected int $status; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)] @@ -30,9 +29,12 @@ class JobState protected ?ModelJobState $jobState = null; - public function __construct(protected ?WorkflowState $workflow, protected string $jobId) + public function __construct( + protected readonly string $id, + protected ?WorkflowState $workflow, + protected string $jobId, + ) { - $this->id = Uuid::uuid4()->toString(); } public function getId(): string @@ -62,6 +64,7 @@ public function setState(ModelJobState $state, EntityManagerInterface $em): void $this->endedAt = $state->getEndedAt()?->getDateTimeObject(); $this->startedAt = $state->getStartedAt()?->getDateTimeObject(); $this->status = $state->getStatus(); + $this->number = $state->getNumber(); } public function getJobState(): ModelJobState @@ -117,4 +120,9 @@ public function getJobId(): string { return $this->jobId; } + + public function getNumber(): int + { + return $this->number; + } } diff --git a/lib/php/workflow/src/Dumper/ConsoleWorkflowDumper.php b/lib/php/workflow/src/Dumper/ConsoleWorkflowDumper.php index 6acac7111..8ee95f2d4 100644 --- a/lib/php/workflow/src/Dumper/ConsoleWorkflowDumper.php +++ b/lib/php/workflow/src/Dumper/ConsoleWorkflowDumper.php @@ -56,7 +56,7 @@ public function dumpWorkflow(WorkflowState $state, Plan $plan, OutputInterface $ foreach ($stage->getRuns() as $runIndex => $run) { foreach ($run->getJob()->getSteps() as $stepIndex => $step) { $jobId = $run->getJob()->getId(); - $jobState = $state->getJobState($jobId); + $jobState = $state->getLastJobState($jobId); $table->addRow([ 0 === $runIndex ? $stageIndex + 1 : '', diff --git a/lib/php/workflow/src/Dumper/JsonWorkflowDumper.php b/lib/php/workflow/src/Dumper/JsonWorkflowDumper.php index 49ce846de..99f215f83 100644 --- a/lib/php/workflow/src/Dumper/JsonWorkflowDumper.php +++ b/lib/php/workflow/src/Dumper/JsonWorkflowDumper.php @@ -21,10 +21,10 @@ public function dumpWorkflow(WorkflowState $state, Plan $plan, OutputInterface $ foreach ($stage->getRuns() as $run) { $j = $run->getJob(); $jobId = $j->getId(); - $jobState = $state->getJobState($jobId); + $jobState = $state->getLastJobState($jobId); $job = [ - 'id' => $jobId, + 'jobId' => $jobId, 'name' => $j->getName(), 'needs' => array_values($j->getNeeds()->getArrayCopy()), 'if' => $j->getIf(), @@ -34,7 +34,18 @@ public function dumpWorkflow(WorkflowState $state, Plan $plan, OutputInterface $ ]; if ($jobState instanceof JobState) { - $job = [...$job, 'id' => $jobState->getJobId(), 'status' => $jobState->getStatus(), 'inputs' => $jobState->getInputs(), 'outputs' => $jobState->getOutputs(), 'triggeredAt' => $jobState->getTriggeredAt()->formatAtom(), 'startedAt' => $jobState->getStartedAt()?->formatAtom(), 'endedAt' => $jobState->getEndedAt()?->formatAtom(), 'duration' => StateUtil::getFormattedDuration($jobState->getDuration())]; + $job = [ + ...$job, + 'stateId' => $jobState->getId(), + 'status' => $jobState->getStatus(), + 'number' => $jobState->getNumber(), + 'inputs' => $jobState->getInputs(), + 'outputs' => $jobState->getOutputs(), + 'triggeredAt' => $jobState->getTriggeredAt()->formatAtom(), + 'startedAt' => $jobState->getStartedAt()?->formatAtom(), + 'endedAt' => $jobState->getEndedAt()?->formatAtom(), + 'duration' => StateUtil::getFormattedDuration($jobState->getDuration()) + ]; if (!empty($jobState->getErrors())) { $job['errors'] = $jobState->getErrors(); diff --git a/lib/php/workflow/src/Executor/Expression/JobsAccessor.php b/lib/php/workflow/src/Executor/Expression/JobsAccessor.php index a42acf8e7..3e9f06222 100644 --- a/lib/php/workflow/src/Executor/Expression/JobsAccessor.php +++ b/lib/php/workflow/src/Executor/Expression/JobsAccessor.php @@ -14,7 +14,7 @@ public function __construct(private WorkflowState $workflowState) public function __get(string $name) { - if (null !== $jobState = $this->workflowState->getJobState($name)) { + if (null !== $jobState = $this->workflowState->getLastJobState($name)) { return new ObjectOrArrayAccessor($jobState); } diff --git a/lib/php/workflow/src/Executor/JobExecutor.php b/lib/php/workflow/src/Executor/JobExecutor.php index 439814e39..40337f492 100644 --- a/lib/php/workflow/src/Executor/JobExecutor.php +++ b/lib/php/workflow/src/Executor/JobExecutor.php @@ -15,6 +15,7 @@ use Alchemy\Workflow\State\JobState; use Alchemy\Workflow\State\Repository\LockAwareStateRepositoryInterface; use Alchemy\Workflow\State\Repository\StateRepositoryInterface; +use Alchemy\Workflow\State\Repository\TransactionalStateRepositoryInterface; use Alchemy\Workflow\State\WorkflowState; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; @@ -23,18 +24,23 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcher; -readonly class JobExecutor +final class JobExecutor { - private LoggerInterface $logger; - private OutputInterface $output; - private EnvContainer $envs; - private EventDispatcherInterface $eventDispatcher; + private readonly LoggerInterface $logger; + private readonly OutputInterface $output; + private readonly EnvContainer $envs; + private readonly EventDispatcherInterface $eventDispatcher; + + /** + * @var JobUpdateEvent[] + */ + private array $eventsToDispatch = []; public function __construct( - private iterable $executors, - private ActionRegistryInterface $actionRegistry, - private ExpressionParser $expressionParser, - private StateRepositoryInterface $stateRepository, + private readonly iterable $executors, + private readonly ActionRegistryInterface $actionRegistry, + private readonly ExpressionParser $expressionParser, + private readonly StateRepositoryInterface $stateRepository, ?OutputInterface $output = null, ?LoggerInterface $logger = null, ?EnvContainer $envs = null, @@ -68,80 +74,107 @@ private function shouldBeSkipped(JobExecutionContext $context, Job $job): bool return false; } - public function executeJob(WorkflowState $workflowState, Job $job, array $env = []): void + private function wrapInTransaction(callable $callback): mixed { - $workflowId = $workflowState->getId(); - $jobId = $job->getId(); - - if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { - $this->stateRepository->acquireJobLock($workflowId, $jobId); + if ($this->stateRepository instanceof TransactionalStateRepositoryInterface) { + return $this->stateRepository->transactional($callback); } - try { - $jobState = $this->stateRepository->getJobState($workflowId, $jobId); + return $callback(); + } + + public function executeJob(WorkflowState $workflowState, Job $job, string $jobStateId, array $env = []): void + { + $context = $this->wrapInTransaction(function () use ($workflowState, $job, $jobStateId, $env): ?JobExecutionContext { + $workflowId = $workflowState->getId(); - if (null === $jobState) { - throw new \InvalidArgumentException(sprintf('State of job "%s" does not exists for workflow "%s"', $jobId, $workflowId)); + if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { + $this->stateRepository->acquireJobLock($workflowId, $jobStateId); } - $status = $jobState->getStatus(); - if (JobState::STATUS_TRIGGERED !== $status) { - if (JobState::STATUS_CANCELLED === $status) { - return; - } + $jobState = $this->stateRepository->getJobState($workflowId, $jobStateId); - throw new ConcurrencyException(sprintf('Job "%s" has not the "%s" status for workflow "%s" (got "%s")', $jobId, JobState::STATUS_LABELS[JobState::STATUS_TRIGGERED], $workflowId, JobState::STATUS_LABELS[$status])); - } + $jobId = $job->getId(); - $context = new JobExecutionContext( - $workflowState, - $jobState, - $this->output, - $this->envs->mergeWith($env), - ($workflowState->getEvent()?->getInputs() ?? new Inputs())->mergeWith($jobState->getInputs()?->getArrayCopy() ?? []) - ); + try { + $status = $jobState->getStatus(); + if (JobState::STATUS_TRIGGERED !== $status) { + if (JobState::STATUS_CANCELLED === $status) { + return null; + } - $jobInputs = $context->getInputs() - ->mergeWith($this->expressionParser->evaluateArray($job->getWith()->getArrayCopy(), $context)); - $context->replaceInputs($jobInputs); + throw new ConcurrencyException(sprintf('Job "%s" has not the "%s" status for workflow "%s" (got "%s")', $jobId, JobState::STATUS_LABELS[JobState::STATUS_TRIGGERED], $workflowId, JobState::STATUS_LABELS[$status])); + } - $jobState->setInputs($jobInputs); + $context = new JobExecutionContext( + $workflowState, + $jobState, + $this->output, + $this->envs->mergeWith($env), + ($workflowState->getEvent()?->getInputs() ?? new Inputs())->mergeWith($jobState->getInputs()?->getArrayCopy() ?? []) + ); + + $jobInputs = $context->getInputs() + ->mergeWith($this->expressionParser->evaluateArray($job->getWith()->getArrayCopy(), $context)); + $context->replaceInputs($jobInputs); + + $jobState->setInputs($jobInputs); + + try { + $shouldBeSkipped = $this->shouldBeSkipped($context, $job); + } catch (\Throwable $e) { + $error = sprintf('Error while evaluating if condition: %s', $e->getMessage()); + $this->logger->error($error); + $jobState->addError($error); + $jobState->setStatus(JobState::STATUS_ERROR); + $this->persistJobState($jobState); + + return null; + } - try { - $shouldBeSkipped = $this->shouldBeSkipped($context, $job); - } catch (\Throwable $e) { - $error = sprintf('Error while evaluating if condition: %s', $e->getMessage()); - $this->logger->error($error); - $jobState->addError($error); - $jobState->setStatus(JobState::STATUS_ERROR); - $this->persistJobState($jobState); + if ($shouldBeSkipped) { + $jobState->setStatus(JobState::STATUS_SKIPPED); + $this->persistJobState($jobState); - return; - } + return null; + } - if ($shouldBeSkipped) { - $jobState->setStatus(JobState::STATUS_SKIPPED); + $jobState->setStatus(JobState::STATUS_RUNNING); + $jobState->setStartedAt(new MicroDateTime()); $this->persistJobState($jobState); - return; - } - - $jobState->setStatus(JobState::STATUS_RUNNING); - $jobState->setStartedAt(new MicroDateTime()); - $this->persistJobState($jobState); - } catch (\Throwable $e) { - try { - if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { - $this->stateRepository->releaseJobLock($workflowId, $jobId); + return $context; + } catch (\Throwable $e) { + try { + if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { + $this->stateRepository->releaseJobLock($workflowId, $jobState->getId()); + } + } catch (\Throwable $e2) { + throw new \RuntimeException(sprintf('Error while releasing job lock after another error: %s (First error was: %s)', $e2->getMessage(), $e->getMessage()), 0, $e); } - } catch (\Throwable $e2) { - throw new \RuntimeException(sprintf('Error while releasing job lock after another error: %s (First error was: %s)', $e2->getMessage(), $e->getMessage()), 0, $e); + + throw $e; } + }); - throw $e; + $this->flushEvents(); + + if (null === $context) { + return; } $this->runJob($context, $job); + + $this->flushEvents(); + } + + private function dispatchEvent(JobUpdateEvent $event): void + { + if ($this->stateRepository instanceof TransactionalStateRepositoryInterface) { + $this->eventsToDispatch[] = $event; + } else { + $this->eventDispatcher->dispatch($event); + } } private function persistJobState(JobState $jobState): void @@ -149,10 +182,10 @@ private function persistJobState(JobState $jobState): void $this->stateRepository->persistJobState($jobState); if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { - $this->stateRepository->releaseJobLock($jobState->getWorkflowId(), $jobState->getJobId()); + $this->stateRepository->releaseJobLock($jobState->getWorkflowId(), $jobState->getId()); } - $this->eventDispatcher->dispatch(new JobUpdateEvent($jobState->getWorkflowId(), $jobState->getJobId(), $jobState->getStatus())); + $this->dispatchEvent(new JobUpdateEvent($jobState->getWorkflowId(), $jobState->getJobId(), $jobState->getId(), $jobState->getStatus())); } private function runJob(JobExecutionContext $context, Job $job): void @@ -257,4 +290,13 @@ private function extractOutputs(Job $job, JobExecutionContext $context): void } } } + + private function flushEvents(): void + { + $events = $this->eventsToDispatch; + $this->eventsToDispatch = []; + foreach ($events as $event) { + $this->eventDispatcher->dispatch($event); + } + } } diff --git a/lib/php/workflow/src/Executor/PlanExecutor.php b/lib/php/workflow/src/Executor/PlanExecutor.php index b08d085ac..f4c985171 100644 --- a/lib/php/workflow/src/Executor/PlanExecutor.php +++ b/lib/php/workflow/src/Executor/PlanExecutor.php @@ -7,6 +7,7 @@ use Alchemy\Workflow\Planner\WorkflowPlanner; use Alchemy\Workflow\Repository\WorkflowRepositoryInterface; use Alchemy\Workflow\State\Repository\StateRepositoryInterface; +use Alchemy\Workflow\Trigger\JobTrigger; final readonly class PlanExecutor { @@ -17,9 +18,9 @@ public function __construct( ) { } - public function executePlan(string $workflowId, string $jobId): void + public function executePlan(JobTrigger $jobTrigger): void { - $workflowState = $this->stateRepository->getWorkflowState($workflowId); + $workflowState = $this->stateRepository->getWorkflowState($jobTrigger->getWorkflowId()); $event = $workflowState->getEvent(); $workflow = $this->workflowRepository->loadWorkflowByName($workflowState->getWorkflowName()); @@ -30,6 +31,6 @@ public function executePlan(string $workflowId, string $jobId): void $planner = new WorkflowPlanner([$workflow]); $plan = null === $event ? $planner->planAll() : $planner->planEvent($event); - $this->jobExecutor->executeJob($workflowState, $plan->getJob($jobId), $workflow->getEnv()->getArrayCopy()); + $this->jobExecutor->executeJob($workflowState, $plan->getJob($jobTrigger->getJobId()), $jobTrigger->getJobStateId(), $workflow->getEnv()->getArrayCopy()); } } diff --git a/lib/php/workflow/src/Listener/JobUpdateEvent.php b/lib/php/workflow/src/Listener/JobUpdateEvent.php index c0fab0ff2..1c754f1b4 100644 --- a/lib/php/workflow/src/Listener/JobUpdateEvent.php +++ b/lib/php/workflow/src/Listener/JobUpdateEvent.php @@ -7,6 +7,7 @@ public function __construct( private string $workflowId, private string $jobId, + private string $jobStateId, private int $status, ) { } @@ -21,6 +22,11 @@ public function getJobId(): string return $this->jobId; } + public function getJobStateId(): string + { + return $this->jobStateId; + } + public function getStatus(): int { return $this->status; diff --git a/lib/php/workflow/src/Message/JobConsumer.php b/lib/php/workflow/src/Message/JobConsumer.php index 5d17f5e0c..4aaaa48b8 100644 --- a/lib/php/workflow/src/Message/JobConsumer.php +++ b/lib/php/workflow/src/Message/JobConsumer.php @@ -4,7 +4,11 @@ final readonly class JobConsumer { - public function __construct(private string $workflowId, private string $jobId) + public function __construct( + private string $workflowId, + private string $jobId, + private string $jobStateId, + ) { } @@ -17,4 +21,9 @@ public function getJobId(): string { return $this->jobId; } + + public function getJobStateId(): string + { + return $this->jobStateId; + } } diff --git a/lib/php/workflow/src/Message/JobConsumerHandler.php b/lib/php/workflow/src/Message/JobConsumerHandler.php index dfc2d833f..377324724 100644 --- a/lib/php/workflow/src/Message/JobConsumerHandler.php +++ b/lib/php/workflow/src/Message/JobConsumerHandler.php @@ -2,7 +2,8 @@ namespace Alchemy\Workflow\Message; -use Alchemy\Workflow\Executor\PlanExecutor; +use Alchemy\Workflow\Runner\RunnerInterface; +use Alchemy\Workflow\Trigger\JobTrigger; use Alchemy\Workflow\WorkflowOrchestrator; use Symfony\Component\Messenger\Attribute\AsMessageHandler; @@ -10,14 +11,19 @@ final readonly class JobConsumerHandler { public function __construct( - private PlanExecutor $planExecutor, + private RunnerInterface $runner, private WorkflowOrchestrator $orchestrator, ) { } public function __invoke(JobConsumer $message): void { - $this->planExecutor->executePlan($message->getWorkflowId(), $message->getJobId()); + $this->runner->run(new JobTrigger( + $message->getWorkflowId(), + $message->getJobId(), + $message->getJobStateId(), + )); + $this->orchestrator->continueWorkflow($message->getWorkflowId()); } } diff --git a/lib/php/workflow/src/Runner/RunnerInterface.php b/lib/php/workflow/src/Runner/RunnerInterface.php index bb9290ee7..c92fc29b7 100644 --- a/lib/php/workflow/src/Runner/RunnerInterface.php +++ b/lib/php/workflow/src/Runner/RunnerInterface.php @@ -4,7 +4,9 @@ namespace Alchemy\Workflow\Runner; +use Alchemy\Workflow\Trigger\JobTrigger; + interface RunnerInterface { - public function run(string $workflowId, string $jobId): void; + public function run(JobTrigger $jobTrigger): void; } diff --git a/lib/php/workflow/src/Runner/RuntimeRunner.php b/lib/php/workflow/src/Runner/RuntimeRunner.php index 8d618a199..f00bfccb5 100644 --- a/lib/php/workflow/src/Runner/RuntimeRunner.php +++ b/lib/php/workflow/src/Runner/RuntimeRunner.php @@ -5,6 +5,7 @@ namespace Alchemy\Workflow\Runner; use Alchemy\Workflow\Executor\PlanExecutor; +use Alchemy\Workflow\Trigger\JobTrigger; readonly class RuntimeRunner implements RunnerInterface { @@ -12,8 +13,8 @@ public function __construct(private PlanExecutor $planExecutor) { } - public function run(string $workflowId, string $jobId): void + public function run(JobTrigger $jobTrigger): void { - $this->planExecutor->executePlan($workflowId, $jobId); + $this->planExecutor->executePlan($jobTrigger); } } diff --git a/lib/php/workflow/src/State/JobState.php b/lib/php/workflow/src/State/JobState.php index 6dccb13ca..bf51f255b 100644 --- a/lib/php/workflow/src/State/JobState.php +++ b/lib/php/workflow/src/State/JobState.php @@ -5,18 +5,19 @@ namespace Alchemy\Workflow\State; use Alchemy\Workflow\Date\MicroDateTime; +use Ramsey\Uuid\Uuid; -class JobState +final class JobState { - final public const STATUS_TRIGGERED = 0; - final public const STATUS_SUCCESS = 1; - final public const STATUS_FAILURE = 2; - final public const STATUS_SKIPPED = 3; - final public const STATUS_RUNNING = 4; - final public const STATUS_ERROR = 5; - final public const STATUS_CANCELLED = 6; - - final public const STATUS_LABELS = [ + final public const int STATUS_TRIGGERED = 0; + final public const int STATUS_SUCCESS = 1; + final public const int STATUS_FAILURE = 2; + final public const int STATUS_SKIPPED = 3; + final public const int STATUS_RUNNING = 4; + final public const int STATUS_ERROR = 5; + final public const int STATUS_CANCELLED = 6; + + final public const array STATUS_LABELS = [ self::STATUS_TRIGGERED => 'triggered', self::STATUS_SUCCESS => 'success', self::STATUS_FAILURE => 'failure', @@ -25,6 +26,9 @@ class JobState self::STATUS_ERROR => 'error', self::STATUS_CANCELLED => 'cancelled', ]; + + private readonly string $id; + private array $errors = []; private Outputs $outputs; private ?Inputs $inputs = null; @@ -37,12 +41,23 @@ class JobState private ?MicroDateTime $startedAt = null; private ?MicroDateTime $endedAt = null; - public function __construct(private string $workflowId, private string $jobId, private int $status) - { + public function __construct( + private readonly string $workflowId, + private readonly string $jobId, + private int $status = self::STATUS_TRIGGERED, + ?string $id = null, + private readonly int $number = 0, + ) { + $this->id = $id ?? Uuid::uuid4()->toString(); $this->triggeredAt = new MicroDateTime(); $this->outputs = new Outputs(); } + public function getId(): string + { + return $this->id; + } + public function getStatus(): int { return $this->status; @@ -139,11 +154,14 @@ public function __serialize(): array 'startedAt' => $this->startedAt, 'endedAt' => $this->endedAt, 'errors' => $this->errors, + 'id' => $this->id, + 'number' => $this->number, ]; } public function __unserialize(array $data): void { + $this->id = $data['id'] ?? 'unset'; $this->workflowId = $data['workflowId']; $this->jobId = $data['jobId']; $this->status = $data['status']; @@ -153,6 +171,7 @@ public function __unserialize(array $data): void $this->startedAt = $data['startedAt'] ?? null; $this->endedAt = $data['endedAt'] ?? null; $this->errors = $data['errors'] ?? []; + $this->number = $data['number'] ?? 0; } public function getSteps(): array @@ -171,4 +190,9 @@ public function setInputs(?Inputs $inputs): JobState return $this; } + + public function getNumber(): int + { + return $this->number; + } } diff --git a/lib/php/workflow/src/State/Repository/DoctrineStateRepository.php b/lib/php/workflow/src/State/Repository/DoctrineStateRepository.php index b3ffed877..eb203251e 100644 --- a/lib/php/workflow/src/State/Repository/DoctrineStateRepository.php +++ b/lib/php/workflow/src/State/Repository/DoctrineStateRepository.php @@ -16,12 +16,13 @@ use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; -#[AsEventListener(event: WorkerMessageHandledEvent::class, method: 'clear')] -#[AsEventListener(event: WorkerMessageFailedEvent::class, method: 'clear')] -#[AsEventListener(event: KernelEvents::TERMINATE, method: 'clear')] -class DoctrineStateRepository implements LockAwareStateRepositoryInterface +#[AsEventListener(event: WorkerMessageHandledEvent::class, method: 'flush')] +#[AsEventListener(event: WorkerMessageFailedEvent::class, method: 'flush')] +#[AsEventListener(event: KernelEvents::TERMINATE, method: 'flush')] +class DoctrineStateRepository implements LockAwareStateRepositoryInterface, TransactionalStateRepositoryInterface { - private array $jobs = []; + use JobStatusCacheTrait; + private readonly string $workflowStateEntity; private readonly string $jobStateEntity; @@ -34,11 +35,6 @@ public function __construct( $this->jobStateEntity = $jobStateEntity ?? JobStateEntity::class; } - public function clear(): void - { - $this->jobs = []; - } - public function getWorkflowState(string $id): WorkflowState { $entity = $this->em->getRepository($this->workflowStateEntity)->find($id); @@ -52,6 +48,27 @@ public function getWorkflowState(string $id): WorkflowState return $state; } + public function flush(): void + { + $this->clearCache(); + } + + public function createJobState(string $workflowId, string $jobId): JobState + { + $number = $this->createListQueryBuilder($workflowId, $jobId) + ->select('MAX(t.number) + 1') + ->getQuery() + ->getSingleScalarResult(); + + return new JobState( + $workflowId, + $jobId, + JobState::STATUS_TRIGGERED, + null, + $number ?? 0, + ); + } + public function persistWorkflowState(WorkflowState $state): void { $workflowStateEntity = $this->workflowStateEntity; @@ -66,24 +83,34 @@ public function persistWorkflowState(WorkflowState $state): void $this->em->flush($entity); } - public function getJobState(string $workflowId, string $jobId): ?JobState + public function getJobState(string $workflowId, string $jobStateId): ?JobState { - $entity = $this->fetchJobEntity($workflowId, $jobId); - if (!$entity instanceof JobStateEntity) { - return null; + return $this->fetchJobState($workflowId, $jobStateId)?->getJobState(); + } + + public function getLastJobState(string $workflowId, string $jobId): ?JobState + { + return $this->fetchLastJobState($workflowId, $jobId)?->getJobState(); + } + + public function getJobStates(string $workflowId, string $jobId): array + { + $entities = $this->fetchJobStates($workflowId, $jobId); + if (empty($entities)) { + return []; } - return $entity->getJobState(); + return array_map(fn (JobStateEntity $state) => $state->getJobState(), $entities); } - public function removeJobState(string $workflowId, string $jobId): void + public function removeJobState(string $workflowId, string $jobStateId): void { - $entity = $this->fetchJobEntity($workflowId, $jobId); + $entity = $this->fetchJobState($workflowId, $jobStateId); if ($entity instanceof JobStateEntity) { $this->em->remove($entity); $this->em->flush(); - unset($this->jobs[$workflowId][$jobId]); + $this->removeJobStateFromCache($workflowId, $jobStateId); } } @@ -91,27 +118,16 @@ public function resetJobState(string $workflowId, string $jobId): void { } - public function acquireJobLock(string $workflowId, string $jobId): void + public function acquireJobLock(string $workflowId, string $jobStateId): void { - $this->em->beginTransaction(); - try { - $entity = $this->createQueryBuilder($workflowId, $jobId) - ->getQuery() - ->setLockMode(LockMode::PESSIMISTIC_WRITE) - ->getOneOrNullResult(); - - if ($entity instanceof JobStateEntity) { - $this->jobs[$workflowId][$jobId] = $entity; - } else { - unset($this->jobs[$workflowId][$jobId]); - } - } catch (\Throwable $e) { - $this->em->rollback(); - throw $e; - } + $entity = $this->em + ->getRepository($this->jobStateEntity) + ->find($jobStateId, LockMode::PESSIMISTIC_WRITE); + + $this->cacheJobState($workflowId, $jobStateId, $entity); } - private function createQueryBuilder(string $workflowId, string $jobId): QueryBuilder + private function createListQueryBuilder(string $workflowId, string $jobId): QueryBuilder { return $this->em->getRepository($this->jobStateEntity) ->createQueryBuilder('t') @@ -121,30 +137,29 @@ private function createQueryBuilder(string $workflowId, string $jobId): QueryBui ->setParameters([ 'w' => $workflowId, 'j' => $jobId, - ]) - ->addOrderBy('t.triggeredAt', 'DESC') - ->setMaxResults(1); + ]); } - public function releaseJobLock(string $workflowId, string $jobId): void + public function releaseJobLock(string $workflowId, string $jobStateId): void { - $this->em->commit(); } public function persistJobState(JobState $state): void { $entity = null; if (JobState::STATUS_TRIGGERED !== $state->getStatus()) { - $entity = $this->fetchJobEntity($state->getWorkflowId(), $state->getJobId()); + $entity = $this->fetchJobState($state->getWorkflowId(), $state->getId()); } if (!$entity instanceof JobStateEntity) { $jobStateEntity = $this->jobStateEntity; $entity = new $jobStateEntity( + $state->getId(), $this->em->getReference($this->workflowStateEntity, $state->getWorkflowId()), $state->getJobId() ); - $this->jobs[$entity->getWorkflow()->getId()][$entity->getJobId()] = $entity; + $this->statuses[$state->getId()] = $state; + $this->statuses[$entity->getWorkflow()->getId()][$entity->getJobId()][] = $entity; } $entity->setState($state, $this->em); @@ -153,22 +168,85 @@ public function persistJobState(JobState $state): void $this->em->flush($entity); } - private function fetchJobEntity(string $workflowId, string $jobId): ?JobStateEntity + private function fetchJobState(string $workflowId, string $jobStateId): ?JobStateEntity + { + $state = $this->statuses[$jobStateId] ?? null; + if ($state instanceof JobStateEntity) { + return $state; + } + + $state = $this->em->getRepository($this->jobStateEntity) + ->findOneBy(['id' => $jobStateId]); + + $this->cacheJobState($workflowId, $jobStateId, $state); + + return $state; + } + + private function fetchLastJobState(string $workflowId, string $jobId): ?JobStateEntity { - if (isset($this->jobs[$workflowId][$jobId])) { - return $this->jobs[$workflowId][$jobId]; + $state = $this->lastByJobId[$workflowId][$jobId] ?? null; + if ($state instanceof JobStateEntity) { + return $state; } - $entity = $this->createQueryBuilder($workflowId, $jobId) + $state = $this->createListQueryBuilder($workflowId, $jobId) + ->addOrderBy('t.triggeredAt', 'DESC') + ->setMaxResults(1) ->getQuery() ->getOneOrNullResult(); - if ($entity) { - $this->jobs[$workflowId][$jobId] = $entity; - } else { - unset($this->jobs[$workflowId][$jobId]); + if ($state) { + $this->cacheLastJobState($workflowId, $jobId, $state); + } + + return $state; + } + + /** + * @return JobStateEntity[] + */ + private function fetchJobStates(string $workflowId, string $jobId): array + { + $statuses = $this->statusesByJobId[$workflowId][$jobId] ?? null; + if (null !== $statuses) { + return $statuses; } - return $entity; + $states = $this->createListQueryBuilder($workflowId, $jobId) + ->addOrderBy('t.triggeredAt', 'DESC') + ->getQuery() + ->getResult(); + + $this->cacheJobStates($workflowId, $jobId, $states); + + return $states; + } + + public function acquireWorkflowLock(string $workflowId): void + { + $entity = $this->em + ->getRepository($this->workflowStateEntity) + ->find($workflowId, LockMode::PESSIMISTIC_WRITE); + + $this->cacheWorkflowState($workflowId, $entity); + } + + public function releaseWorkflowLock(string $workflowId): void + { + } + + public function transactional(callable $callback) + { + $this->em->beginTransaction(); + try { + $response = $callback(); + $this->em->commit(); + + return $response; + } catch (\Throwable $e) { + $this->em->rollback(); + throw $e; + } } } diff --git a/lib/php/workflow/src/State/Repository/FileSystemStateRepository.php b/lib/php/workflow/src/State/Repository/FileSystemStateRepository.php index 3e4089455..08ed61c10 100644 --- a/lib/php/workflow/src/State/Repository/FileSystemStateRepository.php +++ b/lib/php/workflow/src/State/Repository/FileSystemStateRepository.php @@ -14,7 +14,8 @@ class FileSystemStateRepository implements LockAwareStateRepositoryInterface private const string JOB_PREFIX = 'job::'; private readonly string $path; - private array $fileDescriptors = []; + private array $jobFileDescriptors = []; + private array $workflowFileDescriptors = []; public function __construct(string $path) { @@ -43,23 +44,11 @@ public function getWorkflowState(string $id): WorkflowState return $state; } - public function persistWorkflowState(WorkflowState $state): void + public function getJobState(string $workflowId, string $jobStateId): ?JobState { - $path = $this->getWorkflowPath($state->getId(), self::WORKFLOW_FILENAME); - - $dir = dirname($path); - if (!is_dir($dir)) { - mkdir($dir, 0755); - } - - file_put_contents($path, serialize($state)); - } - - public function getJobState(string $workflowId, string $jobId): ?JobState - { - $fd = $this->fileDescriptors[$workflowId][$jobId] ?? null; + $fd = $this->jobFileDescriptors[$workflowId][$jobStateId] ?? null; if (null === $fd) { - return $this->readJobState($workflowId, $jobId); + return $this->readJobState($workflowId, $jobStateId); } fseek($fd, 0); @@ -75,9 +64,37 @@ public function getJobState(string $workflowId, string $jobId): ?JobState return unserialize($content); } - private function readJobState(string $workflowId, string $jobId): ?JobState + public function getLastJobState(string $workflowId, string $jobId): ?JobState + { + $jobStateId = $this->getStateId($jobId); + + return $this->getJobState($workflowId, $jobStateId); + } + + public function createJobState(string $workflowId, string $jobId): JobState + { + return new JobState($workflowId, $jobId, id: $this->getStateId($jobId)); + } + + private function getStateId(string $jobId): string { - $path = $this->getJobPath($workflowId, $jobId); + return sprintf('%s-0', $jobId); + } + + public function getJobStates(string $workflowId, string $jobId): array + { + $jobStateId = $this->getStateId($jobId); + $state = $this->getJobState($workflowId, $jobStateId); + if (null === $state) { + return []; + } + + return [$state]; + } + + private function readJobState(string $workflowId, string $jobStateId): ?JobState + { + $path = $this->getJobPath($workflowId, $jobStateId); if (file_exists($path)) { return unserialize(file_get_contents($path)); } @@ -85,47 +102,86 @@ private function readJobState(string $workflowId, string $jobId): ?JobState return null; } - public function acquireJobLock(string $workflowId, string $jobId): void + public function acquireWorkflowLock(string $workflowId): void { - $path = $this->getJobPath($workflowId, $jobId); + $path = $this->getWorkflowPath($workflowId, self::WORKFLOW_FILENAME); $fd = fopen($path, 'c+'); if (!flock($fd, LOCK_EX)) { throw new LockException(sprintf('Cannot acquire lock on "%s"', $path)); } - $this->fileDescriptors[$workflowId][$jobId] = $fd; + $this->workflowFileDescriptors[$workflowId] = $fd; } - public function releaseJobLock(string $workflowId, string $jobId): void + public function releaseWorkflowLock(string $workflowId): void { - $fd = $this->fileDescriptors[$workflowId][$jobId] ?? null; + $fd = $this->workflowFileDescriptors[$workflowId] ?? null; if ($fd) { flock($fd, LOCK_UN); fclose($fd); } - unset($this->fileDescriptors[$workflowId][$jobId]); + unset($this->workflowFileDescriptors[$workflowId]); + } + + public function persistWorkflowState(WorkflowState $state): void + { + $path = $this->getWorkflowPath($state->getId(), self::WORKFLOW_FILENAME); + + $dir = dirname($path); + if (!is_dir($dir)) { + mkdir($dir, 0755); + } + + file_put_contents($path, serialize($state)); + } + + public function acquireJobLock(string $workflowId, string $jobStateId): void + { + $path = $this->getJobPath($workflowId, $jobStateId); + $fd = fopen($path, 'c+'); + + if (!flock($fd, LOCK_EX)) { + throw new LockException(sprintf('Cannot acquire lock on "%s"', $path)); + } + + $this->jobFileDescriptors[$workflowId][$jobStateId] = $fd; + } + + public function releaseJobLock(string $workflowId, string $jobStateId): void + { + $fd = $this->jobFileDescriptors[$workflowId][$jobStateId] ?? null; + if ($fd) { + flock($fd, LOCK_UN); + fclose($fd); + } + + unset($this->jobFileDescriptors[$workflowId][$jobStateId]); } public function persistJobState(JobState $state): void { - $fd = $this->fileDescriptors[$state->getWorkflowId()][$state->getJobId()] ?? null; - if (null === $fd) { - $path = $this->getJobPath($state->getWorkflowId(), $state->getJobId()); - $fd = fopen($path, 'r+'); + $jobStateId = $state->getId(); + $fd = $this->jobFileDescriptors[$state->getWorkflowId()][$jobStateId] ?? null; + $hadLock = null === $fd; + if ($hadLock) { + $path = $this->getJobPath($state->getWorkflowId(), $jobStateId); + $fd = fopen($path, 'c+'); } ftruncate($fd, 0); fseek($fd, 0); fwrite($fd, serialize($state)); fflush($fd); - flock($fd, LOCK_UN); + if ($hadLock) { + flock($fd, LOCK_UN); + } } - public function removeJobState(string $workflowId, string $jobId): void + public function removeJobState(string $workflowId, string $jobStateId): void { - $path = $this->getJobPath($workflowId, $jobId); + $path = $this->getJobPath($workflowId, $jobStateId); if (file_exists($path)) { unlink($path); } @@ -141,8 +197,8 @@ private function getWorkflowPath(string $id, string $filename): string return $this->path.DIRECTORY_SEPARATOR.$id.DIRECTORY_SEPARATOR.$filename.'.state'; } - private function getJobPath(string $workflowId, string $jobId): string + private function getJobPath(string $workflowId, string $jobStateId): string { - return $this->getWorkflowPath($workflowId, self::JOB_PREFIX.$jobId); + return $this->getWorkflowPath($workflowId, self::JOB_PREFIX.$jobStateId); } } diff --git a/lib/php/workflow/src/State/Repository/JobStatusCacheTrait.php b/lib/php/workflow/src/State/Repository/JobStatusCacheTrait.php new file mode 100644 index 000000000..5df66cf23 --- /dev/null +++ b/lib/php/workflow/src/State/Repository/JobStatusCacheTrait.php @@ -0,0 +1,122 @@ + + */ + protected array $workflows = []; + + /** + * @var array + */ + protected array $statuses = []; + + /** + * @var array> + */ + protected array $statusesByJobId = []; + + /** + * @var array> + */ + protected array $lastByJobId = []; + + public function clearCache(): void + { + $this->statuses = []; + $this->statusesByJobId = []; + $this->workflows = []; + $this->lastByJobId = []; + } + + protected function cacheWorkflowState(string $workflowId, ?object $state): void + { + if (null === $state) { + unset($this->workflows[$workflowId]); + } else { + $this->workflows[$workflowId] = $state; + } + } + + protected function cacheJobState(string $workflowId, string $jobStateId, ?object $state): void + { + if (null === $state) { + $this->removeJobStateFromCache($workflowId, $jobStateId); + } else { + $this->statuses[$state->getId()] = $state; + + if (isset($this->lastByJobId[$workflowId])) { + foreach ($this->lastByJobId[$workflowId] as $jobId => $state) { + if ($state->getId() === $jobStateId) { + $this->lastByJobId[$workflowId][$jobId] = $state; + } + } + } + + if (isset($this->statusesByJobId[$workflowId][$state->getJobId()])) { + $this->statusesByJobId[$workflowId][$state->getJobId()] = array_map( + function (object $s) use ($state): object { + if ($s->getId() === $state->getId()) { + return $state; + } else { + return $s; + } + }, + $this->statusesByJobId[$workflowId][$state->getJobId()] + ); + } + } + } + + protected function cacheJobStates(string $workflowId, string $jobId, array $states): void + { + $this->statusesByJobId[$workflowId][$jobId] = $states; + foreach ($states as $state) { + $this->statuses[$state->getId()] = $state; + } + } + + protected function cacheLastJobState(string $workflowId, string $jobId, object $state): void + { + $this->lastByJobId[$workflowId][$jobId] = $state; + $this->statuses[$state->getId()] = $state; + } + + public function resetJobStateFromCache(string $workflowId, string $jobId): void + { + foreach ($this->statusesByJobId[$workflowId][$jobId] ?? [] as $jobState) { + unset($this->statuses[$jobState->getId()]); + } + + $this->statusesByJobId[$workflowId][$jobId]=[]; + } + + protected function removeJobStateFromCache(string $workflowId, string $jobStateId): void + { + unset($this->statuses[$jobStateId]); + + if (isset($this->statusesByJobId[$workflowId])) { + foreach ($this->statusesByJobId[$workflowId] as $jobId => $states) { + $this->statusesByJobId[$workflowId][$jobId] = array_filter( + $states, + fn (object $s): bool => $s->getId() !== $jobStateId + ); + } + } + + if (isset($this->lastByJobId[$workflowId])) { + foreach ($this->lastByJobId[$workflowId] as $jobId => $state) { + if ($state->getId() === $jobStateId) { + unset($this->lastByJobId[$workflowId][$jobId]); + } + } + } + } +} diff --git a/lib/php/workflow/src/State/Repository/LockAwareStateRepositoryInterface.php b/lib/php/workflow/src/State/Repository/LockAwareStateRepositoryInterface.php index a7192b2de..ee5d61662 100644 --- a/lib/php/workflow/src/State/Repository/LockAwareStateRepositoryInterface.php +++ b/lib/php/workflow/src/State/Repository/LockAwareStateRepositoryInterface.php @@ -6,7 +6,11 @@ interface LockAwareStateRepositoryInterface extends StateRepositoryInterface { - public function acquireJobLock(string $workflowId, string $jobId): void; + public function acquireJobLock(string $workflowId, string $jobStateId): void; - public function releaseJobLock(string $workflowId, string $jobId): void; + public function releaseJobLock(string $workflowId, string $jobStateId): void; + + public function acquireWorkflowLock(string $workflowId): void; + + public function releaseWorkflowLock(string $workflowId): void; } diff --git a/lib/php/workflow/src/State/Repository/MemoryStateRepository.php b/lib/php/workflow/src/State/Repository/MemoryStateRepository.php index 067524379..d9e69a920 100644 --- a/lib/php/workflow/src/State/Repository/MemoryStateRepository.php +++ b/lib/php/workflow/src/State/Repository/MemoryStateRepository.php @@ -6,18 +6,24 @@ use Alchemy\Workflow\State\JobState; use Alchemy\Workflow\State\WorkflowState; +use function array_filter; class MemoryStateRepository implements StateRepositoryInterface { /** - * @var array + * @var array */ - private array $workflows = []; + protected array $statuses = []; + + /** + * @var array> + */ + protected array $statusesByJobId = []; /** - * @var array + * @var array */ - private array $jobs = []; + private array $workflows = []; public function getWorkflowState(string $id): WorkflowState { @@ -32,14 +38,46 @@ public function persistWorkflowState(WorkflowState $state): void { $id = $state->getId(); $this->workflows[$id] = $state; - $this->jobs[$id] ??= []; + $this->statusesByJobId[$id] ??= []; + } + + public function getJobState(string $workflowId, string $jobStateId): ?JobState + { + $this->ensureWorkflowExists($workflowId); + + return $this->statuses[$jobStateId] ?? null; + } + + public function getLastJobState(string $workflowId, string $jobId): ?JobState + { + $this->ensureWorkflowExists($workflowId); + + $states = $this->statusesByJobId[$workflowId][$jobId] ?? []; + + return end($states) ?: null; } - public function getJobState(string $workflowId, string $jobId): ?JobState + public function createJobState(string $workflowId, string $jobId): JobState + { + $number = count($this->statusesByJobId[$workflowId][$jobId] ?? []); + + $state = new JobState( + $workflowId, + $jobId, + id: sprintf('%s-%d', $jobId, $number), + number: $number, + ); + + $this->persistJobState($state); + + return $state; + } + + public function getJobStates(string $workflowId, string $jobId): array { $this->ensureWorkflowExists($workflowId); - return $this->jobs[$workflowId][$jobId] ?? null; + return $this->statusesByJobId[$workflowId][$jobId] ?? []; } public function persistJobState(JobState $state): void @@ -47,22 +85,51 @@ public function persistJobState(JobState $state): void $workflowId = $state->getWorkflowId(); $this->ensureWorkflowExists($workflowId); - $this->jobs[$workflowId][$state->getJobId()] = $state; + $exists = isset($this->statuses[$state->getId()]); + $this->statuses[$state->getId()] = $state; + $this->statusesByJobId[$workflowId][$state->getJobId()] ??= []; + + if ($exists) { + $this->statusesByJobId[$workflowId][$state->getJobId()] = array_map( + function (object $s) use ($state, &$found): object { + if ($s->getId() === $state->getId()) { + $found = true; + return $state; + } else { + return $s; + } + }, + $this->statusesByJobId[$workflowId][$state->getJobId()] + ); + } else { + $this->statusesByJobId[$workflowId][$state->getJobId()][] = $state; + } } - public function removeJobState(string $workflowId, string $jobId): void + public function removeJobState(string $workflowId, string $jobStateId): void { - unset($this->jobs[$workflowId][$jobId]); + unset($this->statuses[$jobStateId]); + + foreach ($this->statusesByJobId[$workflowId] as $jobId => $states) { + $this->statusesByJobId[$workflowId][$jobId] = array_filter( + $states, + fn(object $s): bool => $s->getId() !== $jobStateId + ); + } } public function resetJobState(string $workflowId, string $jobId): void { - $this->removeJobState($workflowId, $jobId); + foreach ($this->statusesByJobId[$workflowId][$jobId] ?? [] as $jobState) { + unset($this->statuses[$jobState->getId()]); + } + + $this->statusesByJobId[$workflowId][$jobId]=[]; } private function ensureWorkflowExists(string $workflowId): void { - if (!isset($this->jobs[$workflowId])) { + if (!isset($this->statusesByJobId[$workflowId])) { throw new \LogicException(sprintf('Job container for workflow "%s" was not created. Please ensure the WorkflowState is persisted before.', $workflowId)); } } diff --git a/lib/php/workflow/src/State/Repository/StateRepositoryInterface.php b/lib/php/workflow/src/State/Repository/StateRepositoryInterface.php index 8d62b12cd..9a6236e74 100644 --- a/lib/php/workflow/src/State/Repository/StateRepositoryInterface.php +++ b/lib/php/workflow/src/State/Repository/StateRepositoryInterface.php @@ -16,11 +16,17 @@ public function getWorkflowState(string $id): WorkflowState; public function persistWorkflowState(WorkflowState $state): void; - public function getJobState(string $workflowId, string $jobId): ?JobState; + public function getJobState(string $workflowId, string $jobStateId): ?JobState; + + public function getJobStates(string $workflowId, string $jobId): array; + + public function getLastJobState(string $workflowId, string $jobId): ?JobState; public function persistJobState(JobState $state): void; - public function removeJobState(string $workflowId, string $jobId): void; + public function removeJobState(string $workflowId, string $jobStateId): void; public function resetJobState(string $workflowId, string $jobId): void; + + public function createJobState(string $workflowId, string $jobId): JobState; } diff --git a/lib/php/workflow/src/State/Repository/TransactionalStateRepositoryInterface.php b/lib/php/workflow/src/State/Repository/TransactionalStateRepositoryInterface.php new file mode 100644 index 000000000..34e7c0704 --- /dev/null +++ b/lib/php/workflow/src/State/Repository/TransactionalStateRepositoryInterface.php @@ -0,0 +1,8 @@ +startedAt; } - public function getJobState(string $jobId): ?JobState + public function getLastJobState(string $jobId): ?JobState { - return $this->stateRepository->getJobState($this->id, $jobId); + return $this->stateRepository->getLastJobState($this->id, $jobId); } public function getDuration(): ?float diff --git a/lib/php/workflow/src/Trigger/JobTrigger.php b/lib/php/workflow/src/Trigger/JobTrigger.php new file mode 100644 index 000000000..660c4d8ca --- /dev/null +++ b/lib/php/workflow/src/Trigger/JobTrigger.php @@ -0,0 +1,29 @@ +workflowId; + } + + public function getJobId(): string + { + return $this->jobId; + } + + public function getJobStateId(): string + { + return $this->jobStateId; + } +} diff --git a/lib/php/workflow/src/Trigger/JobTriggerInterface.php b/lib/php/workflow/src/Trigger/JobTriggerInterface.php index 364e1ceac..a061d5423 100644 --- a/lib/php/workflow/src/Trigger/JobTriggerInterface.php +++ b/lib/php/workflow/src/Trigger/JobTriggerInterface.php @@ -6,8 +6,12 @@ interface JobTriggerInterface { + public function triggerJob(JobTrigger $jobTrigger): void; + /** * @return bool whether workflow should continue in the process */ - public function triggerJob(string $workflowId, string $jobId): bool; + public function shouldContinue(): bool; + + public function isSynchronous(): bool; } diff --git a/lib/php/workflow/src/Trigger/MessengerJobTrigger.php b/lib/php/workflow/src/Trigger/MessengerJobTrigger.php index 59bc1c003..1fc9732a4 100644 --- a/lib/php/workflow/src/Trigger/MessengerJobTrigger.php +++ b/lib/php/workflow/src/Trigger/MessengerJobTrigger.php @@ -9,14 +9,24 @@ final readonly class MessengerJobTrigger implements JobTriggerInterface { - public function __construct(private MessageBusInterface $bus) + public function __construct( + private MessageBusInterface $bus + ) { } - public function triggerJob(string $workflowId, string $jobId): bool + public function triggerJob(JobTrigger $jobTrigger): void { - $this->bus->dispatch(new JobConsumer($workflowId, $jobId)); + $this->bus->dispatch(new JobConsumer($jobTrigger->getWorkflowId(), $jobTrigger->getJobId(), $jobTrigger->getJobStateId())); + } + public function shouldContinue(): bool + { return true; } + + public function isSynchronous(): bool + { + return false; + } } diff --git a/lib/php/workflow/src/Trigger/RuntimeJobTrigger.php b/lib/php/workflow/src/Trigger/RuntimeJobTrigger.php index 350576b44..265bf453c 100644 --- a/lib/php/workflow/src/Trigger/RuntimeJobTrigger.php +++ b/lib/php/workflow/src/Trigger/RuntimeJobTrigger.php @@ -12,10 +12,18 @@ public function __construct(private RunnerInterface $runner) { } - public function triggerJob(string $workflowId, string $jobId): bool + public function triggerJob(JobTrigger $jobTrigger): void { - $this->runner->run($workflowId, $jobId); + $this->runner->run($jobTrigger); + } + + public function shouldContinue(): bool + { + return true; + } + public function isSynchronous(): bool + { return true; } } diff --git a/lib/php/workflow/src/Trigger/SymfonyConsoleJobTrigger.php b/lib/php/workflow/src/Trigger/SymfonyConsoleJobTrigger.php index ce6d20495..00fcb072e 100644 --- a/lib/php/workflow/src/Trigger/SymfonyConsoleJobTrigger.php +++ b/lib/php/workflow/src/Trigger/SymfonyConsoleJobTrigger.php @@ -6,10 +6,18 @@ class SymfonyConsoleJobTrigger implements JobTriggerInterface { - public function triggerJob(string $workflowId, string $jobId): bool + public function triggerJob(JobTrigger $jobTrigger): void { - exec(sprintf('bin/console workflow:run "%s"', escapeshellarg($jobId))); + exec(sprintf('bin/console workflow:run "%s"', escapeshellarg($jobTrigger->getJobStateId()))); + } + public function shouldContinue(): bool + { return false; } + + public function isSynchronous(): bool + { + return true; + } } diff --git a/lib/php/workflow/src/WorkflowOrchestrator.php b/lib/php/workflow/src/WorkflowOrchestrator.php index da5383285..2529dbbde 100644 --- a/lib/php/workflow/src/WorkflowOrchestrator.php +++ b/lib/php/workflow/src/WorkflowOrchestrator.php @@ -15,17 +15,24 @@ use Alchemy\Workflow\State\JobState; use Alchemy\Workflow\State\Repository\LockAwareStateRepositoryInterface; use Alchemy\Workflow\State\Repository\StateRepositoryInterface; +use Alchemy\Workflow\State\Repository\TransactionalStateRepositoryInterface; use Alchemy\Workflow\State\WorkflowState; +use Alchemy\Workflow\Trigger\JobTrigger; use Alchemy\Workflow\Trigger\JobTriggerInterface; use Alchemy\Workflow\Validator\EventValidatorInterface; -readonly class WorkflowOrchestrator +final class WorkflowOrchestrator { + /** + * @var JobTrigger[] + */ + private array $workflowsToTrigger = []; + public function __construct( - private WorkflowRepositoryInterface $workflowRepository, - private StateRepositoryInterface $stateRepository, - private JobTriggerInterface $trigger, - private EventValidatorInterface $eventValidator, + private readonly WorkflowRepositoryInterface $workflowRepository, + private readonly StateRepositoryInterface $stateRepository, + private readonly JobTriggerInterface $trigger, + private readonly EventValidatorInterface $eventValidator, ) { } @@ -71,35 +78,37 @@ public function startWorkflow(string $workflowName, ?WorkflowEvent $event = null $this->stateRepository->persistWorkflowState($workflowState); - $this->continueWorkflow($workflowState->getId(), $workflowState); + $this->doContinueWorkflow($workflowState); + + $this->flush(); return $workflowState; } public function cancelWorkflow(string $workflowId): void { - $workflowState = $this->stateRepository->getWorkflowState($workflowId); + $this->wrapInTransaction(function () use ($workflowId): void { + if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { + $this->stateRepository->acquireWorkflowLock($workflowId); + } - if (null !== $workflowState->getEndedAt()) { - return; - } + $workflowState = $this->stateRepository->getWorkflowState($workflowId); - if (WorkflowState::STATUS_FAILURE === $workflowState->getStatus()) { - return; - } + if (null !== $workflowState->getEndedAt()) { + return; + } - $workflowState->setCancelledAt(new MicroDateTime()); - $workflowState->setStatus(WorkflowState::STATUS_CANCELLED); + if (WorkflowState::STATUS_FAILURE === $workflowState->getStatus()) { + return; + } - $workflow = $this->loadWorkflowByName($workflowState->getWorkflowName()); + $workflowState->setCancelledAt(new MicroDateTime()); + $workflowState->setStatus(WorkflowState::STATUS_CANCELLED); - foreach ($workflow->getJobIds() as $jobId) { - if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { - $this->stateRepository->acquireJobLock($workflowId, $jobId); - } + $workflow = $this->loadWorkflowByName($workflowState->getWorkflowName()); - try { - $jobState = $this->stateRepository->getJobState($workflowId, $jobId); + foreach ($workflow->getJobIds() as $jobId) { + $jobState = $this->stateRepository->getLastJobState($workflowId, $jobId); if (null !== $jobState && in_array($jobState->getStatus(), [ JobState::STATUS_TRIGGERED, JobState::STATUS_RUNNING, @@ -107,34 +116,61 @@ public function cancelWorkflow(string $workflowId): void $jobState->setStatus(JobState::STATUS_CANCELLED); $this->stateRepository->persistJobState($jobState); } - } finally { - if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { - $this->stateRepository->releaseJobLock($workflowId, $jobId); - } } - } - $this->stateRepository->persistWorkflowState($workflowState); + $this->stateRepository->persistWorkflowState($workflowState); + + if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { + $this->stateRepository->releaseWorkflowLock($workflowId); + } + }); } - public function continueWorkflow(string $workflowId, ?WorkflowState $workflowState = null): void + public function continueWorkflow(string $workflowId): void { - if (null === $workflowState) { + $this->wrapInTransaction(function () use ($workflowId): void { + if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { + $this->stateRepository->acquireWorkflowLock($workflowId); + } + $workflowState = $this->stateRepository->getWorkflowState($workflowId); - } + if ($workflowState->isCancelled()) { + return; + } + + $this->doContinueWorkflow($workflowState); + + if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { + $this->stateRepository->releaseWorkflowLock($workflowId); + } + }); + + $this->flush(); + } + + private function wrapInTransaction(callable $callback): void + { + if ($this->stateRepository instanceof TransactionalStateRepositoryInterface) { + $this->stateRepository->transactional($callback); - if ($workflowState->isCancelled()) { return; } + $callback(); + } + + private function doContinueWorkflow(WorkflowState $workflowState): void + { $event = $workflowState->getEvent(); $planner = new WorkflowPlanner([$this->loadWorkflowByName($workflowState->getWorkflowName())]); $plan = null === $event ? $planner->planAll() : $planner->planEvent($event); [$nextJobId, $workflowEndStatus] = $this->getNextJob($plan, $workflowState); if (null !== $nextJobId) { + $continue = $this->trigger->shouldContinue(); + do { - $continue = $this->triggerJob($workflowState, $nextJobId); + $this->triggerJob($workflowState, $nextJobId); if ($continue) { [$nextJobId, $workflowEndStatus] = $this->getNextJob($plan, $workflowState); @@ -173,6 +209,19 @@ public function continueJob(string $workflowId, string $jobId, ?array $jobInputs public function rerunJobs(string $workflowId, ?string $jobIdFilter = null, ?array $expectedStatuses = null, ?array $jobInputs = null): void { + $this->wrapInTransaction(function () use ($workflowId, $jobIdFilter, $expectedStatuses, $jobInputs): void { + $this->doRerunJobs($workflowId, $jobIdFilter, $expectedStatuses, $jobInputs); + }); + + $this->flush(); + } + + private function doRerunJobs(string $workflowId, ?string $jobIdFilter, ?array $expectedStatuses, ?array $jobInputs): void + { + if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { + $this->stateRepository->acquireWorkflowLock($workflowId); + } + $workflowState = $this->stateRepository->getWorkflowState($workflowId); $event = $workflowState->getEvent(); @@ -190,22 +239,10 @@ public function rerunJobs(string $workflowId, ?string $jobIdFilter = null, ?arra continue; } - if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { - $this->stateRepository->acquireJobLock($workflowId, $jobId); - } - - try { - $jobState = $this->stateRepository->getJobState($workflowId, $jobId); - if (null !== $jobState && (null === $expectedStatuses || in_array($jobState->getStatus(), $expectedStatuses, true))) { - $this->stateRepository->resetJobState($workflowId, $jobId); - - if (!$run->getJob()->isDisabled()) { - $jobsToTrigger[] = $jobId; - } - } - } finally { - if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { - $this->stateRepository->releaseJobLock($workflowId, $jobId); + $jobState = $this->stateRepository->getLastJobState($workflowId, $jobId); + if (null !== $jobState && (null === $expectedStatuses || in_array($jobState->getStatus(), $expectedStatuses, true))) { + if (!$run->getJob()->isDisabled()) { + $jobsToTrigger[] = $jobId; } } @@ -224,6 +261,10 @@ public function rerunJobs(string $workflowId, ?string $jobIdFilter = null, ?arra $this->triggerJob($workflowState, $jobId, $jobInputs); } } + + if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { + $this->stateRepository->releaseWorkflowLock($workflowId); + } } private function loadWorkflowByName(string $name): Workflow @@ -256,7 +297,7 @@ private function getNextJob(Plan $plan, WorkflowState $state): array continue; } - $jobState = $this->stateRepository->getJobState($state->getId(), $jobId); + $jobState = $this->stateRepository->getLastJobState($state->getId(), $jobId); if (null === $jobState) { if ($this->satisfiesAllNeeds($statuses, $job)) { return [$jobId, null]; @@ -302,25 +343,31 @@ private function satisfiesAllNeeds(array $states, Job $job): bool return true; } - private function triggerJob(WorkflowState $state, string $jobId, ?array $jobInputs = null): bool + private function triggerJob(WorkflowState $workflowState, string $jobId, ?array $jobInputs = null): void { - $workflowId = $state->getId(); - - if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { - $this->stateRepository->acquireJobLock($workflowId, $jobId); - } + $workflowId = $workflowState->getId(); - $jobState = new JobState($workflowId, $jobId, JobState::STATUS_TRIGGERED); + $jobState = $this->stateRepository->createJobState($workflowId, $jobId); if (null !== $jobInputs) { $inputs = $jobState->getInputs() ?? new Inputs(); $jobState->setInputs($inputs->mergeWith($jobInputs)); } $this->stateRepository->persistJobState($jobState); - if ($this->stateRepository instanceof LockAwareStateRepositoryInterface) { - $this->stateRepository->releaseJobLock($workflowId, $jobId); + $jobTrigger = new JobTrigger($workflowId, $jobId, $jobState->getId()); + if ($this->trigger->isSynchronous()) { + $this->trigger->triggerJob($jobTrigger); + } else { + $this->workflowsToTrigger[] = $jobTrigger; } + } - return $this->trigger->triggerJob($state->getId(), $jobId); + private function flush(): void + { + $jobsToTrigger = $this->workflowsToTrigger; + $this->workflowsToTrigger = []; + foreach ($jobsToTrigger as $jobTrigger) { + $this->trigger->triggerJob($jobTrigger); + } } } diff --git a/lib/php/workflow/tests/AbstractWorkflowTest.php b/lib/php/workflow/tests/AbstractWorkflowTest.php index ac16d5745..4dd349076 100644 --- a/lib/php/workflow/tests/AbstractWorkflowTest.php +++ b/lib/php/workflow/tests/AbstractWorkflowTest.php @@ -85,9 +85,9 @@ protected function assertJobResultsStates(array $expected, StateRepositoryInterf { foreach ($expected as $jobId => $result) { if (null === $result) { - $this->assertNull($repository->getJobState($workflowId, $jobId)); + $this->assertNull($repository->getLastJobState($workflowId, $jobId)); } else { - $this->assertEquals($result, $repository->getJobState($workflowId, $jobId)->getStatus(), $jobId); + $this->assertEquals($result, $repository->getLastJobState($workflowId, $jobId)->getStatus(), $jobId); } } } diff --git a/lib/php/workflow/tests/Dumper/AbstractDumperTest.php b/lib/php/workflow/tests/Dumper/AbstractDumperTest.php index f6fad5a2f..010e898c7 100644 --- a/lib/php/workflow/tests/Dumper/AbstractDumperTest.php +++ b/lib/php/workflow/tests/Dumper/AbstractDumperTest.php @@ -15,24 +15,24 @@ abstract class AbstractDumperTest extends AbstractWorkflowTest { protected function createWorkflowState(string $workflowId): WorkflowState { - $intro = new JobState($workflowId, 'intro', JobState::STATUS_SUCCESS); + $intro = new JobState($workflowId, 'intro', JobState::STATUS_SUCCESS, id: 'intro-0'); $intro->setStartedAt(new MicroDateTime('2000-05-12T12:12:42', 424242)); $intro->setEndedAt(new MicroDateTime('2000-05-12T12:12:43', 424242)); - $content = new JobState($workflowId, 'content', JobState::STATUS_RUNNING); + $content = new JobState($workflowId, 'content', JobState::STATUS_RUNNING, id: 'content-0'); $content->setStartedAt(new MicroDateTime('2000-05-12T12:12:44', 424242)); $content->setInputs(new Inputs([ 'foo' => 'bar', 'baz' => 42, ])); - $contentBis = new JobState($workflowId, 'content_bis', JobState::STATUS_RUNNING); + $contentBis = new JobState($workflowId, 'content_bis', JobState::STATUS_RUNNING, id: 'content_bis-0'); $contentBis->setStartedAt(new MicroDateTime('2000-05-12T12:12:44', 424242)); $contentBis->setInputs(new Inputs()); $jobMap = [ [$workflowId, 'intro', $intro], - [$workflowId, 'never-called', new JobState($workflowId, 'never-called', JobState::STATUS_SKIPPED)], + [$workflowId, 'never-called', new JobState($workflowId, 'never-called', JobState::STATUS_SKIPPED, id: 'never-called-0')], [$workflowId, 'content', $content], [$workflowId, 'content_bis', $contentBis], [$workflowId, 'outro', null], @@ -41,7 +41,7 @@ protected function createWorkflowState(string $workflowId): WorkflowState $stateRepository = $this->createMock(StateRepositoryInterface::class); $stateRepository ->expects($this->exactly(count($jobMap))) - ->method('getJobState') + ->method('getLastJobState') ->will($this->returnValueMap($jobMap)); return new WorkflowState($stateRepository, 'foo', null, $workflowId); diff --git a/lib/php/workflow/tests/Dumper/JsonDumperTest.php b/lib/php/workflow/tests/Dumper/JsonDumperTest.php index b71bb2061..868f153a7 100644 --- a/lib/php/workflow/tests/Dumper/JsonDumperTest.php +++ b/lib/php/workflow/tests/Dumper/JsonDumperTest.php @@ -36,7 +36,7 @@ public function testJsonDumper(): void 'stage' => 1, 'jobs' => [ [ - 'id' => 'intro', + 'jobId' => 'intro', 'name' => 'intro', 'status' => JobState::STATUS_SUCCESS, 'startedAt' => '2000-05-12T12:12:42.424242+00:00', @@ -47,9 +47,11 @@ public function testJsonDumper(): void 'needs' => [], 'with' => [], 'disabled' => false, - ], + 'stateId' => 'intro-0', + 'number' => 0, + ], [ - 'id' => 'never-called', + 'jobId' => 'never-called', 'name' => 'never-called', 'status' => JobState::STATUS_SKIPPED, 'outputs' => [], @@ -59,6 +61,8 @@ public function testJsonDumper(): void 'if' => 'env.WF_TEST == "bar"', 'with' => [], 'disabled' => false, + 'stateId' => 'never-called-0', + 'number' => 0, ], ], ], @@ -66,7 +70,7 @@ public function testJsonDumper(): void 'stage' => 2, 'jobs' => [ [ - 'id' => 'content', + 'jobId' => 'content', 'name' => 'content', 'status' => JobState::STATUS_RUNNING, 'startedAt' => '2000-05-12T12:12:44.424242+00:00', @@ -82,9 +86,11 @@ public function testJsonDumper(): void ], 'with' => [], 'disabled' => false, + 'stateId' => 'content-0', + 'number' => 0, ], [ - 'id' => 'content_bis', + 'jobId' => 'content_bis', 'name' => 'content_bis', 'status' => JobState::STATUS_RUNNING, 'startedAt' => '2000-05-12T12:12:44.424242+00:00', @@ -99,6 +105,8 @@ public function testJsonDumper(): void 'foo' => 'bar', ], 'disabled' => false, + 'stateId' => 'content_bis-0', + 'number' => 0, ], ], ], @@ -106,7 +114,7 @@ public function testJsonDumper(): void 'stage' => 3, 'jobs' => [ [ - 'id' => 'outro', + 'jobId' => 'outro', 'name' => 'outro', 'needs' => [ 'content', diff --git a/lib/php/workflow/tests/OrchestratorTest.php b/lib/php/workflow/tests/OrchestratorTest.php index c99e417b9..0de34b2fa 100644 --- a/lib/php/workflow/tests/OrchestratorTest.php +++ b/lib/php/workflow/tests/OrchestratorTest.php @@ -92,7 +92,7 @@ public function testEndToEndEchoerComplexWorkflow(): void 'outro' => JobState::STATUS_SUCCESS, ], $stateRepository, $workflowState->getId()); - $contentBisState = $stateRepository->getJobState($workflowState->getId(), 'content_bis'); + $contentBisState = $stateRepository->getLastJobState($workflowState->getId(), 'content_bis'); $this->assertEquals([ 'foo' => 'bar', 'duration' => $contentBisState->getSteps()['first']->getDuration(), diff --git a/lib/php/workflow/tests/State/StateRepositoryTest.php b/lib/php/workflow/tests/State/StateRepositoryTest.php index 283678fca..ed92f6234 100644 --- a/lib/php/workflow/tests/State/StateRepositoryTest.php +++ b/lib/php/workflow/tests/State/StateRepositoryTest.php @@ -34,79 +34,84 @@ public function testStateAreCorrectlyPersistedForSuccessWorkflow(StateRepository $this->assertEquals([ ['persistWorkflowState', $workflowId, WorkflowState::STATUS_STARTED], - ['getJobState', $workflowId, 'intro'], - ['acquireJobLock', $workflowId, 'intro'], + ['getLastJobState', $workflowId, 'intro'], + ['createJobState', $workflowId, 'intro'], ['persistJobState', $workflowId, 'intro', JobState::STATUS_TRIGGERED], - ['releaseJobLock', $workflowId, 'intro'], ['getWorkflowState', $workflowId], - ['acquireJobLock', $workflowId, 'intro'], - ['getJobState', $workflowId, 'intro'], + ['beginTransaction'], + ['acquireJobLock', $workflowId, 'intro-0'], + ['getJobState', $workflowId, 'intro-0'], ['persistJobState', $workflowId, 'intro', JobState::STATUS_RUNNING], - ['releaseJobLock', $workflowId, 'intro'], + ['releaseJobLock', $workflowId, 'intro-0'], + ['endTransaction'], ['persistJobState', $workflowId, 'intro', JobState::STATUS_SUCCESS], - ['getJobState', $workflowId, 'intro'], - ['getJobState', $workflowId, 'never-called'], + ['getLastJobState', $workflowId, 'intro'], + ['getLastJobState', $workflowId, 'never-called'], - ['acquireJobLock', $workflowId, 'never-called'], + ['createJobState', $workflowId, 'never-called'], ['persistJobState', $workflowId, 'never-called', JobState::STATUS_TRIGGERED], - ['releaseJobLock', $workflowId, 'never-called'], ['getWorkflowState', $workflowId], - ['acquireJobLock', $workflowId, 'never-called'], - ['getJobState', $workflowId, 'never-called'], + ['beginTransaction'], + ['acquireJobLock', $workflowId, 'never-called-0'], + ['getJobState', $workflowId, 'never-called-0'], ['persistJobState', $workflowId, 'never-called', JobState::STATUS_SKIPPED], - ['releaseJobLock', $workflowId, 'never-called'], + ['releaseJobLock', $workflowId, 'never-called-0'], + ['endTransaction'], - ['getJobState', $workflowId, 'intro'], - ['getJobState', $workflowId, 'never-called'], - ['getJobState', $workflowId, 'content'], + ['getLastJobState', $workflowId, 'intro'], + ['getLastJobState', $workflowId, 'never-called'], + ['getLastJobState', $workflowId, 'content'], - ['acquireJobLock', $workflowId, 'content'], + ['createJobState', $workflowId, 'content'], ['persistJobState', $workflowId, 'content', JobState::STATUS_TRIGGERED], - ['releaseJobLock', $workflowId, 'content'], ['getWorkflowState', $workflowId], - ['acquireJobLock', $workflowId, 'content'], - ['getJobState', $workflowId, 'content'], + ['beginTransaction'], + ['acquireJobLock', $workflowId, 'content-0'], + ['getJobState', $workflowId, 'content-0'], ['persistJobState', $workflowId, 'content', JobState::STATUS_RUNNING], - ['releaseJobLock', $workflowId, 'content'], + ['releaseJobLock', $workflowId, 'content-0'], + ['endTransaction'], ['persistJobState', $workflowId, 'content', JobState::STATUS_SUCCESS], - ['getJobState', $workflowId, 'intro'], - ['getJobState', $workflowId, 'never-called'], - ['getJobState', $workflowId, 'content'], - ['getJobState', $workflowId, 'content_bis'], + ['getLastJobState', $workflowId, 'intro'], + ['getLastJobState', $workflowId, 'never-called'], + ['getLastJobState', $workflowId, 'content'], + ['getLastJobState', $workflowId, 'content_bis'], - ['acquireJobLock', $workflowId, 'content_bis'], + ['createJobState', $workflowId, 'content_bis'], ['persistJobState', $workflowId, 'content_bis', JobState::STATUS_TRIGGERED], - ['releaseJobLock', $workflowId, 'content_bis'], ['getWorkflowState', $workflowId], - ['acquireJobLock', $workflowId, 'content_bis'], - ['getJobState', $workflowId, 'content_bis'], + ['beginTransaction'], + ['acquireJobLock', $workflowId, 'content_bis-0'], + ['getJobState', $workflowId, 'content_bis-0'], ['persistJobState', $workflowId, 'content_bis', JobState::STATUS_RUNNING], - ['releaseJobLock', $workflowId, 'content_bis'], + ['releaseJobLock', $workflowId, 'content_bis-0'], + ['endTransaction'], ['persistJobState', $workflowId, 'content_bis', JobState::STATUS_SUCCESS], - ['getJobState', $workflowId, 'intro'], - ['getJobState', $workflowId, 'never-called'], - ['getJobState', $workflowId, 'content'], - ['getJobState', $workflowId, 'content_bis'], - ['getJobState', $workflowId, 'outro'], + ['getLastJobState', $workflowId, 'intro'], + ['getLastJobState', $workflowId, 'never-called'], + ['getLastJobState', $workflowId, 'content'], + ['getLastJobState', $workflowId, 'content_bis'], + ['getLastJobState', $workflowId, 'outro'], - ['acquireJobLock', $workflowId, 'outro'], + ['createJobState', $workflowId, 'outro'], ['persistJobState', $workflowId, 'outro', JobState::STATUS_TRIGGERED], - ['releaseJobLock', $workflowId, 'outro'], ['getWorkflowState', $workflowId], - ['acquireJobLock', $workflowId, 'outro'], - ['getJobState', $workflowId, 'outro'], + ['beginTransaction'], + ['acquireJobLock', $workflowId, 'outro-0'], + ['getJobState', $workflowId, 'outro-0'], ['persistJobState', $workflowId, 'outro', JobState::STATUS_RUNNING], - ['releaseJobLock', $workflowId, 'outro'], + ['releaseJobLock', $workflowId, 'outro-0'], + ['endTransaction'], ['persistJobState', $workflowId, 'outro', JobState::STATUS_SUCCESS], - ['getJobState', $workflowId, 'intro'], - ['getJobState', $workflowId, 'never-called'], - ['getJobState', $workflowId, 'content'], - ['getJobState', $workflowId, 'content_bis'], - ['getJobState', $workflowId, 'outro'], + ['getLastJobState', $workflowId, 'intro'], + ['getLastJobState', $workflowId, 'never-called'], + ['getLastJobState', $workflowId, 'content'], + ['getLastJobState', $workflowId, 'content_bis'], + ['getLastJobState', $workflowId, 'outro'], ['persistWorkflowState', $workflowId, WorkflowState::STATUS_SUCCESS], ], $testStateRepositoryDecorator->getLogs()); @@ -132,46 +137,49 @@ public function testStateAreCorrectlyPersistedForFailJob(StateRepositoryInterfac $this->assertEquals([ ['persistWorkflowState', $workflowId, WorkflowState::STATUS_STARTED], - ['getJobState', $workflowId, 'intro'], - ['acquireJobLock', $workflowId, 'intro'], + ['getLastJobState', $workflowId, 'intro'], + ['createJobState', $workflowId, 'intro'], ['persistJobState', $workflowId, 'intro', JobState::STATUS_TRIGGERED], - ['releaseJobLock', $workflowId, 'intro'], ['getWorkflowState', $workflowId], - ['acquireJobLock', $workflowId, 'intro'], - ['getJobState', $workflowId, 'intro'], + ['beginTransaction'], + ['acquireJobLock', $workflowId, 'intro-0'], + ['getJobState', $workflowId, 'intro-0'], ['persistJobState', $workflowId, 'intro', JobState::STATUS_RUNNING], - ['releaseJobLock', $workflowId, 'intro'], + ['releaseJobLock', $workflowId, 'intro-0'], + ['endTransaction'], ['persistJobState', $workflowId, 'intro', JobState::STATUS_SUCCESS], - ['getJobState', $workflowId, 'intro'], - ['getJobState', $workflowId, 'never-called'], + ['getLastJobState', $workflowId, 'intro'], + ['getLastJobState', $workflowId, 'never-called'], - ['acquireJobLock', $workflowId, 'never-called'], + ['createJobState', $workflowId, 'never-called'], ['persistJobState', $workflowId, 'never-called', JobState::STATUS_TRIGGERED], - ['releaseJobLock', $workflowId, 'never-called'], ['getWorkflowState', $workflowId], - ['acquireJobLock', $workflowId, 'never-called'], - ['getJobState', $workflowId, 'never-called'], + ['beginTransaction'], + ['acquireJobLock', $workflowId, 'never-called-0'], + ['getJobState', $workflowId, 'never-called-0'], // 16 ['persistJobState', $workflowId, 'never-called', JobState::STATUS_SKIPPED], - ['releaseJobLock', $workflowId, 'never-called'], + ['releaseJobLock', $workflowId, 'never-called-0'], + ['endTransaction'], - ['getJobState', $workflowId, 'intro'], - ['getJobState', $workflowId, 'never-called'], - ['getJobState', $workflowId, 'content'], + ['getLastJobState', $workflowId, 'intro'], + ['getLastJobState', $workflowId, 'never-called'], + ['getLastJobState', $workflowId, 'content'], - ['acquireJobLock', $workflowId, 'content'], + ['createJobState', $workflowId, 'content'], ['persistJobState', $workflowId, 'content', JobState::STATUS_TRIGGERED], - ['releaseJobLock', $workflowId, 'content'], ['getWorkflowState', $workflowId], - ['acquireJobLock', $workflowId, 'content'], - ['getJobState', $workflowId, 'content'], + ['beginTransaction'], + ['acquireJobLock', $workflowId, 'content-0'], + ['getJobState', $workflowId, 'content-0'], ['persistJobState', $workflowId, 'content', JobState::STATUS_RUNNING], - ['releaseJobLock', $workflowId, 'content'], + ['releaseJobLock', $workflowId, 'content-0'], + ['endTransaction'], ['persistJobState', $workflowId, 'content', JobState::STATUS_FAILURE], - ['getJobState', $workflowId, 'intro'], - ['getJobState', $workflowId, 'never-called'], - ['getJobState', $workflowId, 'content'], + ['getLastJobState', $workflowId, 'intro'], + ['getLastJobState', $workflowId, 'never-called'], + ['getLastJobState', $workflowId, 'content'], ['persistWorkflowState', $workflowId, WorkflowState::STATUS_FAILURE], ], $testStateRepositoryDecorator->getLogs()); diff --git a/lib/php/workflow/tests/State/TestStateStateRepository.php b/lib/php/workflow/tests/State/TestStateStateRepository.php index 2f5535395..377a5a6bd 100644 --- a/lib/php/workflow/tests/State/TestStateStateRepository.php +++ b/lib/php/workflow/tests/State/TestStateStateRepository.php @@ -7,9 +7,10 @@ use Alchemy\Workflow\State\JobState; use Alchemy\Workflow\State\Repository\LockAwareStateRepositoryInterface; use Alchemy\Workflow\State\Repository\StateRepositoryInterface; +use Alchemy\Workflow\State\Repository\TransactionalStateRepositoryInterface; use Alchemy\Workflow\State\WorkflowState; -class TestStateStateRepository implements LockAwareStateRepositoryInterface +class TestStateStateRepository implements LockAwareStateRepositoryInterface, TransactionalStateRepositoryInterface { private array $logs = []; @@ -31,11 +32,32 @@ public function persistWorkflowState(WorkflowState $state): void $this->inner->persistWorkflowState($state); } - public function getJobState(string $workflowId, string $jobId): ?JobState + public function getJobState(string $workflowId, string $jobStateId): ?JobState + { + $this->logs[] = ['getJobState', $workflowId, $jobStateId]; + + return $this->inner->getJobState($workflowId, $jobStateId); + } + + public function getLastJobState(string $workflowId, string $jobId): ?JobState + { + $this->logs[] = ['getLastJobState', $workflowId, $jobId]; + + return $this->inner->getLastJobState($workflowId, $jobId); + } + + public function createJobState(string $workflowId, string $jobId): JobState + { + $this->logs[] = ['createJobState', $workflowId, $jobId]; + + return $this->inner->createJobState($workflowId, $jobId); + } + + public function getJobStates(string $workflowId, string $jobId): array { $this->logs[] = ['getJobState', $workflowId, $jobId]; - return $this->inner->getJobState($workflowId, $jobId); + return $this->inner->getJobStates($workflowId, $jobId); } public function persistJobState(JobState $state): void @@ -45,11 +67,20 @@ public function persistJobState(JobState $state): void $this->inner->persistJobState($state); } - public function removeJobState(string $workflowId, string $jobId): void + public function removeJobState(string $workflowId, string $jobStateId): void { - $this->logs[] = ['removeJobState', $workflowId, $jobId]; + $this->logs[] = ['removeJobState', $workflowId, $jobStateId]; - $this->inner->removeJobState($workflowId, $jobId); + $this->inner->removeJobState($workflowId, $jobStateId); + } + + public function acquireJobLock(string $workflowId, string $jobStateId): void + { + $this->logs[] = ['acquireJobLock', $workflowId, $jobStateId]; + + if ($this->inner instanceof LockAwareStateRepositoryInterface) { + $this->inner->acquireJobLock($workflowId, $jobStateId); + } } public function resetJobState(string $workflowId, string $jobId): void @@ -57,24 +88,48 @@ public function resetJobState(string $workflowId, string $jobId): void $this->removeJobState($workflowId, $jobId); } - public function acquireJobLock(string $workflowId, string $jobId): void + public function releaseJobLock(string $workflowId, string $jobStateId): void + { + $this->logs[] = ['releaseJobLock', $workflowId, $jobStateId]; + + if ($this->inner instanceof LockAwareStateRepositoryInterface) { + $this->inner->releaseJobLock($workflowId, $jobStateId); + } + } + + public function acquireWorkflowLock(string $workflowId): void { - $this->logs[] = ['acquireJobLock', $workflowId, $jobId]; + $this->logs[] = ['acquireWorkflowLock', $workflowId]; if ($this->inner instanceof LockAwareStateRepositoryInterface) { - $this->inner->acquireJobLock($workflowId, $jobId); + $this->inner->acquireWorkflowLock($workflowId); } } - public function releaseJobLock(string $workflowId, string $jobId): void + public function releaseWorkflowLock(string $workflowId): void { - $this->logs[] = ['releaseJobLock', $workflowId, $jobId]; + $this->logs[] = ['releaseWorkflowLock', $workflowId]; if ($this->inner instanceof LockAwareStateRepositoryInterface) { - $this->inner->releaseJobLock($workflowId, $jobId); + $this->inner->releaseWorkflowLock($workflowId); } } + public function transactional(callable $callback) + { + $this->logs[] = ['beginTransaction']; + + if ($this->inner instanceof TransactionalStateRepositoryInterface) { + $response = $this->inner->transactional($callback); + } else { + $response = $callback(); + } + + $this->logs[] = ['endTransaction']; + + return $response; + } + public function getLogs(): array { return $this->logs;