Skip to content

Stop owned processes when their coroutine exits - #728

Merged
binaryfire merged 15 commits into
0.4from
fix/process-coroutine-cleanup
Nov 2, 2026
Merged

binaryfire merged 15 commits into
0.4from
fix/process-coroutine-cleanup

Conversation

@binaryfire

@binaryfire binaryfire commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Asynchronous processes left behind by their starting coroutine could retain native process resources until shutdown. Output callback failures could also interrupt cleanup in framework callers using Symfony Process directly.

Track asynchronous processes weakly in their owning coroutine and stop any remaining children when that coroutine exits. Explicitly detached processes remain detached, and processes started outside a coroutine retain their existing lifetime.

Share the output-failure cleanup guard with schema commands, queue listeners and Composer operations. Move suitable test fixtures onto the Process API, retain independent cleanup in termination regressions, and document when tests should use the framework API or Symfony Process directly.

Targeted tests cover coroutine exit, cancellation, callback failures, process pools and non-coroutine execution. Source analysis and affected tests pass; the full suite and service matrices run in CI.

View guided diff Turn on auto-fix

Note

Stop owned processes when their coroutine exits

  • Adds a coroutine process registry in ProcessState.php: processes started in a coroutine are tracked in a WeakMap and stopped when that coroutine exits (returns or is canceled). Detached processes via create_new_console and starts outside a coroutine are not tracked.
  • PendingProcess.start registers each async process with the current coroutine; pools inherit the same ownership.
  • Moves GuardsProcessOutput to support/src/Traits and applies it to schema dumps, queue worker listeners, and Composer package operations so a throwing output callback stops the subprocess first.
  • Documents coroutine ownership in processes.md, porting-from-laravel.md, and testing.md. Updates many tests to use the Hypervel process factory and clean up in finally.
  • Risk: processes that previously kept running after a coroutine exited are now stopped automatically — check PendingProcess.start registration and any callers relying on processes outliving their starting coroutine.

Macroscope summarized 57e74a4.

Summary by CodeRabbit

  • New Features
    • Asynchronous processes started within a coroutine now stop automatically when that coroutine exits, including when it is canceled. Detached processes are not stopped automatically.
    • Process pools follow the same lifecycle behavior as their owning coroutine.
  • Bug Fixes
    • Processes are now stopped when output-handling callbacks fail, while the original error is preserved.
  • Documentation
    • Clarified process ownership, detachment, and cleanup guidance for asynchronous processes and test fixtures.

Track native process handles weakly in coroutine-local state and reuse pool cleanup at owner exit. Preserve detached, fake and non-coroutine starts. Share the output guard through Support while retaining interrupted-stop behavior.

Verified ProcessTest, ProcessNonCoroutineTest, real concurrency consumers, source/type analysis and facade consistency.
Guard require and remove output callbacks so their exceptions cannot leave Composer changing the project in the background.

Both real-child regressions fail before correction; SupportComposerTest passes.
Apply the shared output guard to MySQL, MariaDB and PostgreSQL dump callbacks without changing schema extension points.

Real-child regressions fail before correction; MySQL and PostgreSQL schema-state test files pass.
Prevent a caller-supplied output handler from abandoning the current worker process after an exception.

The regression fails before correction; QueueListenerTest passes.
Declare the Process component used directly by Foundation console commands and Composer integration, using the framework's existing version constraint.

Package metadata parses successfully; no root dependency versions change.
Start the fixture through the Process API while retaining graceful shutdown before log inspection.

SwooleMaintenanceTimerWorkerRecycleTest passes.
Use coroutine-owned subprocesses and return ProcessResult from the profiler helper, preserving output, exit-code and report-cleanup assertions.

ProfileScriptTest passes.
Retain each child before starting it and stop all started readers if startup, waiting or an assertion fails.

FilesystemNonCoroutineTest passes.
Avoid signaling the test runner's process group when the fixture has no process ID.

HttpClientStreamingTest passes with its established platform skip.
Keep real workers and supervisor processes inside exception-safe cleanup while preserving mocked behavior and assertions.

