From 28dfc9cff24e27c8234a36af28b58be09e3b418c Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Fri, 11 Jul 2025 04:05:49 +0000 Subject: [PATCH 01/19] Continue as new --- src/ContinuedWorkflow.php | 9 ++++++ src/Models/StoredWorkflow.php | 33 ++++++++++++++++++++ src/States/WorkflowContinuedStatus.php | 10 ++++++ src/States/WorkflowStatus.php | 1 + src/Traits/Continues.php | 32 +++++++++++++++++++ src/Workflow.php | 6 ++++ src/WorkflowStub.php | 33 +++++++++++++------- tests/Feature/ContinueAsNewWorkflowTest.php | 30 ++++++++++++++++++ tests/Fixtures/TestContinueAsNewWorkflow.php | 20 ++++++++++++ tests/Fixtures/TestWorkflow.php | 4 --- tests/TestCase.php | 2 ++ tests/Unit/States/WorkflowStatusTest.php | 2 ++ 12 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 src/ContinuedWorkflow.php create mode 100644 src/States/WorkflowContinuedStatus.php create mode 100644 src/Traits/Continues.php create mode 100644 tests/Feature/ContinueAsNewWorkflowTest.php create mode 100644 tests/Fixtures/TestContinueAsNewWorkflow.php diff --git a/src/ContinuedWorkflow.php b/src/ContinuedWorkflow.php new file mode 100644 index 0000000..849d657 --- /dev/null +++ b/src/ContinuedWorkflow.php @@ -0,0 +1,9 @@ +withPivot(['parent_index', 'parent_now']); } + public function continuedWorkflows(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + { + return $this->belongsToMany( + config('workflows.stored_workflow_model', self::class), + config('workflows.workflow_relationships_table', 'workflow_relationships'), + 'parent_workflow_id', + 'child_workflow_id' + )->wherePivot('parent_index', -1) + ->withPivot(['parent_index', 'parent_now']); + } + + public function root(): self + { + $root = $this; + + while ($parent = $root->parents()->wherePivot('parent_index', -1)->first()) { + $root = $parent; + } + + return $root; + } + + public function active(): self + { + $active = $this->root(); + + while ($next = $active->continuedWorkflows()->first()) { + $active = $next; + } + + return $active; + } + public function prunable(): Builder { return static::where('status', 'completed') diff --git a/src/States/WorkflowContinuedStatus.php b/src/States/WorkflowContinuedStatus.php new file mode 100644 index 0000000..99f7b8b --- /dev/null +++ b/src/States/WorkflowContinuedStatus.php @@ -0,0 +1,10 @@ +allowTransition(WorkflowPendingStatus::class, WorkflowFailedStatus::class) ->allowTransition(WorkflowPendingStatus::class, WorkflowRunningStatus::class) ->allowTransition(WorkflowRunningStatus::class, WorkflowCompletedStatus::class) + ->allowTransition(WorkflowRunningStatus::class, WorkflowContinuedStatus::class) ->allowTransition(WorkflowRunningStatus::class, WorkflowFailedStatus::class) ->allowTransition(WorkflowRunningStatus::class, WorkflowWaitingStatus::class) ->allowTransition(WorkflowWaitingStatus::class, WorkflowFailedStatus::class) diff --git a/src/Traits/Continues.php b/src/Traits/Continues.php new file mode 100644 index 0000000..5eb597a --- /dev/null +++ b/src/Traits/Continues.php @@ -0,0 +1,32 @@ +replaying) { + $newWorkflow = self::make($context->storedWorkflow->class); + $newWorkflow->start(...$arguments); + + $newWorkflow->storedWorkflow->parents() + ->attach($context->storedWorkflow, [ + 'parent_index' => -1, + 'parent_now' => $context->now, + ]); + } + + self::$context = $context; + + return resolve(new ContinuedWorkflow()); + } +} diff --git a/src/Workflow.php b/src/Workflow.php index 636ffb7..3e3aacd 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -23,6 +23,7 @@ use Workflow\Models\StoredWorkflow; use Workflow\Serializers\Serializer; use Workflow\States\WorkflowCompletedStatus; +use Workflow\States\WorkflowContinuedStatus; use Workflow\States\WorkflowRunningStatus; use Workflow\States\WorkflowWaitingStatus; use Workflow\Traits\Sagas; @@ -214,6 +215,11 @@ public function handle(): void throw new Exception('Workflow failed.', 0, $th); } + if ($return instanceof ContinuedWorkflow) { + $this->storedWorkflow->status->transitionTo(WorkflowContinuedStatus::class); + return; + } + $this->storedWorkflow->output = Serializer::serialize($return); $this->storedWorkflow->status->transitionTo(WorkflowCompletedStatus::class); diff --git a/src/WorkflowStub.php b/src/WorkflowStub.php index 9e61c5e..41af837 100644 --- a/src/WorkflowStub.php +++ b/src/WorkflowStub.php @@ -20,6 +20,7 @@ use Workflow\States\WorkflowPendingStatus; use Workflow\Traits\Awaits; use Workflow\Traits\AwaitWithTimeouts; +use Workflow\Traits\Continues; use Workflow\Traits\Fakes; use Workflow\Traits\SideEffects; use Workflow\Traits\Timers; @@ -28,6 +29,7 @@ final class WorkflowStub { use Awaits; use AwaitWithTimeouts; + use Continues; use Fakes; use Macroable; use SideEffects; @@ -54,20 +56,22 @@ public function __call($method, $arguments) ->map(static fn ($method) => $method->getName()) ->contains($method) ) { - $this->storedWorkflow->signals() + $latestWorkflow = $this->storedWorkflow->active(); + + $latestWorkflow->signals() ->create([ 'method' => $method, 'arguments' => Serializer::serialize($arguments), ]); - $this->storedWorkflow->toWorkflow(); + $latestWorkflow->toWorkflow(); if (static::faked()) { $this->resume(); return; } - return Signal::dispatch($this->storedWorkflow, self::connection(), self::queue()); + return Signal::dispatch($latestWorkflow, self::connection(), self::queue()); } if (collect((new ReflectionClass($this->storedWorkflow->class))->getMethods()) @@ -76,9 +80,11 @@ public function __call($method, $arguments) ->map(static fn ($method) => $method->getName()) ->contains($method) ) { - return (new $this->storedWorkflow->class( - $this->storedWorkflow, - ...Serializer::unserialize($this->storedWorkflow->arguments), + $latestWorkflow = $this->storedWorkflow->active(); + + return (new $latestWorkflow->class( + $latestWorkflow, + ...Serializer::unserialize($latestWorkflow->arguments), )) ->query($method); } @@ -140,21 +146,25 @@ public function id() public function logs() { - return $this->storedWorkflow->logs; + return $this->storedWorkflow->active() + ->logs; } public function exceptions() { - return $this->storedWorkflow->exceptions; + return $this->storedWorkflow->active() + ->exceptions; } public function output() { - if ($this->storedWorkflow->fresh()->output === null) { + $latestWorkflow = $this->storedWorkflow->active(); + + if ($latestWorkflow->fresh()->output === null) { return null; } - return Serializer::unserialize($this->storedWorkflow->fresh()->output); + return Serializer::unserialize($latestWorkflow->fresh()->output); } public function completed(): bool @@ -179,7 +189,8 @@ public function running(): bool public function status(): string|bool { - return $this->storedWorkflow->fresh() + return $this->storedWorkflow->active() + ->fresh() ->status::class; } diff --git a/tests/Feature/ContinueAsNewWorkflowTest.php b/tests/Feature/ContinueAsNewWorkflowTest.php new file mode 100644 index 0000000..6d3b980 --- /dev/null +++ b/tests/Feature/ContinueAsNewWorkflowTest.php @@ -0,0 +1,30 @@ +start(); + + while ($workflow->running()); + + $this->assertEquals(WorkflowCompletedStatus::class, $workflow->status()); + $this->assertEquals('workflow_3', $workflow->output()); + + $workflow = WorkflowStub::load(2); + + $this->assertEquals(WorkflowCompletedStatus::class, $workflow->status()); + $this->assertEquals('workflow_3', $workflow->output()); + } +} diff --git a/tests/Fixtures/TestContinueAsNewWorkflow.php b/tests/Fixtures/TestContinueAsNewWorkflow.php new file mode 100644 index 0000000..c3ee60c --- /dev/null +++ b/tests/Fixtures/TestContinueAsNewWorkflow.php @@ -0,0 +1,20 @@ += 3) { + return 'workflow_' . $count; + } + + return yield WorkflowStub::continueAsNew($count + 1); + } +} diff --git a/tests/Fixtures/TestWorkflow.php b/tests/Fixtures/TestWorkflow.php index 230772d..2ec8359 100644 --- a/tests/Fixtures/TestWorkflow.php +++ b/tests/Fixtures/TestWorkflow.php @@ -41,10 +41,6 @@ public function execute(Application $app, $shouldAssert = false) $otherResult = yield ActivityStub::make(TestOtherActivity::class, 'other'); - if ($shouldAssert) { - assert(! $this->canceled); - } - yield WorkflowStub::await(fn (): bool => $this->canceled); $result = yield ActivityStub::make(TestActivity::class); diff --git a/tests/TestCase.php b/tests/TestCase.php index 804f923..d69c0d2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -57,6 +57,8 @@ protected function defineDatabaseMigrations() '--realpath' => true, ]); + $this->artisan('queue:failed-table'); + $this->loadLaravelMigrations(); } diff --git a/tests/Unit/States/WorkflowStatusTest.php b/tests/Unit/States/WorkflowStatusTest.php index bc99973..50bafe1 100644 --- a/tests/Unit/States/WorkflowStatusTest.php +++ b/tests/Unit/States/WorkflowStatusTest.php @@ -7,6 +7,7 @@ use Tests\TestCase; use Workflow\Models\StoredWorkflow; use Workflow\States\WorkflowCompletedStatus; +use Workflow\States\WorkflowContinuedStatus; use Workflow\States\WorkflowCreatedStatus; use Workflow\States\WorkflowFailedStatus; use Workflow\States\WorkflowPendingStatus; @@ -20,6 +21,7 @@ public function testConfig(): void $config = StoredWorkflow::make()->getStates()->first()->all(); $this->assertSame([ WorkflowCompletedStatus::$name, + WorkflowContinuedStatus::$name, WorkflowCreatedStatus::$name, WorkflowFailedStatus::$name, WorkflowPendingStatus::$name, From 8734c92ed97fba789c0b734ca0ae420565363bff Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Fri, 11 Jul 2025 04:25:27 +0000 Subject: [PATCH 02/19] Continue as new --- .github/workflows/php.yml | 2 ++ tests/.env.feature | 1 + tests/.env.unit | 1 + 3 files changed, 4 insertions(+) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 6874790..ea9f2b1 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -75,6 +75,7 @@ jobs: DB_USERNAME: root DB_PASSWORD: password QUEUE_CONNECTION: redis + QUEUE_FAILED_DRIVER: null REDIS_HOST: 127.0.0.1 REDIS_PASSWORD: REDIS_PORT: 6379 @@ -90,6 +91,7 @@ jobs: DB_USERNAME: root DB_PASSWORD: password QUEUE_CONNECTION: redis + QUEUE_FAILED_DRIVER: null REDIS_HOST: 127.0.0.1 REDIS_PASSWORD: REDIS_PORT: 6379 diff --git a/tests/.env.feature b/tests/.env.feature index d47a269..406cbc8 100644 --- a/tests/.env.feature +++ b/tests/.env.feature @@ -8,6 +8,7 @@ DB_USERNAME=laravel DB_PASSWORD=laravel QUEUE_CONNECTION=redis +QUEUE_FAILED_DRIVER=null REDIS_HOST=redis REDIS_PASSWORD= diff --git a/tests/.env.unit b/tests/.env.unit index 4d6aa94..c300690 100644 --- a/tests/.env.unit +++ b/tests/.env.unit @@ -8,6 +8,7 @@ DB_USERNAME=laravel DB_PASSWORD=laravel QUEUE_CONNECTION=sync +QUEUE_FAILED_DRIVER=null REDIS_HOST=redis REDIS_PASSWORD= From 4f429737759b5100390a61284e0525304699aa68 Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Fri, 11 Jul 2025 19:22:36 +0000 Subject: [PATCH 03/19] Upload log on failure --- .github/workflows/php.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index ea9f2b1..dde8ae2 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -96,6 +96,13 @@ jobs: REDIS_PASSWORD: REDIS_PORT: 6379 + - name: Upload laravel.log if tests fail + if: failure() + uses: actions/upload-artifact@v4 + with: + name: laravel-log + path: vendor/orchestra/testbench-core/laravel/storage/logs/laravel.log + - name: Code Coverage run: | vendor/bin/phpunit --testdox --coverage-clover=coverage.clover --testsuite unit From 0e7d82ecbae270beb9c555741e14b43d43a6049e Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Fri, 11 Jul 2025 19:52:11 +0000 Subject: [PATCH 04/19] Use PHP_INT_MAX --- src/Models/StoredWorkflow.php | 4 ++-- src/Traits/Continues.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Models/StoredWorkflow.php b/src/Models/StoredWorkflow.php index 6299da3..e1b407e 100644 --- a/src/Models/StoredWorkflow.php +++ b/src/Models/StoredWorkflow.php @@ -93,7 +93,7 @@ public function continuedWorkflows(): \Illuminate\Database\Eloquent\Relations\Be config('workflows.workflow_relationships_table', 'workflow_relationships'), 'parent_workflow_id', 'child_workflow_id' - )->wherePivot('parent_index', -1) + )->wherePivot('parent_index', PHP_INT_MAX) ->withPivot(['parent_index', 'parent_now']); } @@ -101,7 +101,7 @@ public function root(): self { $root = $this; - while ($parent = $root->parents()->wherePivot('parent_index', -1)->first()) { + while ($parent = $root->parents()->wherePivot('parent_index', PHP_INT_MAX)->first()) { $root = $parent; } diff --git a/src/Traits/Continues.php b/src/Traits/Continues.php index 5eb597a..08c7285 100644 --- a/src/Traits/Continues.php +++ b/src/Traits/Continues.php @@ -20,7 +20,7 @@ public static function continueAsNew(...$arguments): PromiseInterface $newWorkflow->storedWorkflow->parents() ->attach($context->storedWorkflow, [ - 'parent_index' => -1, + 'parent_index' => PHP_INT_MAX, 'parent_now' => $context->now, ]); } From 1a089e62627a822e9e89200c8612de528dc2dd5e Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Fri, 11 Jul 2025 16:58:58 -0500 Subject: [PATCH 05/19] Update php.yml --- .github/workflows/php.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index dde8ae2..6fde9ae 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -75,7 +75,7 @@ jobs: DB_USERNAME: root DB_PASSWORD: password QUEUE_CONNECTION: redis - QUEUE_FAILED_DRIVER: null + QUEUE_FAILED_DRIVER: "null" REDIS_HOST: 127.0.0.1 REDIS_PASSWORD: REDIS_PORT: 6379 @@ -91,7 +91,7 @@ jobs: DB_USERNAME: root DB_PASSWORD: password QUEUE_CONNECTION: redis - QUEUE_FAILED_DRIVER: null + QUEUE_FAILED_DRIVER: "null" REDIS_HOST: 127.0.0.1 REDIS_PASSWORD: REDIS_PORT: 6379 From 4c36443926869195f50d421c49d95952eb3747ae Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Sat, 12 Jul 2025 04:31:28 +0000 Subject: [PATCH 06/19] Changes from review --- src/Models/StoredWorkflow.php | 16 +++++++++++--- src/Traits/Continues.php | 3 ++- src/WorkflowStub.php | 22 ++++++++++---------- tests/Fixtures/TestContinueAsNewWorkflow.php | 2 +- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/Models/StoredWorkflow.php b/src/Models/StoredWorkflow.php index e1b407e..688ab09 100644 --- a/src/Models/StoredWorkflow.php +++ b/src/Models/StoredWorkflow.php @@ -18,6 +18,11 @@ class StoredWorkflow extends Model use HasStates; use Prunable; + /** + * @var int + */ + public const CONTINUE_PARENT_INDEX = PHP_INT_MAX; + /** * @var string */ @@ -93,15 +98,20 @@ public function continuedWorkflows(): \Illuminate\Database\Eloquent\Relations\Be config('workflows.workflow_relationships_table', 'workflow_relationships'), 'parent_workflow_id', 'child_workflow_id' - )->wherePivot('parent_index', PHP_INT_MAX) - ->withPivot(['parent_index', 'parent_now']); + )->wherePivot('parent_index', self::CONTINUE_PARENT_INDEX) + ->withPivot(['parent_index', 'parent_now']) + ->orderBy('child_workflow_id'); } public function root(): self { $root = $this; - while ($parent = $root->parents()->wherePivot('parent_index', PHP_INT_MAX)->first()) { + while ($parent = $root->parents() + ->wherePivot('parent_index', self::CONTINUE_PARENT_INDEX) + ->orderBy('parent_workflow_id') + ->first() + ) { $root = $parent; } diff --git a/src/Traits/Continues.php b/src/Traits/Continues.php index 08c7285..b04d55a 100644 --- a/src/Traits/Continues.php +++ b/src/Traits/Continues.php @@ -7,6 +7,7 @@ use React\Promise\PromiseInterface; use function React\Promise\resolve; use Workflow\ContinuedWorkflow; +use Workflow\Models\StoredWorkflow; trait Continues { @@ -20,7 +21,7 @@ public static function continueAsNew(...$arguments): PromiseInterface $newWorkflow->storedWorkflow->parents() ->attach($context->storedWorkflow, [ - 'parent_index' => PHP_INT_MAX, + 'parent_index' => StoredWorkflow::CONTINUE_PARENT_INDEX, 'parent_now' => $context->now, ]); } diff --git a/src/WorkflowStub.php b/src/WorkflowStub.php index 41af837..b997b8f 100644 --- a/src/WorkflowStub.php +++ b/src/WorkflowStub.php @@ -56,22 +56,22 @@ public function __call($method, $arguments) ->map(static fn ($method) => $method->getName()) ->contains($method) ) { - $latestWorkflow = $this->storedWorkflow->active(); + $activeWorkflow = $this->storedWorkflow->active(); - $latestWorkflow->signals() + $activeWorkflow->signals() ->create([ 'method' => $method, 'arguments' => Serializer::serialize($arguments), ]); - $latestWorkflow->toWorkflow(); + $activeWorkflow->toWorkflow(); if (static::faked()) { $this->resume(); return; } - return Signal::dispatch($latestWorkflow, self::connection(), self::queue()); + return Signal::dispatch($activeWorkflow, self::connection(), self::queue()); } if (collect((new ReflectionClass($this->storedWorkflow->class))->getMethods()) @@ -80,11 +80,11 @@ public function __call($method, $arguments) ->map(static fn ($method) => $method->getName()) ->contains($method) ) { - $latestWorkflow = $this->storedWorkflow->active(); + $activeWorkflow = $this->storedWorkflow->active(); - return (new $latestWorkflow->class( - $latestWorkflow, - ...Serializer::unserialize($latestWorkflow->arguments), + return (new $activeWorkflow->class( + $activeWorkflow, + ...Serializer::unserialize($activeWorkflow->arguments), )) ->query($method); } @@ -158,13 +158,13 @@ public function exceptions() public function output() { - $latestWorkflow = $this->storedWorkflow->active(); + $activeWorkflow = $this->storedWorkflow->active(); - if ($latestWorkflow->fresh()->output === null) { + if ($activeWorkflow->fresh()->output === null) { return null; } - return Serializer::unserialize($latestWorkflow->fresh()->output); + return Serializer::unserialize($activeWorkflow->fresh()->output); } public function completed(): bool diff --git a/tests/Fixtures/TestContinueAsNewWorkflow.php b/tests/Fixtures/TestContinueAsNewWorkflow.php index c3ee60c..17c0a40 100644 --- a/tests/Fixtures/TestContinueAsNewWorkflow.php +++ b/tests/Fixtures/TestContinueAsNewWorkflow.php @@ -9,7 +9,7 @@ class TestContinueAsNewWorkflow extends Workflow { - public function execute($count = 0) + public function execute(int $count = 0) { if ($count >= 3) { return 'workflow_' . $count; From e55cdefa191bb020c312be5e3ef3ff6c2b7d733e Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Sat, 12 Jul 2025 05:08:37 +0000 Subject: [PATCH 07/19] Optimize --- src/Models/StoredWorkflow.php | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/Models/StoredWorkflow.php b/src/Models/StoredWorkflow.php index 688ab09..0e2fd9c 100644 --- a/src/Models/StoredWorkflow.php +++ b/src/Models/StoredWorkflow.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Prunable; use Spatie\ModelStates\HasStates; +use Workflow\States\WorkflowContinuedStatus; use Workflow\States\WorkflowStatus; use Workflow\WorkflowStub; @@ -103,26 +104,11 @@ public function continuedWorkflows(): \Illuminate\Database\Eloquent\Relations\Be ->orderBy('child_workflow_id'); } - public function root(): self - { - $root = $this; - - while ($parent = $root->parents() - ->wherePivot('parent_index', self::CONTINUE_PARENT_INDEX) - ->orderBy('parent_workflow_id') - ->first() - ) { - $root = $parent; - } - - return $root; - } - public function active(): self { - $active = $this->root(); + $active = $this; - while ($next = $active->continuedWorkflows()->first()) { + while ($active->status::class === WorkflowContinuedStatus::class && ($next = $active->continuedWorkflows()->first())) { $active = $next; } From 6b650fbe054b96fbd0a887073a2ab2fe8d694ac3 Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Sat, 12 Jul 2025 17:00:30 +0000 Subject: [PATCH 08/19] Call fresh --- src/Models/StoredWorkflow.php | 2 +- src/WorkflowStub.php | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Models/StoredWorkflow.php b/src/Models/StoredWorkflow.php index 0e2fd9c..51b347c 100644 --- a/src/Models/StoredWorkflow.php +++ b/src/Models/StoredWorkflow.php @@ -106,7 +106,7 @@ public function continuedWorkflows(): \Illuminate\Database\Eloquent\Relations\Be public function active(): self { - $active = $this; + $active = $this->fresh(); while ($active->status::class === WorkflowContinuedStatus::class && ($next = $active->continuedWorkflows()->first())) { $active = $next; diff --git a/src/WorkflowStub.php b/src/WorkflowStub.php index b997b8f..fd3d8ad 100644 --- a/src/WorkflowStub.php +++ b/src/WorkflowStub.php @@ -160,11 +160,11 @@ public function output() { $activeWorkflow = $this->storedWorkflow->active(); - if ($activeWorkflow->fresh()->output === null) { + if ($activeWorkflow->output === null) { return null; } - return Serializer::unserialize($activeWorkflow->fresh()->output); + return Serializer::unserialize($activeWorkflow->output); } public function completed(): bool @@ -190,7 +190,6 @@ public function running(): bool public function status(): string|bool { return $this->storedWorkflow->active() - ->fresh() ->status::class; } From baf9c216bd1702f4a3d08fff3f8896832e4928bf Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Sat, 12 Jul 2025 18:00:03 +0000 Subject: [PATCH 09/19] Coverage --- composer.json | 1 + tests/Unit/Models/StoredWorkflowTest.php | 48 ++++++++++++++++++++++++ tests/Unit/WorkflowTest.php | 18 +++++++++ 3 files changed, 67 insertions(+) diff --git a/composer.json b/composer.json index 084fc66..7cdc83c 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "feature": "phpunit --testdox --testsuite feature", "unit": "phpunit --testdox --testsuite unit", "test": "phpunit --testdox", + "coverage": "XDEBUG_MODE=coverage phpunit --testdox --testsuite unit --coverage-clover coverage.xml", "post-autoload-dump": [ "@clear", "@prepare" diff --git a/tests/Unit/Models/StoredWorkflowTest.php b/tests/Unit/Models/StoredWorkflowTest.php index a256a96..358fce3 100644 --- a/tests/Unit/Models/StoredWorkflowTest.php +++ b/tests/Unit/Models/StoredWorkflowTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\Carbon; use Tests\TestCase; use Workflow\Models\StoredWorkflow; +use Workflow\States\WorkflowContinuedStatus; final class StoredWorkflowTest extends TestCase { @@ -70,4 +71,51 @@ public function testModel(): void $this->assertSame(0, $workflow->timers()->count()); $this->assertSame(0, $workflow->children()->count()); } + + public function testContinuedWorkflows(): void + { + $parentWorkflow = StoredWorkflow::create([ + 'class' => 'ParentWorkflow', + 'status' => 'continued', + ]); + + $continuedWorkflow = StoredWorkflow::create([ + 'class' => 'ContinuedWorkflow', + 'status' => 'completed', + ]); + + $continuedWorkflow->parents() + ->attach($parentWorkflow, [ + 'parent_index' => StoredWorkflow::CONTINUE_PARENT_INDEX, + 'parent_now' => now(), + ]); + + $result = $parentWorkflow->continuedWorkflows(); + + $this->assertSame(1, $parentWorkflow->continuedWorkflows()->count()); + $this->assertSame($continuedWorkflow->id, $parentWorkflow->continuedWorkflows()->first()->id); + } + + public function testActiveWithContinuedWorkflow(): void + { + $parentWorkflow = StoredWorkflow::create([ + 'class' => 'ParentWorkflow', + 'status' => WorkflowContinuedStatus::class, + ]); + + $continuedWorkflow = StoredWorkflow::create([ + 'class' => 'ContinuedWorkflow', + 'status' => 'completed', + ]); + + $continuedWorkflow->parents() + ->attach($parentWorkflow, [ + 'parent_index' => StoredWorkflow::CONTINUE_PARENT_INDEX, + 'parent_now' => now(), + ]); + + $active = $parentWorkflow->active(); + + $this->assertSame($continuedWorkflow->id, $active->id); + } } diff --git a/tests/Unit/WorkflowTest.php b/tests/Unit/WorkflowTest.php index 3f95402..c3e83f1 100644 --- a/tests/Unit/WorkflowTest.php +++ b/tests/Unit/WorkflowTest.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Event; use Tests\Fixtures\TestActivity; use Tests\Fixtures\TestChildWorkflow; +use Tests\Fixtures\TestContinueAsNewWorkflow; use Tests\Fixtures\TestOtherActivity; use Tests\Fixtures\TestParentWorkflow; use Tests\Fixtures\TestThrowOnReturnWorkflow; @@ -20,6 +21,7 @@ use Workflow\Models\StoredWorkflow; use Workflow\Serializers\Serializer; use Workflow\States\WorkflowCompletedStatus; +use Workflow\States\WorkflowContinuedStatus; use Workflow\States\WorkflowFailedStatus; use Workflow\States\WorkflowPendingStatus; use Workflow\Workflow; @@ -327,4 +329,20 @@ public function testThrowsWrappedException(): void $workflow = new TestThrowOnReturnWorkflow($storedWorkflow); $workflow->handle(); } + + public function testContinueAsNew(): void + { + $storedWorkflow = StoredWorkflow::create([ + 'class' => TestContinueAsNewWorkflow::class, + 'arguments' => Serializer::serialize([0]), + 'status' => WorkflowPendingStatus::class, + ]); + + $workflow = new TestContinueAsNewWorkflow($storedWorkflow); + $workflow->handle(); + + $this->assertInstanceOf(WorkflowContinuedStatus::class, $storedWorkflow->fresh()->status); + + $this->assertSame(1, $storedWorkflow->continuedWorkflows()->count()); + } } From a3e5f7e41b438d3b9041aa024d19813c1edd8988 Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Sat, 12 Jul 2025 18:59:58 +0000 Subject: [PATCH 10/19] More tests --- .gitignore | 1 + tests/Feature/ContinueAsNewWorkflowTest.php | 52 ++++++++++++++++++++ tests/Fixtures/TestContinueAsNewWorkflow.php | 11 +++-- tests/Fixtures/TestCountActivity.php | 15 ++++++ tests/Unit/WorkflowTest.php | 11 ++++- 5 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 tests/Fixtures/TestCountActivity.php diff --git a/.gitignore b/.gitignore index 271eb9a..4d84bc3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build composer.lock vendor coverage +coverage.xml .env .phpunit.cache .phpunit.result.cache diff --git a/tests/Feature/ContinueAsNewWorkflowTest.php b/tests/Feature/ContinueAsNewWorkflowTest.php index 6d3b980..8e0547d 100644 --- a/tests/Feature/ContinueAsNewWorkflowTest.php +++ b/tests/Feature/ContinueAsNewWorkflowTest.php @@ -6,7 +6,10 @@ use Tests\Fixtures\TestContinueAsNewWorkflow; use Tests\TestCase; +use Workflow\Models\StoredWorkflow; +use Workflow\Serializers\Serializer; use Workflow\States\WorkflowCompletedStatus; +use Workflow\States\WorkflowContinuedStatus; use Workflow\WorkflowStub; final class ContinueAsNewWorkflowTest extends TestCase @@ -27,4 +30,53 @@ public function testCompleted(): void $this->assertEquals(WorkflowCompletedStatus::class, $workflow->status()); $this->assertEquals('workflow_3', $workflow->output()); } + + public function testDeepContinueAsNewChain(): void + { + $chainLength = 1000; + $workflows = []; + + for ($i = 0; $i < $chainLength; $i++) { + $workflows[$i] = StoredWorkflow::create([ + 'class' => TestContinueAsNewWorkflow::class, + 'status' => $i === $chainLength - 1 + ? WorkflowCompletedStatus::class + : WorkflowContinuedStatus::class, + 'output' => $i === $chainLength - 1 + ? Serializer::serialize('workflow_' . $chainLength) + : null, + ]); + } + + for ($i = 0; $i < $chainLength - 1; $i++) { + $workflows[$i + 1]->parents()->attach($workflows[$i], [ + 'parent_index' => StoredWorkflow::CONTINUE_PARENT_INDEX, + 'parent_now' => now(), + ]); + } + + $originalWorkflow = WorkflowStub::load($workflows[0]->id); + + $startTime = microtime(true); + $status = $originalWorkflow->status(); + $endTime = microtime(true); + + $this->assertEquals(WorkflowCompletedStatus::class, $status); + $this->assertLessThan( + 3.0, + $endTime - $startTime, + 'Chain resolution should complete in less than 3 seconds even for 1000 workflows' + ); + + $startTime = microtime(true); + $output = $originalWorkflow->output(); + $endTime = microtime(true); + + $this->assertEquals('workflow_1000', $output); + $this->assertLessThan( + 3.0, + $endTime - $startTime, + 'Output resolution should complete in less than 3 seconds even for 1000 workflows' + ); + } } diff --git a/tests/Fixtures/TestContinueAsNewWorkflow.php b/tests/Fixtures/TestContinueAsNewWorkflow.php index 17c0a40..b0ffde6 100644 --- a/tests/Fixtures/TestContinueAsNewWorkflow.php +++ b/tests/Fixtures/TestContinueAsNewWorkflow.php @@ -4,17 +4,20 @@ namespace Tests\Fixtures; +use Workflow\ActivityStub; use Workflow\Workflow; use Workflow\WorkflowStub; class TestContinueAsNewWorkflow extends Workflow { - public function execute(int $count = 0) + public function execute(int $count = 0, int $totalCount = 3) { - if ($count >= 3) { - return 'workflow_' . $count; + $activityResult = yield ActivityStub::make(TestCountActivity::class, $count); + + if ($count >= $totalCount) { + return 'workflow_' . $activityResult; } - return yield WorkflowStub::continueAsNew($count + 1); + return yield WorkflowStub::continueAsNew($count + 1, $totalCount); } } diff --git a/tests/Fixtures/TestCountActivity.php b/tests/Fixtures/TestCountActivity.php new file mode 100644 index 0000000..1c926ea --- /dev/null +++ b/tests/Fixtures/TestCountActivity.php @@ -0,0 +1,15 @@ + TestContinueAsNewWorkflow::class, - 'arguments' => Serializer::serialize([0]), + 'arguments' => Serializer::serialize([0, 3]), 'status' => WorkflowPendingStatus::class, ]); + $storedWorkflow->logs() + ->create([ + 'index' => 0, + 'now' => now(), + 'class' => TestCountActivity::class, + 'result' => Serializer::serialize(0), + ]); + $workflow = new TestContinueAsNewWorkflow($storedWorkflow); $workflow->handle(); From 2b3cfbd22f3dd0af6e317f5094ccea13b2e50eb5 Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Sun, 13 Jul 2025 00:31:07 +0000 Subject: [PATCH 11/19] Performance --- src/Models/StoredWorkflow.php | 22 ++++ src/Traits/Continues.php | 14 +++ tests/Feature/ContinueAsNewWorkflowTest.php | 52 --------- tests/Unit/Models/StoredWorkflowTest.php | 121 ++++++++++++++++++++ 4 files changed, 157 insertions(+), 52 deletions(-) diff --git a/src/Models/StoredWorkflow.php b/src/Models/StoredWorkflow.php index 51b347c..e06392d 100644 --- a/src/Models/StoredWorkflow.php +++ b/src/Models/StoredWorkflow.php @@ -24,6 +24,11 @@ class StoredWorkflow extends Model */ public const CONTINUE_PARENT_INDEX = PHP_INT_MAX; + /** + * @var int + */ + public const ACTIVE_WORKFLOW_INDEX = PHP_INT_MAX - 1; + /** * @var string */ @@ -104,10 +109,27 @@ public function continuedWorkflows(): \Illuminate\Database\Eloquent\Relations\Be ->orderBy('child_workflow_id'); } + public function activeWorkflow(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + { + return $this->belongsToMany( + config('workflows.stored_workflow_model', self::class), + config('workflows.workflow_relationships_table', 'workflow_relationships'), + 'parent_workflow_id', + 'child_workflow_id' + )->wherePivot('parent_index', self::ACTIVE_WORKFLOW_INDEX) + ->withPivot(['parent_index', 'parent_now']) + ->orderBy('child_workflow_id'); + } + public function active(): self { $active = $this->fresh(); + if ($active->status::class === WorkflowContinuedStatus::class) { + $active = $this->activeWorkflow() + ->first() ?: $active; + } + while ($active->status::class === WorkflowContinuedStatus::class && ($next = $active->continuedWorkflows()->first())) { $active = $next; } diff --git a/src/Traits/Continues.php b/src/Traits/Continues.php index b04d55a..cb52c55 100644 --- a/src/Traits/Continues.php +++ b/src/Traits/Continues.php @@ -24,6 +24,20 @@ public static function continueAsNew(...$arguments): PromiseInterface 'parent_index' => StoredWorkflow::CONTINUE_PARENT_INDEX, 'parent_now' => $context->now, ]); + + $rootWorkflow = $context->storedWorkflow->parents() + ->wherePivot('parent_index', StoredWorkflow::ACTIVE_WORKFLOW_INDEX)->first(); + + if ($rootWorkflow) { + $rootWorkflow->children() + ->wherePivot('parent_index', StoredWorkflow::ACTIVE_WORKFLOW_INDEX)->detach(); + + $rootWorkflow->children() + ->attach($newWorkflow->storedWorkflow, [ + 'parent_index' => StoredWorkflow::ACTIVE_WORKFLOW_INDEX, + 'parent_now' => $context->now, + ]); + } } self::$context = $context; diff --git a/tests/Feature/ContinueAsNewWorkflowTest.php b/tests/Feature/ContinueAsNewWorkflowTest.php index 8e0547d..6d3b980 100644 --- a/tests/Feature/ContinueAsNewWorkflowTest.php +++ b/tests/Feature/ContinueAsNewWorkflowTest.php @@ -6,10 +6,7 @@ use Tests\Fixtures\TestContinueAsNewWorkflow; use Tests\TestCase; -use Workflow\Models\StoredWorkflow; -use Workflow\Serializers\Serializer; use Workflow\States\WorkflowCompletedStatus; -use Workflow\States\WorkflowContinuedStatus; use Workflow\WorkflowStub; final class ContinueAsNewWorkflowTest extends TestCase @@ -30,53 +27,4 @@ public function testCompleted(): void $this->assertEquals(WorkflowCompletedStatus::class, $workflow->status()); $this->assertEquals('workflow_3', $workflow->output()); } - - public function testDeepContinueAsNewChain(): void - { - $chainLength = 1000; - $workflows = []; - - for ($i = 0; $i < $chainLength; $i++) { - $workflows[$i] = StoredWorkflow::create([ - 'class' => TestContinueAsNewWorkflow::class, - 'status' => $i === $chainLength - 1 - ? WorkflowCompletedStatus::class - : WorkflowContinuedStatus::class, - 'output' => $i === $chainLength - 1 - ? Serializer::serialize('workflow_' . $chainLength) - : null, - ]); - } - - for ($i = 0; $i < $chainLength - 1; $i++) { - $workflows[$i + 1]->parents()->attach($workflows[$i], [ - 'parent_index' => StoredWorkflow::CONTINUE_PARENT_INDEX, - 'parent_now' => now(), - ]); - } - - $originalWorkflow = WorkflowStub::load($workflows[0]->id); - - $startTime = microtime(true); - $status = $originalWorkflow->status(); - $endTime = microtime(true); - - $this->assertEquals(WorkflowCompletedStatus::class, $status); - $this->assertLessThan( - 3.0, - $endTime - $startTime, - 'Chain resolution should complete in less than 3 seconds even for 1000 workflows' - ); - - $startTime = microtime(true); - $output = $originalWorkflow->output(); - $endTime = microtime(true); - - $this->assertEquals('workflow_1000', $output); - $this->assertLessThan( - 3.0, - $endTime - $startTime, - 'Output resolution should complete in less than 3 seconds even for 1000 workflows' - ); - } } diff --git a/tests/Unit/Models/StoredWorkflowTest.php b/tests/Unit/Models/StoredWorkflowTest.php index 358fce3..df1f5fb 100644 --- a/tests/Unit/Models/StoredWorkflowTest.php +++ b/tests/Unit/Models/StoredWorkflowTest.php @@ -5,9 +5,14 @@ namespace Tests\Unit\Models; use Illuminate\Support\Carbon; +use Tests\Fixtures\TestContinueAsNewWorkflow; +use Tests\Fixtures\TestWorkflow; use Tests\TestCase; use Workflow\Models\StoredWorkflow; +use Workflow\Serializers\Serializer; use Workflow\States\WorkflowContinuedStatus; +use Workflow\States\WorkflowRunningStatus; +use Workflow\WorkflowStub; final class StoredWorkflowTest extends TestCase { @@ -114,8 +119,124 @@ public function testActiveWithContinuedWorkflow(): void 'parent_now' => now(), ]); + $parentWorkflow->children() + ->attach($continuedWorkflow, [ + 'parent_index' => StoredWorkflow::ACTIVE_WORKFLOW_INDEX, + 'parent_now' => now(), + ]); + $active = $parentWorkflow->active(); $this->assertSame($continuedWorkflow->id, $active->id); } + + public function testActiveWithShortcut(): void + { + $rootWorkflow = StoredWorkflow::create([ + 'class' => 'RootWorkflow', + 'status' => WorkflowContinuedStatus::class, + ]); + + $activeWorkflow = StoredWorkflow::create([ + 'class' => 'ActiveWorkflow', + 'status' => 'completed', + ]); + + $rootWorkflow->children() + ->attach($activeWorkflow, [ + 'parent_index' => StoredWorkflow::ACTIVE_WORKFLOW_INDEX, + 'parent_now' => now(), + ]); + + $active = $rootWorkflow->active(); + + $this->assertSame($activeWorkflow->id, $active->id); + } + + public function testActiveWorkflowShortcutTransferOnContinue(): void + { + $rootWorkflow = StoredWorkflow::create([ + 'class' => TestWorkflow::class, + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowRunningStatus::class, + ]); + + $intermediateWorkflow = StoredWorkflow::create([ + 'class' => TestContinueAsNewWorkflow::class, + 'arguments' => Serializer::serialize([1, 3]), + 'status' => WorkflowRunningStatus::class, + ]); + + $intermediateWorkflow->parents() + ->attach($rootWorkflow->id, [ + 'parent_index' => StoredWorkflow::ACTIVE_WORKFLOW_INDEX, + 'parent_now' => now(), + ]); + + WorkflowStub::setContext([ + 'storedWorkflow' => $intermediateWorkflow, + 'index' => 0, + 'now' => now(), + 'replaying' => false, + ]); + + WorkflowStub::continueAsNew(2, 3); + + $this->assertSame(1, $intermediateWorkflow->continuedWorkflows()->count()); + $newWorkflow = $intermediateWorkflow->continuedWorkflows() + ->first(); + + $activeParent = $newWorkflow->parents() + ->wherePivot('parent_index', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) + ->first(); + + $this->assertNotNull($activeParent); + $this->assertSame($rootWorkflow->id, $activeParent->id); + } + + public function testActiveWorkflowWithMultipleContinuations(): void + { + $rootWorkflow = StoredWorkflow::create([ + 'class' => 'RootWorkflow', + 'status' => WorkflowContinuedStatus::class, + ]); + + $intermediateWorkflow = StoredWorkflow::create([ + 'class' => 'IntermediateWorkflow', + 'status' => WorkflowContinuedStatus::class, + ]); + + $finalWorkflow = StoredWorkflow::create([ + 'class' => 'FinalWorkflow', + 'status' => 'completed', + ]); + + $intermediateWorkflow->parents() + ->attach($rootWorkflow, [ + 'parent_index' => StoredWorkflow::CONTINUE_PARENT_INDEX, + 'parent_now' => now(), + ]); + + $finalWorkflow->parents() + ->attach($intermediateWorkflow, [ + 'parent_index' => StoredWorkflow::CONTINUE_PARENT_INDEX, + 'parent_now' => now(), + ]); + + $rootWorkflow->children() + ->attach($intermediateWorkflow, [ + 'parent_index' => StoredWorkflow::ACTIVE_WORKFLOW_INDEX, + 'parent_now' => now(), + ]); + + $intermediateWorkflow->children() + ->attach($finalWorkflow, [ + 'parent_index' => StoredWorkflow::ACTIVE_WORKFLOW_INDEX, + 'parent_now' => now(), + ]); + + $active = $rootWorkflow->active(); + + $this->assertSame($finalWorkflow->id, $active->id); + } } From 8fe3583d0fade1d64d126be8d7cf430c16d827b7 Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Sun, 13 Jul 2025 02:02:18 +0000 Subject: [PATCH 12/19] Fix order --- src/Models/StoredWorkflow.php | 9 +++++---- src/Traits/Continues.php | 7 ++++--- tests/Feature/ContinueAsNewWorkflowTest.php | 5 ----- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Models/StoredWorkflow.php b/src/Models/StoredWorkflow.php index e06392d..03c948b 100644 --- a/src/Models/StoredWorkflow.php +++ b/src/Models/StoredWorkflow.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Prunable; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Spatie\ModelStates\HasStates; use Workflow\States\WorkflowContinuedStatus; use Workflow\States\WorkflowStatus; @@ -77,7 +78,7 @@ public function exceptions(): \Illuminate\Database\Eloquent\Relations\HasMany ->orderBy('id'); } - public function parents(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + public function parents(): BelongsToMany { return $this->belongsToMany( config('workflows.stored_workflow_model', self::class), @@ -87,7 +88,7 @@ public function parents(): \Illuminate\Database\Eloquent\Relations\BelongsToMany )->withPivot(['parent_index', 'parent_now']); } - public function children(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + public function children(): BelongsToMany { return $this->belongsToMany( config('workflows.stored_workflow_model', self::class), @@ -97,7 +98,7 @@ public function children(): \Illuminate\Database\Eloquent\Relations\BelongsToMan )->withPivot(['parent_index', 'parent_now']); } - public function continuedWorkflows(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + public function continuedWorkflows(): BelongsToMany { return $this->belongsToMany( config('workflows.stored_workflow_model', self::class), @@ -109,7 +110,7 @@ public function continuedWorkflows(): \Illuminate\Database\Eloquent\Relations\Be ->orderBy('child_workflow_id'); } - public function activeWorkflow(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + public function activeWorkflow(): BelongsToMany { return $this->belongsToMany( config('workflows.stored_workflow_model', self::class), diff --git a/src/Traits/Continues.php b/src/Traits/Continues.php index cb52c55..7f2733c 100644 --- a/src/Traits/Continues.php +++ b/src/Traits/Continues.php @@ -29,14 +29,15 @@ public static function continueAsNew(...$arguments): PromiseInterface ->wherePivot('parent_index', StoredWorkflow::ACTIVE_WORKFLOW_INDEX)->first(); if ($rootWorkflow) { - $rootWorkflow->children() - ->wherePivot('parent_index', StoredWorkflow::ACTIVE_WORKFLOW_INDEX)->detach(); - $rootWorkflow->children() ->attach($newWorkflow->storedWorkflow, [ 'parent_index' => StoredWorkflow::ACTIVE_WORKFLOW_INDEX, 'parent_now' => $context->now, ]); + + $rootWorkflow->children() + ->wherePivot('parent_index', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) + ->detach($context->storedWorkflow); } } diff --git a/tests/Feature/ContinueAsNewWorkflowTest.php b/tests/Feature/ContinueAsNewWorkflowTest.php index 6d3b980..e653d12 100644 --- a/tests/Feature/ContinueAsNewWorkflowTest.php +++ b/tests/Feature/ContinueAsNewWorkflowTest.php @@ -21,10 +21,5 @@ public function testCompleted(): void $this->assertEquals(WorkflowCompletedStatus::class, $workflow->status()); $this->assertEquals('workflow_3', $workflow->output()); - - $workflow = WorkflowStub::load(2); - - $this->assertEquals(WorkflowCompletedStatus::class, $workflow->status()); - $this->assertEquals('workflow_3', $workflow->output()); } } From 3e4df6c9647488ab01837981fed84f2407dd2fc8 Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Sun, 13 Jul 2025 02:17:29 +0000 Subject: [PATCH 13/19] Performance --- src/Models/StoredWorkflow.php | 6 +----- src/Traits/Continues.php | 6 ++++++ tests/Unit/Models/StoredWorkflowTest.php | 6 ------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Models/StoredWorkflow.php b/src/Models/StoredWorkflow.php index 03c948b..bf0023a 100644 --- a/src/Models/StoredWorkflow.php +++ b/src/Models/StoredWorkflow.php @@ -128,11 +128,7 @@ public function active(): self if ($active->status::class === WorkflowContinuedStatus::class) { $active = $this->activeWorkflow() - ->first() ?: $active; - } - - while ($active->status::class === WorkflowContinuedStatus::class && ($next = $active->continuedWorkflows()->first())) { - $active = $next; + ->first(); } return $active; diff --git a/src/Traits/Continues.php b/src/Traits/Continues.php index 7f2733c..2cfcff6 100644 --- a/src/Traits/Continues.php +++ b/src/Traits/Continues.php @@ -38,6 +38,12 @@ public static function continueAsNew(...$arguments): PromiseInterface $rootWorkflow->children() ->wherePivot('parent_index', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) ->detach($context->storedWorkflow); + } else { + $context->storedWorkflow->children() + ->attach($newWorkflow->storedWorkflow, [ + 'parent_index' => StoredWorkflow::ACTIVE_WORKFLOW_INDEX, + 'parent_now' => $context->now, + ]); } } diff --git a/tests/Unit/Models/StoredWorkflowTest.php b/tests/Unit/Models/StoredWorkflowTest.php index df1f5fb..95c2148 100644 --- a/tests/Unit/Models/StoredWorkflowTest.php +++ b/tests/Unit/Models/StoredWorkflowTest.php @@ -224,12 +224,6 @@ public function testActiveWorkflowWithMultipleContinuations(): void ]); $rootWorkflow->children() - ->attach($intermediateWorkflow, [ - 'parent_index' => StoredWorkflow::ACTIVE_WORKFLOW_INDEX, - 'parent_now' => now(), - ]); - - $intermediateWorkflow->children() ->attach($finalWorkflow, [ 'parent_index' => StoredWorkflow::ACTIVE_WORKFLOW_INDEX, 'parent_now' => now(), From 4e5f13287706e8ba3818915d5ab04853a5e4418f Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Sun, 13 Jul 2025 23:39:32 +0000 Subject: [PATCH 14/19] Ignore parents --- src/Workflow.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Workflow.php b/src/Workflow.php index 3e3aacd..60fc7b9 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -80,6 +80,8 @@ public function query($method) public function middleware() { $parentWorkflow = $this->storedWorkflow->parents() + ->wherePivot('parent_index', '!=', StoredWorkflow::CONTINUE_PARENT_INDEX) + ->wherePivot('parent_index', '!=', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) ->first(); if ($parentWorkflow) { @@ -122,6 +124,8 @@ public function handle(): void } $parentWorkflow = $this->storedWorkflow->parents() + ->wherePivot('parent_index', '!=', StoredWorkflow::CONTINUE_PARENT_INDEX) + ->wherePivot('parent_index', '!=', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) ->first(); $log = $this->storedWorkflow->logs() From 1ceab18aa2e8a5542867857f37cef041224cd59a Mon Sep 17 00:00:00 2001 From: Travis Austin Date: Thu, 17 Jul 2025 11:50:36 -0700 Subject: [PATCH 15/19] fix: child workflows that call continueAsNew were not dispatching parent workflow at completion --- src/Workflow.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Workflow.php b/src/Workflow.php index 60fc7b9..1b85521 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -128,6 +128,19 @@ public function handle(): void ->wherePivot('parent_index', '!=', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) ->first(); + // If this workflow is a continued workflow that was initiated as a child workflow, + // then the parent workflow will be the parent of the parent workflow. + if (!$parentWorkflow) { + $parentWorkflow = $this->storedWorkflow->active()->parents() + ->wherePivot('parent_index', '!=', StoredWorkflow::CONTINUE_PARENT_INDEX) + ->wherePivot('parent_index', '=', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) + ->first()?->parents() + ->wherePivot('parent_index', '!=', StoredWorkflow::CONTINUE_PARENT_INDEX) + ->wherePivot('parent_index', '!=', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) + ->first() + ; + } + $log = $this->storedWorkflow->logs() ->whereIndex($this->index) ->first(); From 4623a0320d46c6c37f4b7eddc43c56a6413d9cac Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Fri, 18 Jul 2025 16:08:03 -0500 Subject: [PATCH 16/19] Update Workflow.php --- src/Workflow.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Workflow.php b/src/Workflow.php index 1b85521..60fc7b9 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -128,19 +128,6 @@ public function handle(): void ->wherePivot('parent_index', '!=', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) ->first(); - // If this workflow is a continued workflow that was initiated as a child workflow, - // then the parent workflow will be the parent of the parent workflow. - if (!$parentWorkflow) { - $parentWorkflow = $this->storedWorkflow->active()->parents() - ->wherePivot('parent_index', '!=', StoredWorkflow::CONTINUE_PARENT_INDEX) - ->wherePivot('parent_index', '=', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) - ->first()?->parents() - ->wherePivot('parent_index', '!=', StoredWorkflow::CONTINUE_PARENT_INDEX) - ->wherePivot('parent_index', '!=', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) - ->first() - ; - } - $log = $this->storedWorkflow->logs() ->whereIndex($this->index) ->first(); From fbd1fa5203bcbadddfcd87c94a998ab1392d1199 Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Fri, 18 Jul 2025 21:41:49 +0000 Subject: [PATCH 17/19] continueAsNew child workflow --- src/Traits/Continues.php | 21 +++++++++++++++- .../ParentContinueAsNewChildWorkflowTest.php | 25 +++++++++++++++++++ .../TestChildContinueAsNewWorkflow.php | 19 ++++++++++++++ .../TestParentContinueAsNewChildWorkflow.php | 17 +++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/ParentContinueAsNewChildWorkflowTest.php create mode 100644 tests/Fixtures/TestChildContinueAsNewWorkflow.php create mode 100644 tests/Fixtures/TestParentContinueAsNewChildWorkflow.php diff --git a/src/Traits/Continues.php b/src/Traits/Continues.php index 2cfcff6..2294b63 100644 --- a/src/Traits/Continues.php +++ b/src/Traits/Continues.php @@ -16,8 +16,25 @@ public static function continueAsNew(...$arguments): PromiseInterface $context = self::$context; if (! $context->replaying) { + $parentWorkflow = $context->storedWorkflow->parents() + ->wherePivot('parent_index', '!=', StoredWorkflow::CONTINUE_PARENT_INDEX) + ->wherePivot('parent_index', '!=', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) + ->withPivot('parent_index') + ->first(); + $newWorkflow = self::make($context->storedWorkflow->class); - $newWorkflow->start(...$arguments); + + if ($parentWorkflow) { + $parentWorkflow->children() + ->attach($newWorkflow->storedWorkflow, [ + 'parent_index' => $parentWorkflow->pivot->parent_index, + 'parent_now' => $context->now, + ]); + + $parentWorkflow->children() + ->wherePivot('parent_index', $parentWorkflow->pivot->parent_index) + ->detach($context->storedWorkflow); + } $newWorkflow->storedWorkflow->parents() ->attach($context->storedWorkflow, [ @@ -45,6 +62,8 @@ public static function continueAsNew(...$arguments): PromiseInterface 'parent_now' => $context->now, ]); } + + $newWorkflow->start(...$arguments); } self::$context = $context; diff --git a/tests/Feature/ParentContinueAsNewChildWorkflowTest.php b/tests/Feature/ParentContinueAsNewChildWorkflowTest.php new file mode 100644 index 0000000..78961e8 --- /dev/null +++ b/tests/Feature/ParentContinueAsNewChildWorkflowTest.php @@ -0,0 +1,25 @@ +start(); + + while ($workflow->running()); + + $this->assertEquals(WorkflowCompletedStatus::class, $workflow->status()); + $this->assertEquals('parent_child_done', $workflow->output()); + } +} diff --git a/tests/Fixtures/TestChildContinueAsNewWorkflow.php b/tests/Fixtures/TestChildContinueAsNewWorkflow.php new file mode 100644 index 0000000..e61faf4 --- /dev/null +++ b/tests/Fixtures/TestChildContinueAsNewWorkflow.php @@ -0,0 +1,19 @@ += $totalCount) { + return 'child_done'; + } + return yield WorkflowStub::continueAsNew($count + 1, $totalCount); + } +} diff --git a/tests/Fixtures/TestParentContinueAsNewChildWorkflow.php b/tests/Fixtures/TestParentContinueAsNewChildWorkflow.php new file mode 100644 index 0000000..3ead2e8 --- /dev/null +++ b/tests/Fixtures/TestParentContinueAsNewChildWorkflow.php @@ -0,0 +1,17 @@ + Date: Fri, 18 Jul 2025 22:38:18 +0000 Subject: [PATCH 18/19] Add coverage --- tests/Unit/WorkflowTest.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/Unit/WorkflowTest.php b/tests/Unit/WorkflowTest.php index e92a32b..b816e1b 100644 --- a/tests/Unit/WorkflowTest.php +++ b/tests/Unit/WorkflowTest.php @@ -354,4 +354,37 @@ public function testContinueAsNew(): void $this->assertSame(1, $storedWorkflow->continuedWorkflows()->count()); } + + public function testContinueAsNewWithParentWorkflow(): void + { + $parentWorkflow = WorkflowStub::load(WorkflowStub::make(TestContinueAsNewWorkflow::class)->id()); + $storedParentWorkflow = StoredWorkflow::findOrFail($parentWorkflow->id()); + $storedParentWorkflow->arguments = Serializer::serialize([]); + $storedParentWorkflow->save(); + + $storedWorkflow = StoredWorkflow::create([ + 'class' => TestContinueAsNewWorkflow::class, + 'arguments' => Serializer::serialize([0, 3]), + 'status' => WorkflowPendingStatus::class, + ]); + + $storedWorkflow->parents() + ->attach($storedParentWorkflow, [ + 'parent_index' => 0, + 'parent_now' => now(), + ]); + + $storedWorkflow->logs() + ->create([ + 'index' => 0, + 'now' => now(), + 'class' => TestCountActivity::class, + 'result' => Serializer::serialize(0), + ]); + + $workflow = new TestContinueAsNewWorkflow($storedWorkflow); + $workflow->handle(); + + $this->assertSame(1, $storedWorkflow->continuedWorkflows()->count()); + } } From b1cf89fa7b4a52e48477975bd3914cf2131e5c10 Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Sat, 19 Jul 2025 05:31:32 +0000 Subject: [PATCH 19/19] Improve test --- tests/Feature/ParentContinueAsNewChildWorkflowTest.php | 2 +- tests/Fixtures/TestChildContinueAsNewWorkflow.php | 8 ++++++-- tests/Fixtures/TestParentContinueAsNewChildWorkflow.php | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/Feature/ParentContinueAsNewChildWorkflowTest.php b/tests/Feature/ParentContinueAsNewChildWorkflowTest.php index 78961e8..2584d10 100644 --- a/tests/Feature/ParentContinueAsNewChildWorkflowTest.php +++ b/tests/Feature/ParentContinueAsNewChildWorkflowTest.php @@ -20,6 +20,6 @@ public function testChildWorkflowContinuesAsNew(): void while ($workflow->running()); $this->assertEquals(WorkflowCompletedStatus::class, $workflow->status()); - $this->assertEquals('parent_child_done', $workflow->output()); + $this->assertEquals('parent_child_workflow_3', $workflow->output()); } } diff --git a/tests/Fixtures/TestChildContinueAsNewWorkflow.php b/tests/Fixtures/TestChildContinueAsNewWorkflow.php index e61faf4..1fd8b60 100644 --- a/tests/Fixtures/TestChildContinueAsNewWorkflow.php +++ b/tests/Fixtures/TestChildContinueAsNewWorkflow.php @@ -4,16 +4,20 @@ namespace Tests\Fixtures; +use Workflow\ActivityStub; use Workflow\Workflow; use Workflow\WorkflowStub; class TestChildContinueAsNewWorkflow extends Workflow { - public function execute(int $count = 0, int $totalCount = 2) + public function execute(int $count = 0, int $totalCount = 3) { + $activityResult = yield ActivityStub::make(TestCountActivity::class, $count); + if ($count >= $totalCount) { - return 'child_done'; + return 'child_workflow_' . $activityResult; } + return yield WorkflowStub::continueAsNew($count + 1, $totalCount); } } diff --git a/tests/Fixtures/TestParentContinueAsNewChildWorkflow.php b/tests/Fixtures/TestParentContinueAsNewChildWorkflow.php index 3ead2e8..bda091f 100644 --- a/tests/Fixtures/TestParentContinueAsNewChildWorkflow.php +++ b/tests/Fixtures/TestParentContinueAsNewChildWorkflow.php @@ -12,6 +12,7 @@ class TestParentContinueAsNewChildWorkflow extends Workflow public function execute() { $childResult = yield ChildWorkflowStub::make(TestChildContinueAsNewWorkflow::class); + return 'parent_' . $childResult; } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy