diff --git a/src/ChildWorkflow.php b/src/ChildWorkflow.php index 631b3c5a..58d6e6ad 100644 --- a/src/ChildWorkflow.php +++ b/src/ChildWorkflow.php @@ -55,17 +55,19 @@ public function uniqueId() public function handle() { - $workflow = $this->parentWorkflow->toWorkflow(); + if (! $this->parentWorkflow->hasLogByIndex($this->index)) { + $this->parentWorkflow->toWorkflow() + ->next($this->index, $this->now, $this->storedWorkflow->class, $this->return, shouldSignal: false); + } - try { - if ($this->parentWorkflow->hasLogByIndex($this->index)) { + if ($this->shouldWakeParent()) { + $workflow = $this->parentWorkflow->toWorkflow(); + try { $workflow->resume(); - } else { - $workflow->next($this->index, $this->now, $this->storedWorkflow->class, $this->return); - } - } catch (TransitionNotFound) { - if ($workflow->running()) { - $this->release(); + } catch (TransitionNotFound) { + if ($workflow->running()) { + $this->release(); + } } } } @@ -76,4 +78,23 @@ public function middleware() new WithoutOverlappingMiddleware($this->parentWorkflow->id, WithoutOverlappingMiddleware::ACTIVITY, 0, 15), ]; } + + private function shouldWakeParent(): bool + { + $children = $this->parentWorkflow->children() + ->wherePivot('parent_index', '<', StoredWorkflow::ACTIVE_WORKFLOW_INDEX) + ->get(); + + if ($children->isEmpty()) { + return true; + } + + $childIndices = $children->pluck('pivot.parent_index'); + + $logCount = $this->parentWorkflow->logs() + ->whereIn('index', $childIndices) + ->count(); + + return $logCount >= $childIndices->count(); + } } diff --git a/tests/Unit/ChildWorkflowTest.php b/tests/Unit/ChildWorkflowTest.php index 49da05bf..08bfb83c 100644 --- a/tests/Unit/ChildWorkflowTest.php +++ b/tests/Unit/ChildWorkflowTest.php @@ -10,6 +10,7 @@ use Workflow\ChildWorkflow; use Workflow\Models\StoredWorkflow; use Workflow\Serializers\Serializer; +use Workflow\States\WorkflowCreatedStatus; use Workflow\States\WorkflowRunningStatus; use Workflow\WorkflowStub; @@ -36,4 +37,92 @@ public function testHandleReleasesWhenParentWorkflowIsRunning(): void $this->assertSame(1, $storedParent->logs()->count()); $this->assertSame(WorkflowRunningStatus::class, $storedParent->refresh()->status::class); } + + public function testHandleDoesNotWakeParentWhenSiblingsArePending(): void + { + $parent = WorkflowStub::make(TestWorkflow::class); + $storedParent = StoredWorkflow::findOrFail($parent->id()); + $storedParent->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowCreatedStatus::class, + ]); + + $storedChild1 = StoredWorkflow::create([ + 'class' => TestChildWorkflow::class, + 'arguments' => Serializer::serialize([]), + ]); + $storedChild2 = StoredWorkflow::create([ + 'class' => TestChildWorkflow::class, + 'arguments' => Serializer::serialize([]), + ]); + $storedChild3 = StoredWorkflow::create([ + 'class' => TestChildWorkflow::class, + 'arguments' => Serializer::serialize([]), + ]); + + $storedChild1->parents() + ->attach($storedParent, [ + 'parent_index' => 0, + 'parent_now' => now(), + ]); + $storedChild2->parents() + ->attach($storedParent, [ + 'parent_index' => 1, + 'parent_now' => now(), + ]); + $storedChild3->parents() + ->attach($storedParent, [ + 'parent_index' => 2, + 'parent_now' => now(), + ]); + + // Only the first child completes; two siblings are still pending + $job = new ChildWorkflow(0, now()->toDateTimeString(), $storedChild1, true, $storedParent); + $job->handle(); + + // Log written but parent not dispatched (still in created status) + $this->assertSame(1, $storedParent->logs()->count()); + $this->assertSame(WorkflowCreatedStatus::class, $storedParent->refresh()->status::class); + } + + public function testHandleWakesParentOnLastSiblingCompletion(): void + { + $parent = WorkflowStub::make(TestWorkflow::class); + $storedParent = StoredWorkflow::findOrFail($parent->id()); + $storedParent->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowCreatedStatus::class, + ]); + + $storedChild1 = StoredWorkflow::create([ + 'class' => TestChildWorkflow::class, + 'arguments' => Serializer::serialize([]), + ]); + $storedChild2 = StoredWorkflow::create([ + 'class' => TestChildWorkflow::class, + 'arguments' => Serializer::serialize([]), + ]); + + $storedChild1->parents() + ->attach($storedParent, [ + 'parent_index' => 0, + 'parent_now' => now(), + ]); + $storedChild2->parents() + ->attach($storedParent, [ + 'parent_index' => 1, + 'parent_now' => now(), + ]); + + // First child completes — parent should not be woken + $job1 = new ChildWorkflow(0, now()->toDateTimeString(), $storedChild1, true, $storedParent); + $job1->handle(); + $this->assertSame(WorkflowCreatedStatus::class, $storedParent->refresh()->status::class); + + // Second (last) child completes — parent should be dispatched (pending) + $job2 = new ChildWorkflow(1, now()->toDateTimeString(), $storedChild2, true, $storedParent); + $job2->handle(); + $this->assertSame(2, $storedParent->logs()->count()); + $this->assertNotSame(WorkflowCreatedStatus::class, $storedParent->refresh()->status::class); + } }