Both changed Horizon files pass against an isolated temporary Redis instance.
Keep process-group cleanup limited to a fixture with a known process ID.

ShutdownOnInterruptListenerTest passes with its established Swoole skips.
Guard nullable process IDs and remove the startup sleep because Symfony obtains the PID during start.

CommanderServeTest passes directly and through package:test with one worker.
Explain coroutine-exit cleanup, explicit detachment and raw Symfony ownership. Recommend the framework Process API for fixtures while retaining explicit shutdown where timing or process trees require it.

Documentation matches the reviewed source and behavior tests.
Integrate the merged framework and integration cancellation fixes before publishing process ownership and callback cleanup. Preserve cancellation-aware stop, pool cleanup and regression coverage alongside the previously reviewed weak process tracking and shared output guard.

The merge needs no conflict resolution. The process change remains 31 files; differences from its reviewed patch only reflect already-landed dependencies, imports and adjacent cancellation changes. Complete Process and non-coroutine tests, targeted source analysis and formatting pass.
@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between ae4f012 and d4084e1.

Files selected for processing: 31
  • src/database/src/Schema/MySqlSchemaState.php
  • src/database/src/Schema/PostgresSchemaState.php
  • src/database/src/Schema/SchemaState.php
  • src/docs/porting-from-laravel.md
  • src/docs/processes.md
  • src/docs/testing.md
  • src/foundation/composer.json
  • src/process/README.md
  • src/process/composer.json
  • src/process/src/InvokedProcess.php
  • src/process/src/PendingProcess.php
  • src/process/src/Pool.php
  • src/process/src/ProcessState.php
  • src/queue/src/Listener.php
  • src/support/composer.json
  • src/support/src/Composer.php
  • src/support/src/Traits/GuardsProcessOutput.php
  • tests/Cache/SwooleMaintenanceTimerWorkerRecycleTest.php
  • tests/Database/DatabaseMySqlSchemaStateTest.php
  • tests/Database/DatabasePostgresSchemaStateTest.php
  • tests/Filesystem/FilesystemNonCoroutineTest.php
  • tests/Http/HttpClientStreamingTest.php
  • tests/Integration/Horizon/Feature/AddSupervisorTest.php
  • tests/Integration/Horizon/Feature/WorkerProcessTest.php
  • tests/Process/ProcessNonCoroutineTest.php
  • tests/Process/ProcessTest.php
  • tests/Queue/QueueListenerTest.php
  • tests/Server/Listeners/ShutdownOnInterruptListenerTest.php
  • tests/Support/SupportComposerTest.php
  • tests/Testbench/CommanderServeTest.php
  • tests/Testing/Profile/ProfileScriptTest.php

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

This change adds coroutine-scoped cleanup for asynchronous processes, guards output callbacks in several process integrations, and updates subprocess tests and process ownership guidance.

Changes: Process lifecycle and cleanup

