|
13 | 13 | use Illuminate\Http\JsonResponse; |
14 | 14 | use Illuminate\Http\Request; |
15 | 15 | use Illuminate\Support\Str; |
| 16 | +use Illuminate\Validation\ValidationException; |
16 | 17 | use Workflow\V2\Contracts\WorkflowTaskBridge; |
17 | 18 | use Workflow\V2\Exceptions\StructuralLimitExceededException; |
18 | 19 | use Workflow\V2\Models\WorkflowTask; |
@@ -487,6 +488,8 @@ public function completeWorkflowTask(Request $request, string $taskId): JsonResp |
487 | 488 | 'commands.*.timeout_seconds' => ['nullable', 'integer', 'min:0'], |
488 | 489 | ]); |
489 | 490 |
|
| 491 | + $this->validateWorkflowTaskCommandScopes($validated['commands']); |
| 492 | + |
490 | 493 | if ($response = $this->guardWorkflowTaskOwnership( |
491 | 494 | $request, |
492 | 495 | $namespace, |
@@ -529,6 +532,122 @@ public function completeWorkflowTask(Request $request, string $taskId): JsonResp |
529 | 532 | ], $this->workflowOutcomeStatus($outcome['reason'])); |
530 | 533 | } |
531 | 534 |
|
| 535 | + /** |
| 536 | + * @param list<array<string, mixed>> $commands |
| 537 | + * |
| 538 | + * @throws ValidationException |
| 539 | + */ |
| 540 | + private function validateWorkflowTaskCommandScopes(array $commands): void |
| 541 | + { |
| 542 | + $errors = []; |
| 543 | + |
| 544 | + foreach ($commands as $index => $command) { |
| 545 | + $type = $command['type'] ?? null; |
| 546 | + |
| 547 | + if (! is_string($type)) { |
| 548 | + continue; |
| 549 | + } |
| 550 | + |
| 551 | + if ($this->hasCommandValue($command, 'retry_policy') |
| 552 | + && ! in_array($type, ['schedule_activity', 'start_child_workflow'], true) |
| 553 | + ) { |
| 554 | + $errors["commands.{$index}.retry_policy"][] = |
| 555 | + 'retry_policy is only supported for schedule_activity and start_child_workflow commands.'; |
| 556 | + } |
| 557 | + |
| 558 | + foreach (['start_to_close_timeout', 'schedule_to_start_timeout', 'schedule_to_close_timeout', 'heartbeat_timeout'] as $field) { |
| 559 | + if ($this->hasCommandValue($command, $field) && $type !== 'schedule_activity') { |
| 560 | + $errors["commands.{$index}.{$field}"][] = |
| 561 | + "{$field} is only supported for schedule_activity commands."; |
| 562 | + } |
| 563 | + } |
| 564 | + |
| 565 | + foreach (['execution_timeout_seconds', 'run_timeout_seconds'] as $field) { |
| 566 | + if ($this->hasCommandValue($command, $field) && $type !== 'start_child_workflow') { |
| 567 | + $errors["commands.{$index}.{$field}"][] = |
| 568 | + "{$field} is only supported for start_child_workflow commands."; |
| 569 | + } |
| 570 | + } |
| 571 | + |
| 572 | + if ($this->hasCommandValue($command, 'non_retryable') |
| 573 | + && ! in_array($type, ['fail_workflow', 'fail_update'], true) |
| 574 | + ) { |
| 575 | + $errors["commands.{$index}.non_retryable"][] = |
| 576 | + 'non_retryable is only supported for fail_workflow and fail_update commands.'; |
| 577 | + } |
| 578 | + |
| 579 | + if ($type === 'schedule_activity') { |
| 580 | + $this->validateActivityTimeoutEnvelope($command, $index, $errors); |
| 581 | + } |
| 582 | + |
| 583 | + if ($type === 'start_child_workflow') { |
| 584 | + $this->validateChildWorkflowTimeoutEnvelope($command, $index, $errors); |
| 585 | + } |
| 586 | + } |
| 587 | + |
| 588 | + if ($errors !== []) { |
| 589 | + throw ValidationException::withMessages($errors); |
| 590 | + } |
| 591 | + } |
| 592 | + |
| 593 | + /** |
| 594 | + * @param array<string, mixed> $command |
| 595 | + */ |
| 596 | + private function hasCommandValue(array $command, string $field): bool |
| 597 | + { |
| 598 | + return array_key_exists($field, $command) && $command[$field] !== null; |
| 599 | + } |
| 600 | + |
| 601 | + /** |
| 602 | + * @param array<string, mixed> $command |
| 603 | + * @param array<string, list<string>> $errors |
| 604 | + */ |
| 605 | + private function validateActivityTimeoutEnvelope(array $command, int $index, array &$errors): void |
| 606 | + { |
| 607 | + $startToClose = $this->optionalCommandInt($command, 'start_to_close_timeout'); |
| 608 | + $scheduleToStart = $this->optionalCommandInt($command, 'schedule_to_start_timeout'); |
| 609 | + $scheduleToClose = $this->optionalCommandInt($command, 'schedule_to_close_timeout'); |
| 610 | + $heartbeat = $this->optionalCommandInt($command, 'heartbeat_timeout'); |
| 611 | + |
| 612 | + if ($heartbeat !== null && $startToClose !== null && $heartbeat > $startToClose) { |
| 613 | + $errors["commands.{$index}.heartbeat_timeout"][] = |
| 614 | + 'heartbeat_timeout cannot exceed start_to_close_timeout.'; |
| 615 | + } |
| 616 | + |
| 617 | + if ($startToClose !== null && $scheduleToClose !== null && $startToClose > $scheduleToClose) { |
| 618 | + $errors["commands.{$index}.start_to_close_timeout"][] = |
| 619 | + 'start_to_close_timeout cannot exceed schedule_to_close_timeout.'; |
| 620 | + } |
| 621 | + |
| 622 | + if ($scheduleToStart !== null && $scheduleToClose !== null && $scheduleToStart > $scheduleToClose) { |
| 623 | + $errors["commands.{$index}.schedule_to_start_timeout"][] = |
| 624 | + 'schedule_to_start_timeout cannot exceed schedule_to_close_timeout.'; |
| 625 | + } |
| 626 | + } |
| 627 | + |
| 628 | + /** |
| 629 | + * @param array<string, mixed> $command |
| 630 | + * @param array<string, list<string>> $errors |
| 631 | + */ |
| 632 | + private function validateChildWorkflowTimeoutEnvelope(array $command, int $index, array &$errors): void |
| 633 | + { |
| 634 | + $executionTimeout = $this->optionalCommandInt($command, 'execution_timeout_seconds'); |
| 635 | + $runTimeout = $this->optionalCommandInt($command, 'run_timeout_seconds'); |
| 636 | + |
| 637 | + if ($executionTimeout !== null && $runTimeout !== null && $runTimeout > $executionTimeout) { |
| 638 | + $errors["commands.{$index}.run_timeout_seconds"][] = |
| 639 | + 'run_timeout_seconds cannot exceed execution_timeout_seconds.'; |
| 640 | + } |
| 641 | + } |
| 642 | + |
| 643 | + /** |
| 644 | + * @param array<string, mixed> $command |
| 645 | + */ |
| 646 | + private function optionalCommandInt(array $command, string $field): ?int |
| 647 | + { |
| 648 | + return is_int($command[$field] ?? null) ? $command[$field] : null; |
| 649 | + } |
| 650 | + |
532 | 651 | /** |
533 | 652 | * Heartbeat a claimed workflow task to extend its lease. |
534 | 653 | */ |
|
0 commit comments