Skip to content

Harden signal and pool resource lifecycles - #441

Merged
binaryfire merged 14 commits into
0.4from
audit/deterministic-resource-lifecycles
Jul 19, 2026
Merged

Harden signal and pool resource lifecycles#441
binaryfire merged 14 commits into
0.4from
audit/deterministic-resource-lifecycles

Conversation

@binaryfire

@binaryfire binaryfire commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Overview

Hypervel runs inside long-lived Swoole workers. Resource owners therefore cannot rely on request teardown, periodic polling, or eventual garbage collection to restore a clean state.

This PR is a cohesive lifecycle-hardening batch across Signal, Pool, and Object Pool, including their Database, Redis, Sentry, Filesystem, Queue, and Broadcasting consumers. The common theme is exact ownership: a component either commits a resource completely or cleans it up, and terminal teardown acts on the exact handles it owns.

Signal lifecycle

Signal watchers previously polled waitSignal() with a five-second timeout. Worker shutdown allows less time by default, so a watcher could still be sleeping when shutdown completed and force Swoole to terminate the process.

SignalManager now waits indefinitely and records only coroutine IDs that are currently parked in the native signal wait. Terminal stop() snapshots and exception-cancels those exact waiters. A handler that has already started is removed from the cancellable set first, so application signal handling is allowed to finish normally.

Watcher creation remains transactional: if a later spawn fails, every earlier watcher created by that attempt is canceled. A native false wait result is terminal instead of being retried into a busy loop.

The old reversible stopped state and signal.timeout polling configuration are removed. The package README now documents native SIGTERM / SIGINT ownership, process-lifetime handler instances, and the requirement for handlers to be coroutine-safe.

Connection pools

Connection lifecycle ownership is tightened in several places:

  • A KeepaliveConnection that acquires a resource but fails to create its heartbeat timer now closes the resource and preserves the original creation failure.
  • A heartbeat callback failure uses the same close boundary. If another caller temporarily owns the resource, disconnected state ensures that caller drops it instead of returning it to the queue.
  • The documented pool.connect_timeout is now applied to native connection establishment. MySQL and MariaDB use PDO::ATTR_TIMEOUT, PostgreSQL uses the DSN connect_timeout, and Redis uses its native top-level timeout. Explicit driver configuration always wins; SQLite remains unchanged.
  • Pool idle, release, heartbeat, and generation-lifetime timestamps use one monotonic clock domain. Database query timing and Redis command-event timing use the same monotonic primitive, preventing wall-clock changes from producing negative durations or incorrect recycling.
  • Frequency accounting retains exactly the configured ten-second bucket window instead of eleven buckets.

The package also sheds infrastructure that did not own behavior: an unused coroutine Context helper, its package dependency, an unread Pool reference in Frequency, the false constructor requirement on LowFrequencyInterface, and an empty Redis Frequency alias. psr/log is now a direct dependency because the retained keepalive extension surface types its contract directly.

Pool documentation now describes the real ownership model and clarifies that min_connections is a lower bound for excess-idle trimming of the total managed count. It does not prewarm, replenish, guarantee idle capacity, or guarantee a total minimum after failed creation or explicit discard.

Object pools

Recycler maintenance is isolated per registered pool. A custom pool that throws is reported with its registry identity and original exception, skips only its own remaining maintenance, and cannot starve later pools during the same tick.

Worker-wide mutators now state their actual lifecycle boundary at both contract and implementation surfaces: factory flushing and recycler timer/start/stop configuration are boot/test operations, not per-request controls.

ObjectPool, SimpleObjectPool, PoolManager, and the Sentry transport pool no longer retain or forward an unread application container. All affected consumers and tests use only the real constructor dependencies. The stale direct Carbon dependency is removed as well.

Compatibility and performance

Laravel-facing APIs, configuration, documented behavior, and extension patterns are unchanged.

The changed or removed public surfaces are Hypervel-specific lifecycle APIs: reversible Signal stopped state, the unused Signal polling timeout, the false low-frequency constructor dependency, and unread Object Pool constructor arguments. No compatibility shims are retained on the 0.4 line.

There is no additional request-hot-path lookup, allocation, lock, yield, retry, or clock read:

  • Signal watchers no longer wake periodically, reducing steady-state work.
  • Monotonic timing replaces existing wall-clock reads one-for-one.
  • Native connection timeout translation occurs only during construction.
  • Keepalive cleanup is failure-only.
  • Object Pool adds one direct try/catch per pool during the existing maintenance interval; diagnostic objects are allocated only after a failure.
  • Normal connection/object borrow, release, discard, proxy, request, job, and transport paths are unchanged.