Layer / File(s) Summary
Coroutine-scoped process ownership
src/process/src/ProcessState.php, src/process/src/PendingProcess.php, src/process/src/Pool.php, src/process/composer.json, src/process/README.md, src/docs/processes.md, src/docs/porting-from-laravel.md, src/docs/testing.md, tests/Process/*
ProcessState tracks processes started in a coroutine and stops them when that coroutine exits. Processes with create_new_console enabled are not registered. The documentation and tests describe ownership, cancellation, detachment, and non-coroutine use.
Output callback failure cleanup
src/support/src/Traits/GuardsProcessOutput.php, src/process/src/InvokedProcess.php, src/process/src/PendingProcess.php, src/database/src/Schema/*, src/queue/src/Listener.php, src/support/src/Composer.php, src/foundation/composer.json, src/support/composer.json, tests/Database/*, tests/Queue/QueueListenerTest.php, tests/Support/SupportComposerTest.php
GuardsProcessOutput moves to Hypervel\Support\Traits and stops processes when output callbacks throw. Schema dumps, queue workers, and Composer package operations use the guard. Tests check exception propagation and process cleanup.
Subprocess test APIs and cleanup
tests/Cache/*, tests/Filesystem/FilesystemNonCoroutineTest.php, tests/Http/HttpClientStreamingTest.php, tests/Integration/Horizon/Feature/*, tests/Server/Listeners/ShutdownOnInterruptListenerTest.php, tests/Testbench/CommanderServeTest.php, tests/Testing/Profile/ProfileScriptTest.php
Subprocess tests use Hypervel process APIs in selected cases. Other test changes add process cleanup in finally blocks or guard process-group signals when a PID is unavailable.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Coroutine
  participant PendingProcess
  participant SymfonyProcess
  participant ProcessState
  participant InvokedProcessPool
  Coroutine->>PendingProcess: start process
  PendingProcess->>SymfonyProcess: start
  PendingProcess->>ProcessState: register unless detached
  Coroutine-->>ProcessState: exit or cancellation
  ProcessState->>InvokedProcessPool: stop tracked processes
Loading

Suggested labels: bug

Merge Risk: Minimal · previous review d4084

No actionable merge blocker is established by the supplied evidence. Merge after the planned tests and checks pass.

Pre-merge checks: Passed 3 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Description check Warning The description explains the problem, behavior change, affected areas, and broad test coverage. It does not follow the required template because it omits the contribution type, explicit supporting-evi… Add the required template sections. Select the contribution type, document the regression test and before/after results, list every verification command with its result, and complete the Before submitting checklist.
Docstring Coverage Warning Docstring coverage is 38.71% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 3 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: stopping processes owned by a coroutine when that coroutine exits.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description explains the problem, behavior change, affected areas, and broad test coverage. It does not follow the required template because it omits the contribution type, explicit supporting-evidence details, verification commands and results, and the required submission checklist.

Resolution

Add the required template sections. Select the contribution type, document the regression test and before/after results, list every verification command with its result, and complete the Before submitting checklist.

Full details: Docstring Coverage

Explanation

Docstring coverage is 38.71% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 31 functions across 24 files. (7 skipped: 7 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@binaryfire

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@binaryfire

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Nov 2, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@binaryfire cubic can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 123,074 of the 120,000 allowed lines of code this month. Reviews resume on 10 November 2026 (in 8 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Stop asynchronous processes when their owning coroutine exits

🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Stop owned asynchronous processes on coroutine exit or cancellation without retaining completed
 handles.
• Stop schema, queue, and Composer children when output callbacks fail.
• Cover process lifetimes and cleanup failures with regressions, fixture changes, and documentation.
Diagram

graph TD
  A["Async start"] --> B{"Owned coroutine?"} -->|yes| C["Weak process state"] --> D["Deferred pool stop"] --> E["Child process"]
  B -->|no| E
  F["Framework callers"] --> G["Output guard"] -->|callback failure| E
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Require explicit owner cleanup
  • ➕ Keeps subprocess lifetimes entirely under caller control.
  • ➖ Every asynchronous caller must reliably wait or stop, including on cancellation.
  • ➖ Does not address abandoned handles when callers omit cleanup.

Recommendation: Keep coroutine-local weak ownership and deferred pool cleanup: it addresses abandoned handles without retaining completed processes, while preserving explicit detachment and non-coroutine behavior. Retain explicit cleanup where shutdown timing or direct Symfony Process usage requires it.

Files changed (31) +464 / -119

Bug fix (8) +121 / -30
MySqlSchemaState.phpGuard MySQL and MariaDB dump output +1/-1

Guard MySQL and MariaDB dump output

• Wraps the dump process callback so an output-handling exception stops the running child before propagating.

src/database/src/Schema/MySqlSchemaState.php

PostgresSchemaState.phpGuard PostgreSQL dump output +3/-1

Guard PostgreSQL dump output

• Keeps a reference to each dump process and guards its output callback so failures stop the child.

src/database/src/Schema/PostgresSchemaState.php

SchemaState.phpShare the process output guard with schema states +3/-0

Share the process output guard with schema states

• Adds the shared guard trait to the schema-state base class for dump implementations.

src/database/src/Schema/SchemaState.php

PendingProcess.phpRegister asynchronous children with their owner +10/-3

Register asynchronous children with their owner

• Tracks successfully started, non-detached native processes when running inside a coroutine. Preserves startup-failure cleanup and switches to the shared output guard.

src/process/src/PendingProcess.php

ProcessState.phpAdd coroutine-local weak process ownership +61/-0

Add coroutine-local weak process ownership

• Stores native handles in a non-copyable coroutine context using a WeakMap. Registers one deferred exit hook that reuses pool stopping to clean up remaining children.

src/process/src/ProcessState.php

Listener.phpStop queue workers after output-handler failures +5/-2

Stop queue workers after output-handler failures

• Guards worker output handling so a thrown exception cannot leave the current Symfony child running.

src/queue/src/Listener.php

Composer.phpGuard Composer require and remove output +21/-14

Guard Composer require and remove output

• Wraps both operations' output callbacks so failures stop the active Composer child.

src/support/src/Composer.php

GuardsProcessOutput.phpMove and generalize output-failure cleanup +17/-9

Move and generalize output-failure cleanup

• Moves the guard to Support for direct Symfony Process callers. It retries interrupted stopping, prevents callback re-entry, and preserves cancellation precedence.

src/support/src/Traits/GuardsProcessOutput.php

Refactor (1) +1 / -1
InvokedProcess.phpUse the shared output guard +1/-1

Use the shared output guard

• Switches invoked processes to the guard trait in Support without changing their public API.

src/process/src/InvokedProcess.php

Tests (14) +323 / -85
SwooleMaintenanceTimerWorkerRecycleTest.phpUse the framework API for a cache server fixture +9/-8

Use the framework API for a cache server fixture

• Starts the fixture through ProcessFactory and adapts its assertions to the invoked-process API. Retains explicit stopping because the test inspects shutdown logs.

tests/Cache/SwooleMaintenanceTimerWorkerRecycleTest.php

DatabaseMySqlSchemaStateTest.phpTest MySQL dump cleanup after output failure +28/-0

Test MySQL dump cleanup after output failure

• Uses a real child to verify that a failing dump callback propagates its exception and stops the process, with independent test cleanup.

tests/Database/DatabaseMySqlSchemaStateTest.php

DatabasePostgresSchemaStateTest.phpTest PostgreSQL dump cleanup after output failure +28/-0

Test PostgreSQL dump cleanup after output failure

• Injects a real child into the schema state and checks that output failure stops it without replacing the original exception.

tests/Database/DatabasePostgresSchemaStateTest.php

FilesystemNonCoroutineTest.phpEnsure direct Symfony fixtures stop on test failure +18/-12

Ensure direct Symfony fixtures stop on test failure

• Wraps the concurrent file-read subprocesses in finally cleanup, including when startup, waiting, or assertions fail.

tests/Filesystem/FilesystemNonCoroutineTest.php

HttpClientStreamingTest.phpAvoid signaling an unavailable fixture process group +3/-1

Avoid signaling an unavailable fixture process group

• Checks that a process ID was obtained before sending the cleanup signal.

tests/Http/HttpClientStreamingTest.php

AddSupervisorTest.phpAlways stop created Horizon supervisors +13/-11

Always stop created Horizon supervisors

• Moves supervisor stopping into finally so assertion or command failures cannot leave workers running.

tests/Integration/Horizon/Feature/AddSupervisorTest.php

WorkerProcessTest.phpPreserve explicit Horizon worker cleanup +15/-7

Preserve explicit Horizon worker cleanup

• Adds finally-based worker termination to signal and restart tests that manage their own process lifetimes.

tests/Integration/Horizon/Feature/WorkerProcessTest.php

ProcessNonCoroutineTest.phpVerify asynchronous starts outside coroutines +24/-0

Verify asynchronous starts outside coroutines

• Confirms a framework process can start and complete without a coroutine, with explicit cleanup retained.

tests/Process/ProcessNonCoroutineTest.php

ProcessTest.phpCover coroutine ownership and weak cleanup +89/-7

Cover coroutine ownership and weak cleanup

• Tests normal and canceled owner exit, detachment, discarded handles, non-inherited ownership, and release of completed handles. Adjusts pool and output-guard expectations for the new cleanup path.

tests/Process/ProcessTest.php

QueueListenerTest.phpTest worker cleanup after output failure +23/-0

Test worker cleanup after output failure

• Verifies with a real child that a listener output-handler exception is preserved and the worker stops.

tests/Queue/QueueListenerTest.php

ShutdownOnInterruptListenerTest.phpGuard server fixture process-group cleanup +9/-3

Guard server fixture process-group cleanup

• Sends fallback group-kill signals only when a fixture PID is available; retains direct process stopping.

tests/Server/Listeners/ShutdownOnInterruptListenerTest.php

SupportComposerTest.phpTest Composer child cleanup for both operations +30/-0

Test Composer child cleanup for both operations

• Covers require and remove with real children, asserting that callback exceptions propagate and the processes stop.

tests/Support/SupportComposerTest.php

CommanderServeTest.phpHarden serve fixture process-group cleanup +6/-5

Harden serve fixture process-group cleanup

• Avoids signaling an unavailable PID during cleanup and removes an unnecessary delay after process start.

tests/Testbench/CommanderServeTest.php

ProfileScriptTest.phpRun profiler fixtures through the framework Process API +28/-31

Run profiler fixtures through the framework Process API

• Replaces direct Symfony Process startup and result access with ProcessFactory and ProcessResult while preserving profiler outcome assertions.

tests/Testing/Profile/ProfileScriptTest.php

Documentation (5) +16 / -3
porting-from-laravel.mdExplain subprocess ownership when porting integrations +2/-0

Explain subprocess ownership when porting integrations

• Distinguishes the framework's coroutine-owned asynchronous processes from direct Symfony Process usage, which still needs explicit cleanup.

src/docs/porting-from-laravel.md

processes.mdDocument automatic process and pool cleanup +4/-2

Document automatic process and pool cleanup

• Explains owner-coroutine exit and cancellation behavior, non-transferable ownership, explicit detachment, and non-coroutine lifetimes.

src/docs/processes.md

testing.mdDocument subprocess fixture cleanup choices +2/-0

Document subprocess fixture cleanup choices

• Recommends the framework Process API for coroutine-scoped fixtures and explicit cleanup for direct Symfony Process usage or timing-sensitive shutdown.

src/docs/testing.md

README.mdDescribe Hypervel's asynchronous process lifetime +6/-0

Describe Hypervel's asynchronous process lifetime

• Links to process documentation and highlights coroutine-exit cleanup as a difference from Laravel.

src/process/README.md

Pool.phpClarify pool ownership at coroutine exit +2/-1

Clarify pool ownership at coroutine exit

• Updates the start-method contract to describe automatic cleanup of non-detached pooled processes.

src/process/src/Pool.php

Other (3) +3 / -0
composer.jsonDeclare Foundation's Symfony Process dependency +1/-0

Declare Foundation's Symfony Process dependency

• Adds the Symfony Process component as a direct dependency for Foundation's process-using code.

src/foundation/composer.json

composer.jsonDeclare Process's coroutine context dependency +1/-0

Declare Process's coroutine context dependency

• Adds hypervel/context as a direct dependency for coroutine-local process state.

src/process/composer.json

composer.jsonDeclare Support's Symfony Process dependency +1/-0

Declare Support's Symfony Process dependency

• Adds the Symfony Process component as a direct dependency of the shared output guard.

src/support/composer.json

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@binaryfire

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

A callback failure can already carry cancellation in its exception chain. If emergency stop also throws cancellation, checking only the callback exception class replaces that original failure and loses its cleanup context.

Use the shared cancellation-precedence helper when selecting the callback or stop failure. Keep the native-only stop catch, bounded retry and callback re-entry guard unchanged.

Extend the existing emergency-stop regression with a wrapped callback failure and assert the retained exception identity. The added row fails before the correction; the complete Process test file, targeted analysis of the trait and its consumers, and formatting pass.
@binaryfire
binaryfire merged commit c0a474d into 0.4 Nov 2, 2026
52 checks passed
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.

1 participant