Harden lifecycle and concurrency robustness - #430
Conversation
Document the verified causes of the intermittent parallel-suite hang and the related lifecycle, ownership, and concurrency defects uncovered during the audit. Define the final low-level designs for coroutine creation failures, exhaustive cleanup, pool ownership, queue and Horizon process control, watcher lifecycles, Testbench isolation, and deterministic programmatic console execution. Include the complete implementation order, regression matrix, performance constraints, and overengineering guardrails used to validate the work. Normalize older plan headers by removing obsolete author, date, and status metadata while retaining their scope sections.
Replace the ambiguous false and -1 creation outcomes with a typed CoroutineCreateException at the Engine boundary. Make the high-level create, fork, go, and co APIs return positive integer IDs on success and propagate creation failure consistently. Document the new contract and add process-isolated regressions that exhaust Swoole's coroutine capacity without polluting the surrounding test worker.
Roll back capacity tokens and wait-group counts when a child coroutine cannot be created. Record keyed failures in Parallel and the concurrency driver so their normal aggregation and input-order exception behavior remains intact instead of hanging on bookkeeping for work that never started.
Treat inability to create a bounded database or Redis probe coroutine as an unhealthy pooled connection. Catch only CoroutineCreateException so connection and application failures retain their existing behavior while pool maintenance avoids the obsolete boolean creation contract.
Make SafeSocket roll back its send-loop state when coroutine creation fails, treat native send failures as terminal, preserve native error details, and accept the valid payload string zero. Refactor socket regressions to use ephemeral ports, explicit readiness, parent-visible child failures, bounded joins, and unconditional client and server cleanup so parallel tests cannot collide or leak listeners.
Catch CoroutineCreateException at the native Swoole request callback boundary, where the child coroutine's internal exception handler cannot observe a failed spawn. Log the overload and complete the response with HTTP 503 without invoking the application handler or leaking an exception through native code.
Roll back only the handlers and native waiters created by a failed registration call. Use exception-injecting cancellation so Swoole wait loops terminate, treat intentional cancellation as control flow, and prevent partially installed signal sets from surviving coroutine exhaustion.
Make subscriber construction roll back its receive loop, shutdown timer, connection, and every channel when either background registration fails. Retain the shutdown timer ID for normal interruption and preserve the original construction failure if cleanup also encounters an error.
Move Task and Spinner animation creation inside their existing exception-safe lifecycle. Cursor visibility and final rendering now recover even when Swoole cannot create the animation coroutine, with focused regressions for both prompt implementations.
Remove an unnecessary shutdown-time coroutine and resume the worker-exit coordinator in finally after listener dispatch. This guarantees shutdown waiters are released even when a listener throws and avoids consuming a coroutine slot while the worker is already exiting.
Capture coordinator identity before spawning timer work so teardown cannot make a delayed child resolve a fresh open coordinator. Roll back registrations when creation fails, stop cleared ticks before another interval, and preserve immediate zero-timeout behavior with focused lifecycle regressions.
Run each coroutine teardown hook and every independent framework cleanup action even when an earlier action fails. Preserve the test-body exception or first teardown failure while still clearing context, native timers, and worker-exit coordination so a failed test cannot strand later work.
Move Mockery verification into the shared framework base-test lifecycle so unmet expectations are attributed to the test that created them. Keep the PHPUnit subscriber as an exhaustive fallback, capture throwable resource cleanup separately from the pure static-reset registry, and preserve first-failure ordering. Remove the duplicate Testbench trait, cover all supported base cases and failure combinations, and update repository guidance so individual tests neither close Mockery nor duplicate framework-owned resets.
Add an explicit discard operation to connection pools so borrowed resources can be destroyed while restoring capacity. Enforce managed and borrowed ownership across connection wrappers and preserve structurally aligned connection-pool and object-pool behavior. Make outside-coroutine wake failure non-throwing after state is committed, and give checkout exactly one final idle and capacity pass at its deadline. Cover release, discard, missed wake, foreign ownership, and no-second-wait behavior in both pool implementations.
Keep each testing resolver's owning pooled wrapper alongside its stable bare database connection, then discard the wrapper explicitly during named, terminal, and container-change cleanup. Split reusable reset behavior from terminal flush behavior and order resolver teardown before pool shutdown. Always detach a discarded wrapper from its connection while preserving the pool-retained shared in-memory SQLite PDO, including rollback of transactions owned by that wrapper. Add max-one-capacity, write-routing, shared-database, transaction, and lifecycle regressions.
Replace ad hoc timeout-monitor callables with one injected Timer whose exact registration is cleared on every daemon exit. Reset the monitor lock through finally, keep timeout-job suppression intact, and make the recording test worker yield without installing real signal handlers. Treat a hard job timeout as terminal for the poisoned worker process instead of waiting indefinitely for unrelated job coroutines. Unblock the worker's exact control-signal set only after its handlers are installed so a supervising parent can protect the bootstrap window. Cover monitor failures, every daemon return path, scheduler progress, stopping events, signal ownership, and immediate process termination under concurrency.
Block each child's exact handled signal set across Symfony Process startup and restore the parent's prior mask on success or failure. Unblock pending signals only after queue-worker or supervisor handlers are installed, without using Symfony's ignored-signal API that can suppress later control sends on SIGCHLD builds. Add an immediate hard-stop primitive and regressions for delayed handler installation, pending-signal delivery, mask restoration, parent-child signal-set parity, and zero-grace process termination.
Persist supervisor and master state synchronously before loop events so writes cannot overlap, reorder, or escape the initiating error boundary. Scale workers through the existing terminating state machine, enforce configured grace periods, and hard-stop children that remain alive at their deadline. Replace eventual-persistence retries and permissive teardown with deterministic ordering, exhaustive active and terminating process cleanup, and regressions for persisted state visibility and bounded master shutdown.
Give Cache and Reverb striped locks a fixed internal acquisition deadline with backoff after the existing hot-spin window. Preserve the uncontended path as one compare-and-set and release every stripe acquired before a later all-lock timeout. Move Reverb's full-table logging outside critical sections and cover contention recovery, holder death, partial acquisition, and all failed lock-row call sites without adding user-facing lock policy.
Keep a new subscriber locally owned through creation and subscription, recheck disconnect state after yielding operations, and commit only the exact subscriber consumed by the spawned receive loop. Clear and reconnect by object identity so an older consumer cannot tear down a replacement. Reset retry state only after complete startup, drain queued publishes in order, drop permanently invalid JSON payloads, and retain transient publish failures with their ordered tail. Remove the unused public subscribe method and cover spawn, handshake, disconnect, retry-limit, and queue-drain failures.
Move named Route object caching from process-global static state onto the CompiledRouteCollection that owns the route attributes. Replacing a collection now establishes object identity without requiring a global flush while retaining worker-lifetime reuse for the active collection. Remove obsolete reset and warmup guidance and prove that collections sharing a route name cannot exchange domain, port, URI, action, or object identity.
Route Application::call through a dedicated IO configuration and doRun boundary instead of Symfony's root CLI wrapper. Programmatic commands now ignore inherited SHELL_VERBOSITY, avoid process-global exception and terminal mutation, and still honor explicit ANSI, interaction, quiet, and verbosity options. Add behavioral parity coverage for command execution, help, output, events, exceptions, and explicit options while retaining command cloning as the existing concurrent and nested-call isolation boundary.
Standardize driver watch as a blocking operation that returns only after terminal completion or idempotent stop. Replace detached coordinator timers with one lazily owned stop channel so polling cadence remains coroutine-friendly while stop wakes an interval immediately and scan failures reach the Watcher owner. Preserve FindNewer's reference-file and in-flight scan cleanup rules and cover real interval blocking, immediate stop, deferred cleanup, and propagated scan failures across every polling driver.
Process fswatch paths inline in the owned driver coroutine, retain incomplete newline-delimited tails across reads, and deliver complete paths in order under bounded channel backpressure. Terminal read and matching failures now propagate through the driver's lifecycle instead of detached raw coroutines. Compile immutable WatchPath glob patterns once and add regressions for split paths, multi-record chunks, final buffered records, ordered batches, failure propagation, and explicit process teardown.
Own the blocking driver in one joined coroutine, propagate its terminal failure, drain synchronous final batches, and perform driver, strategy, channel, and bounded-join cleanup without replacing the primary operation error. Add an idempotent restart-strategy stop contract, restore server launch tokens in finally, report asynchronous native launch failures safely, and validate positive PID files before any event or POSIX signal. Cover full-channel shutdown, debounce tails, restart failures, managed-process cleanup, and corrupted PID input.
Give every forked cache-test child an explicit PID owner, nonblocking length-prefixed result channel, monotonic deadline, maximum frame size, and exhaustive failure cleanup. Retry interrupted reaping, accept only the owned PID or ECHILD, and never fall back to a global or blocking wait. Add deterministic early-exit, incomplete-payload, child-error, and stall coverage so process failures report promptly instead of hanging a parallel worker.
Replace an unbounded hand-built renderer coroutine join with the framework parallel primitive so child exceptions propagate through an owned wait group. Wrap explicitly created Redis subscribers and raw publish connections in exhaustive cleanup so assertion failures cannot leave background receive work alive in the test worker.
Introduce a small test-only cleanup primitive that runs every supplied owner callback and rethrows the first failure afterward. This gives file-producing integration tests one deterministic way to preserve primary failures without skipping independent restoration or parent teardown.
Pass each application's resolved config and route cache path into the fresh subprocess that rebuilds it. PHP array-only environment overrides are not inherited automatically, so this prevents a child from reading or writing a stale default cache that differs from its parent. Add alternate-path regressions and make the route-cache suite exhaustively own its generated sources and caches while asserting that worker Testbench state is pristine before each run.
Convert every remaining route- and provider-producing Testbench suite to checked, atomic restoration and deletion through CleanupActions. Keep each test's owned resource list local, preserve the first cleanup failure, and still run every later file repair and parent teardown action to prevent worker contamination.
Explain that tests must close or join child coroutines, subscribers, processes, servers, and similar resources through exception-safe ownership. Recommend the framework parallel primitive over unbounded hand-built channel joins when channel behavior is not under test. Document that Hypervel verifies and closes Mockery automatically through framework base cases with a global subscriber fallback, so application and package tests must not call Mockery::close themselves.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (22)
🚧 Files skipped from review as they are similar to previous changes (13)
📝 WalkthroughWalkthroughThis PR hardens coroutine creation, cleanup ownership, pool and lock lifecycles, process shutdown, console execution, Redis handling, routing caches, and watcher lifecycles. It adds extensive regression coverage and updates related documentation and fixtures. ChangesConcurrency and lifecycle robustness
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Use WorkerOptions as the single source of truth for timeout-monitor cadence so the documented queue:work --monitor-interval option reaches the owned Timer. Remove the redundant Worker constructor interval that was never populated by the service provider and always forced the one-second default. Record the requested timeout in the Timer test seam and prove that a non-default option is used during daemon startup. Update the lifecycle plan with the pre-existing wiring bug, final design, and regression coverage.
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (5)
src/reverb/src/Servers/Hypervel/Scaling/RedisPubSubProvider.php (1)
250-256: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep the new method docblock title-only.
Remove the explanatory paragraph; retain
@phpstan-impureif required.Suggested cleanup
/** * Determine whether reconnect work remains enabled. - * - * Hooked Redis I/O and Sleep may yield while disconnect() changes this state. * * `@phpstan-impure` */As per coding guidelines, “Add title-only Laravel-style method docblocks to methods.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/reverb/src/Servers/Hypervel/Scaling/RedisPubSubProvider.php` around lines 250 - 256, Update the docblock for the reconnect-state method near the Redis scaling provider to retain only its title and the required `@phpstan-impure` annotation; remove the explanatory paragraph about Redis I/O, Sleep, and disconnect().Source: Coding guidelines
src/routing/src/CompiledRouteCollection.php (1)
235-288: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep the updated method docblocks title-only.
Remove the new cache-behavior paragraphs while retaining necessary static-analysis annotations.
Suggested cleanup
/** * Get a route instance by its name. - * - * Returns cached Route objects for the lifetime of this collection. */ @@ /** * Get the route instances that should be pre-warmed. - * - * Returns the collection's cached Route instances — these - * are the objects actually used during request matching. Unlike - * getRoutes() which creates fresh throwaway objects every call. * * `@return` array<int, Route> */As per coding guidelines, “Add title-only Laravel-style method docblocks to methods.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routing/src/CompiledRouteCollection.php` around lines 235 - 288, Update the docblocks for getByName and getRoutes in CompiledRouteCollection to use title-only Laravel-style descriptions. Remove the cache-behavior explanatory paragraphs while retaining required static-analysis annotations such as the getRoutes return type.Source: Coding guidelines
src/testing/src/PHPUnit/AfterEachTestSubscriber.php (1)
48-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winImport the database resolver.
Use a
DatabaseConnectionResolverimport and its short name for this new call. As per coding guidelines, “Import classes withusestatements and reference short names.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/testing/src/PHPUnit/AfterEachTestSubscriber.php` around lines 48 - 50, Update the try block in AfterEachTestSubscriber to import Hypervel\Foundation\Testing\DatabaseConnectionResolver with a use statement and call flushCachedConnections() through the short DatabaseConnectionResolver name instead of the fully qualified class name.Source: Coding guidelines
src/foundation/src/Testing/DatabaseConnectionResolver.php (2)
58-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAlign the new lifecycle docs with the project rules.
resetCachedConnections()andflushCachedConnections()need aTests only.warning naming the cross-coroutine stale/discarded-connection failure mode. Keep the addedflush()andconnection()method docblocks title-only. As per coding guidelines, public worker-lifetime mutators require scope warnings and concrete persistence failures, while method docblocks must be title-only.Also applies to: 88-96, 125-129, 141-145
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/foundation/src/Testing/DatabaseConnectionResolver.php` at line 58, Update the docblocks for resetCachedConnections() and flushCachedConnections() to include a “Tests only.” warning describing the cross-coroutine stale/discarded-connection failure mode. Keep the flush() and connection() method docblocks title-only, without adding explanatory text or warnings.Source: Coding guidelines
39-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExpose standard cleanup for the new static wrapper cache.
Add a
flushState()method delegating to terminal wrapper cleanup, and haveAfterEachTestSubscribercall it. This makes the new process-global cache follow the framework static-state lifecycle contract. As per coding guidelines, “When static state is introduced or modified, ... addflushState()for framework static properties and register cleanup withAfterEachTestSubscriberwhen required.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/foundation/src/Testing/DatabaseConnectionResolver.php` around lines 39 - 44, Add a public static flushState() method to DatabaseConnectionResolver that delegates cleanup to the terminal wrapper mechanism, ensuring the pooledConnections cache is cleared. Update AfterEachTestSubscriber to invoke DatabaseConnectionResolver::flushState() during per-test cleanup, following the framework static-state lifecycle.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/testbench/src/Bootstrapper.php`:
- Around line 168-179: Make the post-copy initialization around Bootstrapper’s
$filesystem->replace() exception-safe: ensure the copied runtime directory is
removed if marker creation or related setup throws, then rethrow the original
exception. Preserve normal $runtimePath recording and shutdown cleanup
registration when initialization succeeds.
In `@src/testing/src/PHPUnit/AfterEachTestSubscriber.php`:
- Around line 54-57: Update the cleanup flow in AfterEachTestSubscriber so
flushFrameworkState() exceptions are caught and assigned to $exception only when
no earlier callback, Mockery, or database cleanup failure exists; then preserve
the existing final rethrow behavior.
In `@src/watcher/src/Driver/DriverInterface.php`:
- Around line 14-15: Update the PHPDoc for the watch() method in DriverInterface
to contain only its title, and move the lifecycle-blocking details to an
interface-level docblock. Preserve the documented behavior while ensuring
watch() uses title-only Laravel-style PHPDoc.
In `@src/watcher/src/Events/BeforeServerRestart.php`:
- Around line 9-11: Add the required title-only Laravel-style docblock
immediately above the __construct method in BeforeServerRestart, describing the
constructor without parameter or return annotations. Leave the constructor
signature and behavior unchanged.
In `@src/watcher/src/ServerRestartStrategy.php`:
- Around line 109-110: Update the BeforeServerRestart dispatch in the server
restart strategy to first check whether the events dispatcher has listeners for
that event via hasListeners(). Only construct and dispatch BeforeServerRestart
when listeners are present; otherwise skip the event entirely.
- Around line 91-95: Update the PID-file handling in ServerRestartStrategy so
the check and read cannot fail when the server removes the file between
operations. Replace the separate filesystem exists/get sequence with the
available atomic or exception-safe read approach, while preserving the existing
empty/missing PID behavior so Watcher::run cleanup can continue to restart.
In `@tests/Cache/CacheSwooleStoreConcurrencyTest.php`:
- Around line 241-275: Update the child process closure in the concurrency test
to wrap all setup, callback, payload handling, and writeChildPayload operations
in a try/finally block, placing the existing SIGKILL call in finally. Ensure
exceptions from createStore, beforeReady, or writeChildPayload still
force-terminate the forked child while preserving the current payload behavior
for callback exceptions.
In `@tests/Coordinator/TimerTest.php`:
- Around line 132-146: Widen only the first callback wait in
testTickClearedFromItsCallbackDoesNotWaitAnotherInterval by increasing the
called->pop timeout beyond 0.2 seconds to tolerate CI scheduling delays. Keep
the subsequent short usleep and Timer::stats assertion unchanged so it still
detects an extra scheduled interval.
In `@tests/Foundation/Exceptions/Renderer/ListenerContextIsolationTest.php`:
- Around line 36-45: Add the required : void return type to the modified test
method containing the parallel query assertions in ListenerContextIsolationTest,
while leaving its existing test logic unchanged.
In `@tests/Foundation/Testing/DatabaseConnectionResolverTest.php`:
- Around line 44-53: Wrap the container replacement, cached-connection reset,
assertion, and restoration in a try/finally block within the test, and restore
the original container via Container::setInstance($this->app) in finally so it
runs even when resetCachedConnections or the assertion fails.
In `@tests/Integration/Horizon/Feature/WorkerProcessTest.php`:
- Around line 43-45: Update the callback passed to WorkerProcessTest’s
$process->start call so the stream-type parameter $type is either used when
handling output or renamed according to the repository’s accepted
ignored-parameter convention, eliminating the unused-parameter warning while
preserving output capture.
In `@tests/ObjectPool/ChannelTest.php`:
- Around line 143-153: Synchronize the coroutine created in the channel wait
test before calling push(), ensuring the waiter has registered with wait(1.0)
first. Update the setup around SwooleCoroutine::create and waitResult so the
subsequent push/pop sequence reliably exercises the helper-coroutine fallback
path.
In `@tests/Queue/QueueWorkerTest.php`:
- Around line 139-147: Add the required : void return type to the
testWorkerCanMonitorTimeoutJobs() method declaration. Preserve the existing test
setup and assertions, and do not alter the worker behavior being tested.
---
Nitpick comments:
In `@src/foundation/src/Testing/DatabaseConnectionResolver.php`:
- Line 58: Update the docblocks for resetCachedConnections() and
flushCachedConnections() to include a “Tests only.” warning describing the
cross-coroutine stale/discarded-connection failure mode. Keep the flush() and
connection() method docblocks title-only, without adding explanatory text or
warnings.
- Around line 39-44: Add a public static flushState() method to
DatabaseConnectionResolver that delegates cleanup to the terminal wrapper
mechanism, ensuring the pooledConnections cache is cleared. Update
AfterEachTestSubscriber to invoke DatabaseConnectionResolver::flushState()
during per-test cleanup, following the framework static-state lifecycle.
In `@src/reverb/src/Servers/Hypervel/Scaling/RedisPubSubProvider.php`:
- Around line 250-256: Update the docblock for the reconnect-state method near
the Redis scaling provider to retain only its title and the required
`@phpstan-impure` annotation; remove the explanatory paragraph about Redis I/O,
Sleep, and disconnect().
In `@src/routing/src/CompiledRouteCollection.php`:
- Around line 235-288: Update the docblocks for getByName and getRoutes in
CompiledRouteCollection to use title-only Laravel-style descriptions. Remove the
cache-behavior explanatory paragraphs while retaining required static-analysis
annotations such as the getRoutes return type.
In `@src/testing/src/PHPUnit/AfterEachTestSubscriber.php`:
- Around line 48-50: Update the try block in AfterEachTestSubscriber to import
Hypervel\Foundation\Testing\DatabaseConnectionResolver with a use statement and
call flushCachedConnections() through the short DatabaseConnectionResolver name
instead of the fully qualified class name.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e2de83a5-9516-4fa1-a023-cb710c5fd232
📒 Files selected for processing (132)
AGENTS.mddocs/ai/differences-vs-laravel.mddocs/plans/2026-07-01-fortify-passkeys-port.mddocs/plans/2026-07-03-fortify-otphp-chillerlan-refactor.mddocs/plans/2026-07-04-testing-after-each-cleanup-registrars.mddocs/plans/2026-07-12-test-suite-lifecycle-and-concurrency-robustness.mdsrc/boost/docs/coroutines.mdsrc/boost/docs/testing.mdsrc/cache/src/SwooleTableState.phpsrc/concurrency/src/CoroutineDriver.phpsrc/console/src/Application.phpsrc/console/src/SignalRegistry.phpsrc/contracts/src/Pool/ConnectionInterface.phpsrc/contracts/src/Pool/PoolInterface.phpsrc/coordinator/src/Timer.phpsrc/core/src/Bootstrap/WorkerExitCallback.phpsrc/coroutine/src/Concurrent.phpsrc/coroutine/src/Coroutine.phpsrc/coroutine/src/Parallel.phpsrc/coroutine/src/WaitConcurrent.phpsrc/coroutine/src/functions.phpsrc/database/src/Pool/PooledConnection.phpsrc/engine/src/Coroutine.phpsrc/engine/src/Exceptions/CoroutineCreateException.phpsrc/engine/src/Http/Server.phpsrc/engine/src/SafeSocket.phpsrc/foundation/src/Console/ConfigCacheCommand.phpsrc/foundation/src/Console/RouteCacheCommand.phpsrc/foundation/src/Testing/Concerns/InteractsWithTestCaseLifecycle.phpsrc/foundation/src/Testing/Concerns/RunTestsInCoroutine.phpsrc/foundation/src/Testing/DatabaseConnectionResolver.phpsrc/foundation/src/Testing/TestCase.phpsrc/horizon/src/Console/HorizonRestartStrategy.phpsrc/horizon/src/ListensForSignals.phpsrc/horizon/src/MasterSupervisor.phpsrc/horizon/src/ProcessPool.phpsrc/horizon/src/Supervisor.phpsrc/horizon/src/SupervisorProcess.phpsrc/horizon/src/WorkerProcess.phpsrc/object-pool/src/Channel.phpsrc/object-pool/src/ObjectPool.phpsrc/pool/src/Channel.phpsrc/pool/src/Connection.phpsrc/pool/src/Frequency.phpsrc/pool/src/KeepaliveConnection.phpsrc/pool/src/Pool.phpsrc/prompts/src/Spinner.phpsrc/prompts/src/Task.phpsrc/queue/src/Worker.phpsrc/redis/src/RedisConnection.phpsrc/redis/src/Subscriber/CommandInvoker.phpsrc/reverb/src/Servers/Hypervel/Contracts/PubSubProvider.phpsrc/reverb/src/Servers/Hypervel/Scaling/RedisPubSubProvider.phpsrc/reverb/src/Servers/Hypervel/Scaling/SwooleTableSharedState.phpsrc/routing/src/CompiledRouteCollection.phpsrc/routing/src/Router.phpsrc/signal/src/SignalManager.phpsrc/testbench/src/Bootstrapper.phpsrc/testbench/src/PHPUnit/TestCase.phpsrc/testing/src/Concerns/InteractsWithMockery.phpsrc/testing/src/PHPUnit/AfterEachTestSubscriber.phpsrc/watcher/README.mdsrc/watcher/src/Driver/AbstractDriver.phpsrc/watcher/src/Driver/DriverInterface.phpsrc/watcher/src/Driver/FindDriver.phpsrc/watcher/src/Driver/FindNewerDriver.phpsrc/watcher/src/Driver/FswatchDriver.phpsrc/watcher/src/Driver/ScanFileDriver.phpsrc/watcher/src/Events/BeforeServerRestart.phpsrc/watcher/src/RestartStrategy.phpsrc/watcher/src/ServerRestartStrategy.phpsrc/watcher/src/WatchPath.phpsrc/watcher/src/Watcher.phptests/Cache/CacheSwooleStoreConcurrencyTest.phptests/Console/ConsoleApplicationProgrammaticTest.phptests/Console/SignalRegistryCreateFailureTest.phptests/Coordinator/TimerTest.phptests/Core/Bootstrap/WorkerExitCallbackTest.phptests/Coroutine/CoroutineCreateFailureTest.phptests/Coroutine/CoroutineTest.phptests/Coroutine/FunctionTest.phptests/Engine/CoroutineCreateFailureTest.phptests/Engine/HttpServerTest.phptests/Engine/SocketTest.phptests/Foundation/Exceptions/Renderer/ListenerContextIsolationTest.phptests/Foundation/Testing/DatabaseConnectionResolverTest.phptests/Foundation/Testing/UnitTestTest.phptests/Horizon/Console/InstallCommandTest.phptests/Integration/Foundation/Console/ApiInstallCommandTest.phptests/Integration/Foundation/Console/BroadcastingInstallCommandTest.phptests/Integration/Foundation/Console/ConfigCacheCommandTest.phptests/Integration/Foundation/Console/RouteCacheCommandTest.phptests/Integration/Generators/ProviderMakeCommandTest.phptests/Integration/Horizon/Feature/Fixtures/EternalSupervisor.phptests/Integration/Horizon/Feature/ListenCommandTest.phptests/Integration/Horizon/Feature/MasterSupervisorTest.phptests/Integration/Horizon/Feature/SupervisorTest.phptests/Integration/Horizon/Feature/WorkerProcessTest.phptests/Integration/Redis/RedisSubscribeIntegrationTest.phptests/Integration/Redis/Subscriber/SubscriberIntegrationTest.phptests/ObjectPool/ChannelTest.phptests/ObjectPool/ObjectPoolNonCoroutineTest.phptests/ObjectPool/ObjectPoolTest.phptests/Pool/ChannelTest.phptests/Pool/ConnectionTest.phptests/Pool/FrequencyTest.phptests/Pool/HeartbeatConnectionTest.phptests/Pool/PoolNonCoroutineTest.phptests/Pool/PoolTest.phptests/Prompts/CoroutineCreateFailureTest.phptests/Queue/QueueWorkerTest.phptests/Redis/Subscriber/CommandInvokerCreateFailureTest.phptests/Reverb/Servers/Hypervel/Scaling/RedisPubSubProviderTest.phptests/Reverb/Servers/Hypervel/Scaling/SwooleTableSharedStateLockTest.phptests/Routing/RoutePortTest.phptests/Signal/SignalManagerCreateFailureTest.phptests/Telescope/Console/InstallCommandTest.phptests/TestCase.phptests/Testbench/BootstrapperTest.phptests/Testbench/Foundation/Process/RemoteCommandTest.phptests/Testing/CleanupActionsTest.phptests/Testing/Concerns/InteractsWithMockeryTest.phptests/Testing/Fixtures/CleanupActions.phptests/Testing/PHPUnit/AfterEachTestSubscriberTest.phptests/Watcher/Driver/FindDriverTest.phptests/Watcher/Driver/FindNewerDriverTest.phptests/Watcher/Driver/FswatchDriverTest.phptests/Watcher/Driver/ScanFileDriverTest.phptests/Watcher/Fixtures/FindNewerDriverStub.phptests/Watcher/Fixtures/FswatchDriverStub.phptests/Watcher/ServerRestartStrategyTest.phptests/Watcher/WatcherTest.php
💤 Files with no reviewable changes (2)
- docs/ai/differences-vs-laravel.md
- src/reverb/src/Servers/Hypervel/Contracts/PubSubProvider.php
Greptile SummaryThis PR systematically hardens lifecycle and concurrency contracts across Hypervel's coroutine, pool, watcher, queue, signal, and testing layers. The core change is a uniform coroutine-creation contract: native failures now throw
Confidence Score: 5/5Safe to merge — the changes are disciplined lifecycle hardening with no regressions identified across coroutine, pool, watcher, queue, or test teardown paths. The coroutine creation contract change is consistent across all callers (Concurrent, Parallel, Timer, SignalManager, Prompts, Reverb pub/sub, Channel.signal). Pool discard correctly removes from both managed/borrowed maps and signals waiters through the existing destroyConnection() path. The watcher stop-channel model eliminates the detached-timer lifetime problem. The queue worker's owned Timer clears in finally. The DatabaseConnectionResolver now retains and explicitly discards its pooled wrappers. The testing teardown uses capture() to run every independent cleanup action even after earlier failures. The PR's stated behavioral changes (hard kill for timeout workers, per-instance route cache, console programmatic path bypass) are deliberate and correctly scoped. No files require special attention — the most complex changes (RedisPubSubProvider ownership sequencing, Pool.getConnection() deadline pass, DatabaseConnectionResolver dual-flush) all check out on close reading. Important Files Changed
Reviews (2): Last reviewed commit: "Tighten lifecycle regression tests" | Re-trigger Greptile |
Validate native file reads before returning from the typed filesystem API and convert unreadable or vanished files into the existing framework exception contract.\n\nPreserve the file session driver's empty-session semantics when a session is concurrently removed between its metadata check and locked read. Add regressions for both locked and unlocked filesystem races and the session-handler adaptation.
Treat skeleton copying, environment setup, and process-marker creation as one initialization transaction.\n\nReject partial directory copies, roll back unpublished runtime directories on every creation failure without masking the primary exception, and verify both copy and marker failures through the existing filesystem seam.
Move child self-termination into finally blocks for the cache concurrency harness and Reverb lock regression.\n\nThis prevents exceptional child setup or payload writes from reaching PHPUnit and Testbench shutdown handlers inherited from the parent process and deleting shared test infrastructure.
Keep the earliest teardown failure primary when aggregate framework state cleanup also fails, while retaining terminal database wrapper cleanup as an independently captured resource operation.\n\nClarify the resolver's process-global test-only lifecycle, use the imported resolver at the subscriber boundary, and restore a swapped global container reliably from test failures.
Eliminate the watcher pid-file check/read race and tolerate a pidfile disappearing while the server exits.\n\nGuard all optional watcher and Horizon restart events through the typed dispatcher so event construction and dispatch are skipped without listeners, while preserving EventFake and registered-listener behavior. Add focused listener and disappearance regressions.
Give the timer callback regression enough scheduling margin under parallel CI without weakening its no-reschedule assertion.\n\nComplete the required void return types on the modified coroutine-isolation and queue-worker monitor tests.
|
@greptileai @coderabbitai Thanks for the detailed review. I verified every inline, summary, nitpick, and outside-diff item against the current lifecycle and pushed the accepted fixes. Addressed
Intentionally unchanged
The complete follow-up passes formatting, static analysis, the full parallel suite, Testbench contract tests, and package dogfood tests. |
|
Thorough and well-reasoned follow-up — thank you for working through each item individually. The addressed fixes are exactly right. The transactional runtime creation, The intentionally unchanged items hold up on re-examination:
This is in good shape. The lifecycle contracts are now consistently enforced at ownership boundaries, and the test suite exercises them directly rather than through timing. Happy to approve. |
|
Tip For best results, initiate chat on the files or code changes.
The rationale for the intentionally-unchanged items is convincing:
Nice work getting the full parallel suite, Testbench contract tests, and dogfood tests green through all this. ᕦ(ツ)ᕤ ✏️ Learnings added
|
Summary
A rare parallel test-suite hang exposed a broader class of lifecycle bugs: work could reserve capacity or register cleanup state before spawning a coroutine, while native coroutine creation could fail without a consistent framework contract. Similar ownership gaps existed around timers, pooled connections, child processes, file watchers, subscribers, and test teardown.
This PR fixes those problems at their owning boundaries. Coroutine creation now either returns an integer ID or throws a typed exception. Stateful callers roll back transactionally. Long-lived and asynchronous resources have explicit owners, bounded shutdown behavior, and exception-safe cleanup. The test suite exercises those contracts directly instead of relying on timing, retries, fixed ports, or unbounded waits.
The same audit also corrects several adjacent issues: compiled route objects no longer cross collection identity, programmatic console calls no longer inherit process-global shell verbosity, pool discard restores capacity, Swoole table lock acquisition is bounded, and Testbench verifies process incarnation before signaling a stale PID.
For more detailes, see: docs/plans/2026-07-12-test-suite-lifecycle-and-concurrency-robustness.md
Motivation
The captured hang was a CPU-bound queue worker test. Its fake sleep method recorded the delay but did not yield, allowing the daemon loop to starve a child job coroutine indefinitely. Fixing that one fake would address the observed symptom, but tracing the ownership chain found multiple ways the suite or a production worker could still fail to make progress:
falsewhen coroutine creation fails, while framework layers variously expected an integer,-1, orfalse.These are lifecycle contract problems, not timing problems. Adding retries or larger timeouts would make the failures rarer without making ownership correct.
Coroutine creation
Engine coroutine creation now has one contract:
CoroutineCreateException.The high-level
Coroutine::create(),Coroutine::fork(),go(), andco()APIs expose the same behavior. Callers that reserve state before spawning now roll it back explicitly, including concurrent work limits, wait groups, timers, signal watchers, Redis subscriber loops, prompt animation state, and Reverb pub/sub startup.The native HTTP server callback handles coroutine exhaustion at the callback boundary and completes the request with HTTP 503. Database and Redis health checks intentionally translate only coroutine creation failure into an unhealthy result.
Cleanup and test ownership
Coroutine test teardown now runs every independent cleanup action and preserves the first failure. Test-body failures remain primary while context cleanup, native timer cleanup, worker-exit resume, and coordinator cleanup still execute.
Framework base test cases verify Mockery expectations during their own teardown so failures are attributed to the test that created them. The global PHPUnit subscriber remains a fallback for tests using another base class and keeps framework-static cleanup authoritative. Throwable pooled-database cleanup is captured before the pure static reset list, so it cannot skip unrelated resets.
Tests that create processes, sockets, subscribers, or child coroutines now own them through bounded joins and
finallycleanup. The cache multiprocess harness uses framed nonblocking IPC, monotonic deadlines, and owned-PID reaping. Engine socket tests use ephemeral ports and explicit readiness instead of fixed ports or synthetic probe connections.Pools and testing database connections
Connection pools now expose an explicit
discard()operation for destroying a borrowed connection while restoring capacity. Connection and object pool channels treat a failed outside-coroutine wake as a missed notification after state has already been committed. Checkout performs exactly one final immediate state pass at its deadline, closing the ordinary timeout-versus-release race without polling or a permanent dispatcher coroutine.The testing database resolver retains the pooled wrapper alongside its stable bare connection. Reusable reset and terminal flush are separate operations, and teardown discards wrappers before closing the pool. Shared in-memory SQLite keeps its pool-owned PDO while wrapper-owned transactions are still rolled back correctly.
Queue and Horizon process lifecycle
Queue worker timeout monitoring is owned by one injected timer whose exact registration is cleared on every daemon exit. Monitor locks reset through
finally, test workers retain scheduler progress without installing process signal handlers, and a hard job timeout terminates the poisoned worker immediately instead of waiting for unrelated coroutines.Horizon persistence is synchronous with the loop iteration that owns it, so writes cannot overlap or report failures outside that boundary. Worker termination uses the existing graceful state machine and applies configured hard-stop deadlines.
A parent now blocks each child process's control signals across fork and exec. The child unblocks the exact set only after installing its handlers, so an early pause or termination signal remains pending instead of taking its default disposition. The parent restores its prior signal mask on both successful and failed startup. This intentionally does not use Symfony's ignored-signal API because that can suppress later
Process::signal()calls on SIGCHLD-enabled builds.Watcher lifecycle
Every watcher driver now owns one blocking, explicitly stoppable lifecycle. Polling drivers use an owned stop channel rather than detached timers. Fswatch preserves newline framing across partial reads, processes matching inline, and propagates terminal failures through the owned driver coroutine.
Watcherobserves driver completion and failure, drains final synchronous batches, stops both the driver and restart strategy, closes the change channel, and performs a bounded join without masking the primary failure. Restart strategies expose an idempotentstop()contract. Server restart handling restores launch capacity infinallyand validates positive PID input before dispatching events or sending POSIX signals.Additional correctness fixes
Compatibility
This targets the Hypervel 0.4 architecture and deliberately establishes stricter low-level contracts:
falseor-1;subscribe()contract method is removed;These changes make invalid lifecycle states unrepresentable rather than retaining compatibility shims for ambiguous behavior.
Verification
composer fixgate, including formatting, static analysis, the parallel component suite, Testbench, and dogfood.Summary by CodeRabbit
call()supports consistent programmatic execution.503 Service Unavailablewhen coroutine capacity is exhausted.