Validation

composer fix passes at the final branch state:

  • PHP CS Fixer: no changed files
  • Both PHPStan configurations: no errors
  • Components: 23,196 tests, 66,057 assertions, 1,600 expected skips
  • Testbench contracts: 346 tests, 1,029 assertions, 3 expected skips
  • Testbench dogfood: 4 tests, 7 assertions

Focused regressions cover native signal-wait teardown, multiple parked watchers, active-handler completion, partial watcher creation failure, keepalive creation and heartbeat failure cleanup, driver timeout translation and precedence, monotonic lifecycle and event timing, frequency window bounds, Object Pool maintenance isolation, constructor updates, and affected consumers.

The Signal, Pool, and Object Pool split manifests validate strictly. Repository-wide stale-reference checks, package-checklist parity, and git diff --check are clean.

Summary by CodeRabbit

  • New Features

    • Connection pools now apply configured connection timeouts to supported database and Redis drivers.
    • Signal shutdown now reliably cancels active signal watchers without timeout polling.
    • Object pools no longer require container injection during construction.
  • Bug Fixes

    • Improved heartbeat failure cleanup and pool maintenance isolation.
    • More accurate elapsed-time, idle, lifetime, and frequency calculations.
    • Pool maintenance continues when an individual pool encounters an error.
  • Documentation

    • Clarified connection minimums, trimming behavior, lifecycle ownership, and signal handling.

Replace timeout polling and reversible stopped state with terminal manager-owned teardown. SignalManager now tracks only coroutines parked in native signal waits and exception-cancels those exact waiters during process exit, while allowing active handlers to finish normally.

Preserve transactional rollback when watcher creation fails, stop retrying native wait errors, and enforce exact supported process types. Remove the obsolete signal.timeout configuration and update lifecycle listeners accordingly.

Document handler concurrency and native shutdown-signal ownership, add deterministic single- and multi-waiter teardown coverage, protect active handler completion, and clean up Signal test typing and stale teardown state.
Record the final Signal architecture trace, accepted findings, deterministic watcher-ownership design, rejected overengineering concerns, regression coverage, performance assessment, API and configuration impact, validation results, and independent review sign-off.

Mark Signal complete in the package checklist and advance the audit routing index to Pool with the existing pool-01 and pool-02 revalidation context.
Delete the unused coroutine Context helper and remove the resulting hypervel/context split-package dependency.

Declare psr/log directly because the retained KeepaliveConnection extension surface types LoggerInterface. This keeps package metadata aligned with the code and avoids relying on transitive dependencies.
Make heartbeat registration transactional after a KeepaliveConnection acquires and queues its active resource. A timer creation failure now closes that resource, clears connected state, and rethrows the original creation exception.

Reuse the same close boundary when a heartbeat callback fails. If a concurrent caller temporarily owns the resource, clear state so its existing finally path drops the resource instead of requeueing it, without replacing the primary heartbeat failure.

Cover coroutine creation failure and heartbeat callback failure with regressions that assert disconnected state and exactly one close.
Measure query execution and cumulative-duration thresholds with hrtime instead of wall time. Clock adjustments can no longer produce negative or inflated query durations while the existing number of clock reads remains unchanged.

Add a domain regression that supplies a monotonic start time and verifies the elapsed result stays within a realistic millisecond range; the previous wall-clock subtraction fails this check because of the epoch gap.
Measure Redis command success and failure durations with hrtime so wall-clock adjustments cannot produce invalid event timings. The command path retains the same start and end read count and constructs no new hot-path machinery.

Extend both CommandExecuted and CommandFailed event regressions to require a present, non-negative elapsed duration while preserving the existing command, parameters, connection, and exception assertions.
Apply the validated pool connect_timeout through each native connection primitive. MySQL and MariaDB receive PDO::ATTR_TIMEOUT, PostgreSQL receives a DSN connect_timeout, and Redis receives its native timeout; explicit driver configuration takes precedence and SQLite remains unchanged.

Move pooled idle, release, heartbeat, and generation-lifetime timestamps to one monotonic domain across Pool, Database, Redis, and SimplePool. Existing clock-read counts are preserved, preventing wall-clock changes from prematurely recycling or over-retaining connections without adding a clock abstraction.

Correct Frequency to retain exactly its configured ten buckets. Remove its unused Pool reference and the false LowFrequencyInterface constructor requirement, then replace the empty Redis Frequency alias with the Pool implementation directly.

Add regression coverage for timeout translation and precedence, lifecycle timestamp domains, exact frequency windows, and callback-created SimplePool connection reuse. The changes add no cancellation wrapper, retry loop, or additional borrow/release work.
Document Pool provenance and the explicit borrow, release, discard, and terminal close ownership model for long-lived workers.

Describe min_connections as a lower bound for excess-idle trimming of the total managed count. It does not prewarm, replenish, guarantee idle capacity, or guarantee a total minimum; callers that require a new connection pay its establishment cost.

Update the Pool contracts and Database and Redis guides to use the same terminology, explain that lifecycle failures can reduce the managed count below the configured floor, and retain the documented native connect timeout behavior.
Record pool-03 through pool-09 and the linked Database and Redis duration findings, including final ownership boundaries, native timeout precedence, monotonic clock domains, approved interface cleanup, managed-count trimming semantics, and rejected overengineering.

Capture the implemented changes, regression coverage, final composer fix results, fresh self-review corrections, independent review sign-off, Laravel-facing result, and hot-path assessment.

Mark Pool complete, add the cross-package revalidation routes for Database and Redis, and advance the active audit package to Object Pool.
Document that flushing the factory clears pools shared by every coroutine and should be reserved for boot or test cleanup.

Mark recycler interval, timer, start, and stop mutations with their true boot/test boundaries so callers do not reconfigure singleton worker state during requests.
Drop the stale direct nesbot/carbon requirement after confirming the package has no remaining Carbon usage.

Retain the Container and Contracts split dependencies because recycler error reporting, coordinator startup, and custom pool-factory resolution still use them.
Isolate each registered pool maintenance transaction so a custom pool that throws cannot starve unrelated pools during the same recycler tick. Report a registry-identity wrapper while retaining the exact original exception, and stop later maintenance only for the failed pool.

Remove the unread Container constructor chain from ObjectPool, SimpleObjectPool, PoolManager, and the Sentry transport pool. Update every Object Pool, Sentry, Filesystem, Queue, and Broadcasting construction site without adding a compatibility shim.

Add deterministic coverage proving failure reporting, per-pool short-circuiting, and continued maintenance of later pools. Normal borrow, release, proxy, request, job, and transport paths remain unchanged.
Update the completed Object Pool lifecycle design to show the current dependency-minimal PoolManager and SimpleObjectPool constructors.

Remove only the obsolete container setup from the snippets while preserving the plan discussion of legitimate container-backed factory resolution.
Record the accepted Object Pool findings, rejected speculative concerns, implementation boundaries, validation evidence, performance assessment, independent review sign-off, and owner approval.

Mark Object Pool complete, route the active audit to Process, and preserve the cross-package dependency index without introducing an unsupported revalidation requirement.
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ac1bea75-e292-4061-a9e7-6e0c7d9a3e2e

📥 Commits

Reviewing files that changed from the base of the PR and between 4f3367a and 4d20d79.

