Skip to content

Refactor executor to iterative loop, add condition evaluation, improve validation#36

Merged
lam0819 merged 3 commits intomainfrom
claude/review-codebase-fv2VL
Apr 15, 2026
Merged

Refactor executor to iterative loop, add condition evaluation, improve validation#36
lam0819 merged 3 commits intomainfrom
claude/review-codebase-fv2VL

Conversation

@lam0819
Copy link
Copy Markdown
Contributor

@lam0819 lam0819 commented Apr 15, 2026

Description

This PR makes significant improvements to the workflow execution engine, focusing on robustness, correctness, and scalability:

  1. Executor Refactoring: Converts the recursive processWorkflow() method to an iterative loop with a safety bound to prevent infinite loops and stack overflows on long workflows.

  2. Conditional Step Execution: Implements proper condition evaluation in executeStep(). Steps whose conditions don't match are now skipped (marked completed without running the action) so downstream transitions continue to flow.

  3. Enhanced Condition Evaluator: Extends ConditionEvaluator to support:

    • Truthy key form: "key" or "!key" to check if a dotted path is truthy/falsy
    • Proper literal parsing: true, false, null, integers, floats, and quoted strings
    • Better error messages for malformed conditions
  4. Validation on Instance Restore: WorkflowInstance::fromArray() now validates that:

    • All completed_steps and failed_steps reference steps defined in the workflow
    • The restored state is a valid WorkflowState value
    • Required fields (id, state) are present and well-formed
    • Corrupt or foreign payloads throw InvalidWorkflowStateException
  5. Builder Improvements:

    • Auto-chaining: Sequential steps are automatically connected via transitions
    • Monotonic step ID counter prevents collisions between explicit and auto-generated IDs
    • Support for nested when() conditions that accumulate
  6. Exception Handling: Changed from catching Exception to \Throwable throughout for better error handling, and simplified exception wrapping logic in executeStep().

  7. Security & Reliability:

    • UUID v4 generation now uses random_bytes() instead of mt_rand()
    • HTTP action gains timeout, TLS verification, and redirect controls
    • Removed unused factory methods from WorkflowException

Type of Change

  • New feature (conditional step execution, truthy conditions)
  • Bug fix (infinite loop protection, validation on restore)
  • Refactoring (executor recursion → iteration)
  • Performance improvement (stack safety for long workflows)

Testing

  • Added BuilderExecutionTest.php: End-to-end tests for builder-defined workflows with multi-step execution and conditional branching
  • Added ConditionEvaluatorTest.php: Comprehensive tests for condition evaluation (comparisons, truthy keys, negation, edge cases)
  • Added ExecutorIterationTest.php: Tests that 200-step workflows execute without stack overflow
  • Added WorkflowInstanceRestoreTest.php: Validation tests for instance restoration from arrays
  • Added WorkflowBuilderAutoIdTest.php: Tests for auto-generated step ID collision avoidance
  • All new and existing unit tests pass

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Hard-to-understand areas are commented (iteration bounds, condition forms, validation logic)
  • Tests added for all new functionality
  • All new and existing tests pass
  • No new warnings generated

Additional Notes

The executor refactoring is a significant architectural change that improves both correctness and performance. The iterative approach with a bounded iteration count (maxIterations = totalSteps * 2 + 1) prevents pathological workflows from looping forever while still allowing legitimate conditional branching. The +1 guard handles zero-step workflows.

Condition evaluation now supports both comparison expressions (user.plan === 'premium') and truthy key checks (review.approved, !review.rejected), making workflow definitions more expressive and readable.

Instance validation on restore prevents silent corruption when loading workflows from storage, catching issues early with clear error messages.

https://claude.ai/code/session_01FfatPksXmoqBpfmELb8fza

claude added 3 commits April 15, 2026 03:45
… handling

The fluent WorkflowBuilder API never generated transitions between steps,
so the Executor's transition-driven navigation stopped after the first
step and silently marked multi-step workflows as completed. Builder-based
step conditions were also stored on the Step but never consulted during
execution, and ConditionEvaluator compared every right-hand side as a
string, breaking strict comparisons against booleans, integers, nulls,
and other typed values. Error handling caught Exception rather than
Throwable, leaving instances in an inconsistent state when actions threw
Errors, and classified exceptions by substring-matching their messages.