📒 Files selected for processing (76)
  • docs/plans/2026-07-10-object-pool-lifecycle-and-client-pooled-filesystems.md
  • docs/plans/2026-07-12-framework-coroutine-state-lifecycle-audit-ledger.md
  • docs/plans/2026-07-12-framework-coroutine-state-lifecycle-audit.md
  • src/boost/docs/database.md
  • src/boost/docs/redis.md
  • src/contracts/src/Pool/PoolInterface.php
  • src/contracts/src/Pool/PoolOptionInterface.php
  • src/database/src/Connection.php
  • src/database/src/Connectors/PostgresConnector.php
  • src/database/src/Pool/DbPool.php
  • src/database/src/Pool/PooledConnection.php
  • src/foundation/config/signal.php
  • src/object-pool/composer.json
  • src/object-pool/src/Contracts/Factory.php
  • src/object-pool/src/Contracts/Recycler.php
  • src/object-pool/src/ObjectPool.php
  • src/object-pool/src/PoolManager.php
  • src/object-pool/src/PoolRecycler.php
  • src/object-pool/src/SimpleObjectPool.php
  • src/pool/README.md
  • src/pool/composer.json
  • src/pool/src/Connection.php
  • src/pool/src/Context.php
  • src/pool/src/Frequency.php
  • src/pool/src/KeepaliveConnection.php
  • src/pool/src/LowFrequencyInterface.php
  • src/pool/src/Pool.php
  • src/pool/src/PoolOption.php
  • src/pool/src/SimplePool/Connection.php
  • src/redis/src/Frequency.php
  • src/redis/src/Pool/RedisPool.php
  • src/redis/src/RedisConnection.php
  • src/redis/src/RedisProxy.php
  • src/sentry/src/SentryServiceProvider.php
  • src/sentry/src/Transport/Pool.php
  • src/signal/README.md
  • src/signal/src/SignalDeregisterListener.php
  • src/signal/src/SignalManager.php
  • src/signal/src/SignalServiceProvider.php
  • tests/Broadcasting/BroadcastPoolProxyTest.php
  • tests/Database/DatabaseConnectorTest.php
  • tests/Database/PoolFactoryTest.php
  • tests/Database/QueryDurationThresholdTest.php
  • tests/Filesystem/ClientPooledFilesystemTest.php
  • tests/Filesystem/FileResponseBuilderTest.php
  • tests/Filesystem/FilesystemManagerTest.php
  • tests/Filesystem/FilesystemPoolProxyTest.php
  • tests/Filesystem/LeasedStreamTest.php
  • tests/Integration/Database/PooledConnectionTest.php
  • tests/Integration/Database/Sqlite/DbPoolHeartbeatTest.php
  • tests/ObjectPool/HasPoolProxyTest.php
  • tests/ObjectPool/LeaseTest.php
  • tests/ObjectPool/ObjectPoolNonCoroutineTest.php
  • tests/ObjectPool/ObjectPoolTest.php
  • tests/ObjectPool/PoolManagerTest.php
  • tests/ObjectPool/PoolProxyTest.php
  • tests/ObjectPool/PoolRecyclerTest.php
  • tests/ObjectPool/SimpleObjectPoolTest.php
  • tests/Pool/ConnectionTest.php
  • tests/Pool/FrequencyTest.php
  • tests/Pool/HeartbeatConnectionTest.php
  • tests/Pool/PoolNonCoroutineTest.php
  • tests/Pool/PoolTest.php
  • tests/Pool/SimplePoolTest.php
  • tests/Queue/PooledJobWorkerTest.php
  • tests/Queue/QueueBeanstalkdJobTest.php
  • tests/Queue/QueuePoolProxyTest.php
  • tests/Queue/QueueSqsJobTest.php
  • tests/Redis/RedisConnectionTest.php
  • tests/Redis/RedisPoolHeartbeatTest.php
  • tests/Redis/RedisPoolTest.php
  • tests/Redis/RedisProxyTest.php
  • tests/Signal/SignalDeregisterListenerTest.php
  • tests/Signal/SignalManagerCreateFailureTest.php
  • tests/Signal/SignalManagerTest.php
  • tests/Signal/SignalRegisterListenerTest.php
💤 Files with no reviewable changes (13)
  • src/redis/src/Frequency.php
  • src/pool/src/LowFrequencyInterface.php
  • tests/Filesystem/FileResponseBuilderTest.php
  • tests/Queue/QueueSqsJobTest.php
  • tests/Queue/QueueBeanstalkdJobTest.php
  • tests/ObjectPool/ObjectPoolNonCoroutineTest.php
  • src/pool/src/Context.php
  • src/foundation/config/signal.php
  • src/object-pool/src/ObjectPool.php
  • src/sentry/src/SentryServiceProvider.php
  • tests/ObjectPool/LeaseTest.php
  • tests/Signal/SignalManagerCreateFailureTest.php
  • tests/Filesystem/LeasedStreamTest.php

📝 Walkthrough

Walkthrough

The PR revises signal watcher shutdown, pool timing and timeout handling, object-pool construction and recycler isolation, related package dependencies, documentation, audit plans, and corresponding tests across database, Redis, signal, object-pool, filesystem, queue, and Sentry components.

Changes

Signal lifecycle

Layer / File(s) Summary
Watcher shutdown and cancellation
src/signal/src/*, src/foundation/config/signal.php
Signal watchers use indefinite native waits, track waiting coroutine IDs, and cancel them through SignalManager::stop().
Signal validation and audit records
tests/Signal/*, src/signal/README.md, docs/plans/*
Tests cover watcher cancellation and post-stop behavior; signal lifecycle documentation and audit checklist entries are updated.

Pool resource lifecycle

Layer / File(s) Summary
Native timeout propagation
src/database/src/*, src/redis/src/Pool/*, tests/Database/*, tests/Redis/*
Configured connection timeouts are applied to supported native drivers without replacing explicit options.
Monotonic timing and heartbeat cleanup
src/database/src/*, src/pool/src/*, src/redis/src/*, tests/Pool/*, tests/Redis/*
Elapsed, idle, lifetime, release, and command timings use hrtime; heartbeat failures close connections deterministically and frequency expiration boundaries are corrected.
Pool contracts and documentation
src/boost/docs/*, src/contracts/src/Pool/*, src/pool/README.md, src/pool/composer.json
Pool minimum-connection semantics, connection ownership, and package dependencies are updated.

Object-pool lifecycle

Layer / File(s) Summary
Container-free construction
src/object-pool/src/*, src/sentry/src/*, tests/ObjectPool/*, tests/Filesystem/*, tests/Queue/*
Object-pool constructors and dependent call sites no longer inject application containers.
Recycler maintenance isolation
src/object-pool/src/PoolRecycler.php, tests/ObjectPool/PoolRecyclerTest.php
Per-pool maintenance failures are wrapped, reported, and isolated so subsequent pools continue processing.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: hardening signal and pool lifecycle behavior.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch audit/deterministic-resource-lifecycles

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

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown

Greptile Summary

This PR hardens resource lifecycle ownership across Signal, Pool, and Object Pool subsystems for long-lived Swoole workers where normal GC and request teardown cannot be relied upon.

  • Signal: SignalManager now waits indefinitely per signal (no polling timeout) and tracks exactly the parked coroutine IDs; stop() snapshots and exception-cancels those IDs directly. Watcher creation is transactional and a false return from an indefinite wait is treated as terminal rather than retried.
  • Pool: KeepaliveConnection adds closeAfterFailure() so heartbeat-timer creation failure and heartbeat-callback failure both close the acquired resource before propagating. Frequency.flush() is corrected from $now - $this->time to $now - $this->time + 1, which retains exactly the configured window of seconds instead of one extra. All timing reads migrate from microtime(true) to hrtime(true) / 1e9 for monotonic safety.
  • Object Pool: PoolRecycler wraps each pool's maintenance in an isolated try/catch so a throwing pool is reported with its registry identity and skips only its own maintenance tick. ObjectPool, PoolManager, SimpleObjectPool, and the Sentry transport pool drop unused Container constructor arguments.

Confidence Score: 4/5

Safe to merge; no incorrect data paths or broken contracts introduced

The changes are cohesive and well-tested. The Frequency window fix is a genuine bug correction. The hrtime migration is consistent across every timing site. The lifecycle hardening in KeepaliveConnection, PoolRecycler, and SignalManager follows correct coroutine-ownership logic. The two P2 observations are a narrow shutdown race in SignalManager and the zero-timeout edge case in configureConnectTimeout — neither causes incorrect behaviour in typical deployments.

src/signal/src/SignalManager.php (shutdown race), src/database/src/Pool/DbPool.php (zero-timeout write)

Important Files Changed

Filename Overview
src/signal/src/SignalManager.php Major redesign of shutdown logic: removes polling-based stop in favour of exact-coroutine cancellation; waiting IDs tracked in $waiting and snapshot-cancelled on stop()
src/pool/src/KeepaliveConnection.php Adds closeAfterFailure() for heartbeat-timer-creation and heartbeat-callback failure paths; migrates lastUseTime to hrtime
src/pool/src/Frequency.php Fixes flush() window off-by-one: retains exactly $this->time buckets; removes unused Pool constructor parameter
src/database/src/Pool/DbPool.php Adds configureConnectTimeout() propagating pool timeout to native PDO/DSN; migrates heartbeat timing to hrtime
src/database/src/Connectors/PostgresConnector.php Adds connect_timeout to libpq DSN using extract()-provided variable
src/object-pool/src/PoolRecycler.php Per-pool try/catch isolation; failure reported with registry identity via PoolErrorReporter
src/redis/src/Pool/RedisPool.php Replaces Redis\Frequency alias with Pool\Frequency; propagates connect_timeout to Redis native timeout; migrates to hrtime
src/redis/src/RedisConnection.php Migrates all timing reads from microtime to hrtime for monotonic consistency
src/database/src/Pool/PooledConnection.php Migrates all timing reads from microtime to hrtime for monotonic consistency
src/sentry/src/Transport/Pool.php Removes unused Container constructor argument

Fix All in Claude Code Fix All in Codex

Reviews (1): Last reviewed commit: "chore(audit): complete object-pool lifec..." | Re-trigger Greptile

Comment thread src/signal/src/SignalManager.php
Comment thread src/database/src/Pool/DbPool.php
@binaryfire
binaryfire merged commit 60471c9 into 0.4 Jul 19, 2026
36 checks passed
@binaryfire
binaryfire deleted the audit/deterministic-resource-lifecycles branch July 21, 2026 09:09
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