- WorkflowBuilder::addStep now appends a transition from the previous
  step so sequential builder workflows actually chain.
- when() conditions accumulate into Step::$conditions and are evaluated
  in Executor::executeStep; steps whose conditions don't match are
  marked complete (skipped) so downstream transitions still flow.
- ConditionEvaluator parses RHS literals by type (bool/null/int/float/
  quoted string) and supports the truthy-key form ("review.approved",
  "!review.approved") used by QuickWorkflowBuilder.
- Executor now catches Throwable in execute(), executeStep(), the retry
  loop, and the timeout wrapper; WorkflowFailedEvent and
  StepExecutionException::fromException accept Throwable.
- Removed the brittle str_contains exception classifier; rely on the
  typed exceptions thrown by executeAction().

Adds tests covering multi-step builder execution, conditional builder
branches, and the typed ConditionEvaluator behavior. Test count goes
from 93 to 107 (259 assertions).
…ction

Round 2 of the codebase review fixes builds on the previous commit by
replacing the remaining footguns flagged in the audit.

- Executor: replace the recursive processWorkflow/executeStep call chain
  with a bounded iterative loop so long workflows (200+ steps) no longer
  consume stack frames. Adds a safety iteration cap driven by the total
  step count to catch cyclic transition graphs.
- Executor: cap the retry backoff at 2s (MAX_BACKOFF_MICROSECONDS) and
  extract calculateBackoff() so a misconfigured step cannot block a
  worker for minutes on exponential sleep.
- Executor: harden the pcntl timeout wrapper — enable async signals via
  pcntl_async_signals, restore the previous handler and async-mode on
  exit, and clarify in the warning log that timeout enforcement is
  CLI/queue-only.
- WorkflowInstance::fromArray now validates the restored payload:
  missing/invalid id, unknown state values, non-list completed_steps,
  completed/current step references to steps that don't exist in the
  definition — all raise InvalidWorkflowStateException instead of
  silently producing a broken instance.
- WorkflowBuilder: auto-generated step IDs (then/startWith/email/delay/
  http/condition) now come from a monotonic counter that skips past any
  IDs already claimed by explicit addStep() calls, so mixing "step_1"
  with then() can no longer throw duplicateStepId.
- BaseAction and HttpAction: catch Throwable rather than Exception so
  Errors from action code flow through the normal failure path.
- HttpAction: add CURLOPT_CONNECTTIMEOUT, explicit CURLOPT_SSL_VERIFYPEER/
  VERIFYHOST, a bounded CURLOPT_MAXREDIRS, and an http/https protocol
  allowlist (using CURLOPT_PROTOCOLS_STR when available) so redirects
  cannot hand the request off to file://, gopher://, etc.
- Uuid: swap mt_rand() for random_bytes(), matching the docblock's
  "cryptographically random" claim.
- SimpleWorkflow: drop uniqid() in favor of Uuid::v4() for unique
  workflow instance IDs.
- ConditionAction: remove unused Attributes\Condition import.

Adds 9 new tests covering Executor iteration (200-step workflow),
fromArray validation, and builder auto-ID collisions. Test count goes
from 107 to 116 (277 assertions).
WorkflowException::fromContext() and fromInstance() call `new static(...)`
with the base constructor signature, but every concrete subclass
(StepExecutionException, ActionNotFoundException, InvalidWorkflowState
Exception, ...) has a different constructor. These factories were
PHPStan-ignored and are never called in the codebase — they would throw
ArgumentCountError if anyone tried to use them. Remove them.

InMemoryStorage::findInstances with filter criteria returned the raw
array_filter result, which preserves the original string keys. Wrap it
with array_values so callers receive a sequential list matching the
no-criteria path.
@lam0819 lam0819 merged commit 841ebf4 into main Apr 15, 2026
7 checks passed
@lam0819 lam0819 deleted the claude/review-codebase-fv2VL branch April 15, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants