Rebuild object pool lifecycles and cloud filesystem pooling - #429
Conversation
Record the verified defects, design decisions, implementation details, test matrix, and finishing criteria for the object-pool lifecycle rebuild and dynamic resource pooling work. The plan captures the intended architecture as a durable reference for reviewers and future maintainers.
Document that Swoole channel operations can terminate fatally after the native runtime has been torn down. This makes the deterministic lifecycle requirement explicit and prevents future cleanup paths from invoking native channel methods from destructors or garbage collection.
Introduce normalized pool options, canonical construction fingerprints, and immutable resource definitions. Definitions make pool identity, resource type, construction equivalence, and lifecycle options explicit. Strict validation and deterministic hashing prevent typo-driven configuration drift and unsafe convergence across different resources.
Replace separate coroutine and non-coroutine object stores with one canonical queue and a state-change signal. Objects released in either execution mode are now visible everywhere, exhausted non-coroutine borrowers follow the normal failure path, and coalesced notifications wake waiters without risking an indefinitely blocking channel push.
Rebuild object checkout around explicit managed and borrowed identity tracking, reserved creation capacity, monotonic deadlines, deterministic closure, and no-throw destruction reporting. Add leases for exactly-once release or discard across synchronous and deferred work. Double releases, foreign objects, duplicate factory results, expired objects, suspended factories, and late releases after closure now have defined and regression-tested behavior.
Register pools through immutable definitions and verify resource type, construction fingerprint, and normalized options on reuse. Resolve pools per operation so retained proxies survive purge and idle eviction, remove unsafe public magic forwarding, preserve primary exceptions during finalization, and centralize namespaced automatic and explicit pool identities.
…tenance Replace ratio-based recycling with direct expired-object sweeping, idle trimming, and whole-pool idle TTL eviction. The recycler now removes exact registered instances safely, validates timer intervals, reports maintenance failures, and cannot reset user-activity clocks merely by inspecting idle objects. Obsolete strategy contracts and tests are removed.
Give connection pools the same canonical queue and non-blocking state signaling model as general object pools. Connections released inside or outside a coroutine remain mutually visible, waiters react to both released objects and freed capacity, and signaling is safely coalesced without indefinite pushes.
Reject invalid capacities, non-finite or non-positive durations, undocumented disable sentinels, malformed event lists, and unknown option keys before they reach live pool arithmetic. The validation covers constructors and mutable setters, preserving the established configuration surface while making configuration mistakes fail early with actionable messages.
Track managed, borrowed, and in-flight connections explicitly; reserve capacity before yielding factories; and replace ambiguous full flushing with deterministic terminal closure. Health-check failures now destroy suspect connections without stranding capacity, maintenance preserves idle clocks, late releases are destroyed after closure, and waiters wake on every capacity-relevant state transition.
Migrate database pools to deterministic closure and detach registry entries before teardown can yield, allowing concurrent resolvers to create a fresh pool immediately. Heartbeat maintenance now uses the central ownership paths, shared in-memory SQLite state is cleared only on terminal close, and parallel-safe scratch paths prevent workers from sharing database files.
Detach Redis pools before deterministic closure, remove generation bookkeeping made obsolete by terminal lifecycle semantics, and route heartbeat outcomes through the central ownership model. Healthy probes preserve idle timestamps, failed probes discard suspect connections, late releases cannot re-enter closed pools, and Horizon cleanup now invalidates pools through the factory lifecycle.
Run Redis-backed funnel cleanup inside the test coroutine so corrected cross-mode pool storage never hands a coroutine-created socket to outside-coroutine I/O. Clarify database teardown ordering around terminal pool closure and late releases while preserving the existing isolated cleanup coroutine.
Adapt Sentry's standalone transport pool to the rebuilt ownership API and expose only lifecycle options that an unmanaged pool can actually enforce. A transport that throws during send is now discarded rather than returned as healthy, teardown remains exactly once under coroutine defers, and unsupported maintenance-only configuration fails fast.
Normalize direct paths, make temporary URL callback handling support every valid closure kind, and provide a consistent ranged-stream contract across local, S3, and GCS adapters. Extract range-aware response construction, preserve exact byte limits and primary exceptions, fix malformed and suffix range handling, retain nested S3 HTTP options, align GCS failure semantics, and keep pooled clients leased until returned streams are closed.
Pool the expensive S3 and GCS SDK clients by their exact construction configuration while rebuilding cheap bucket, prefix, visibility, and adapter stacks for each operation. Add immutable pool convergence, safe on-demand builds, shared-resource purge semantics, borrow-scoped raw access, callback isolation, explicit pooled-disk method surfaces, scoped-parent expansion, and regression coverage for dynamic credentials and buckets.
Add per-operation scoped filesystem decorators for request, user, team, or tenant prefixes without mutating shared disk configuration. The boundary resolves each prefix once, fails closed for empty scopes, rejects traversal and unprefixed escape hatches, validates upload targets before I/O, strips returned paths defensively, and explicitly maps the complete safe filesystem surface.
Add explicit opt-in pooling for MailManager build operations while preserving named mailer defaults and direct construction for nested composite children. Pool identities derive from the complete resolved transport input, including service credential fallbacks and recursively resolved composite children. Purge, idle TTL reclamation, global-address handling, and round-robin retry configuration are corrected and covered.
Use non-null connection and queue names throughout job state, normalize nullable synchronous queue input at construction, and initialize framework fakes with meaningful defaults. This aligns concrete jobs with the queue job contract and prevents inherited accessors from reading uninitialized properties in application and Horizon test fixtures.
Attach an object-pool lease to jobs popped from pooled Beanstalkd and SQS connections so backend clients remain exclusively borrowed until delete, release, or bury completes. Successful terminal calls release the connection, failed protocol calls discard it, unsupported third-party jobs are requeued before failing closed, post-terminal client access is guarded, connection names are applied per borrow, and manager purge and application swaps invalidate pools safely.
Build broadcaster pools from resource construction input instead of logical connection names and reapply authenticated-user resolver state on every borrow, including explicit clearing. Purge now invalidates shared pools correctly, custom creators never receive pool metadata, converged construction diagnostics remain name-neutral, contract-only implementations fail only when using unsupported callback capabilities, and Reverb stays on its safe shared client path.
Stop wrapping the Slack notification router in an object pool because the router owns no external resource and the container already shares the same instance. Direct resolution preserves behavior while eliminating duplicate-factory ownership violations, unnecessary configuration surface, and a proxy whose nullable response return could not be modeled safely by generic forwarding.
Remove HTTP client object pooling and retain connection reuse at the state-owning low-level handler, with a fresh middleware stack and cookie jar for every pending request. Add explicit connection presets and option-layer precedence, canonical reserved-option validation, safe re-registration, request-local retry cookies, custom client and handler overrides, and concurrency coverage proving handler sharing without middleware or cookie leakage.
Describe immutable definitions, canonical fingerprints, normalized options, managed convergence, leases, ownership rules, deterministic closure, maintenance, and explicit proxy surfaces. Rename the public guide to object-pools and distinguish the general-purpose ObjectPool package from Hypervel's internal lower-level connection-pool infrastructure.
Explain client-level S3 and GCS pooling, automatic and explicit identities, lifecycle options, shared-resource purge semantics, and borrow-scoped access to client internals. Document dynamic scoped filesystem decorators, fail-closed prefix behavior, on-demand disk convergence, advanced GCS client configuration, and the relevant differences from Laravel.
Document the named-mailer and MailManager build pooling matrix, default lifecycle options, credential-derived convergence, custom transport safety gates, composite transport behavior, and explicit invalidation. Include a multi-provider tenant example showing how dynamic credentials reuse connections without requiring tenant-specific framework APIs.
Explain queue pool identity, options, purge behavior, job-held leases, terminal release and discard semantics, and capacity sizing for concurrent workers. The guide now makes the Beanstalkd same-connection requirement and SQS lease behavior explicit for operators configuring pooled queue workers.
Replace client-pool documentation with the named connection preset and shared low-level transport-handler model. Document option precedence, request-local middleware and cookies, reserved settings, transport sharing, re-registration behavior, and the distinction between reusable connections and per-request client state.
Explain that the Slack router is a shared stateless dispatcher rather than a pooled external resource. Direct custom channel authors toward the general object-pool API only when their implementation actually owns reusable mutable or network resources.
Add a focused design-register item to verify whether the existing Pusher and Ably broadcaster pools isolate any state that their SDK clients cannot safely share. This preserves the current proven behavior while making the remaining resource-ownership question explicit for a dedicated future review.
|
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 (14)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughHypervel rebuilds pooling around immutable identities, validated options, explicit ownership, leases, idle maintenance, and terminal closure. Filesystem, HTTP, mail, queue, broadcasting, Sentry, watcher, configuration, documentation, and test integrations are updated for these lifecycle contracts. ChangesPooling and lifecycle
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 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 |
Remove two non-essential documentation changes from this branch so the primary architectural PR remains within external review limits. Exact copies are preserved outside the components repository for a follow-up change after this PR merges.
Greptile SummaryThis PR is a comprehensive rebuild of the object pool and connection pool lifecycle primitives in Hypervel, replacing the previous ad-hoc approach with explicit ownership tracking (
Confidence Score: 4/5This PR is safe to merge; the concurrency invariants are sound and no P0/P1 bugs were found across the ~40 changed files. Thorough review of all major changed files found no blocking bugs: pool ownership semantics are correct under Swoole's cooperative scheduler, Lease finalization is exactly-once, Channel close correctly wakes all parked waiters, PoolRecycler iteration is safe against concurrent remove(), and DbPool heartbeat uses the correct requeue/destroy paths. The score is 4 rather than 5 only because of the size and complexity of the change — several subsystems (queue lease teardown, composite transport cycle detection, HTTP option-layer merging) interact in ways that warrant careful integration testing before production promotion. src/queue/src/QueuePoolProxy.php and src/queue/src/Jobs/BeanstalkdJob.php / SqsJob.php — the cascading lease-teardown paths on pop failure and job-requeue fallback are the most intricate new code paths and deserve focused integration testing. Important Files Changed
Reviews (3): Last reviewed commit: "Document filesystem architecture follow-..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
src/queue/src/QueuePoolProxy.php (1)
152-168: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: document the defensive inner
discard().This nested
$lease->discard()inside the recovery catch is a safe idempotent no-op whenever the job's own terminalrelease()already finalized the lease (the common case), and only matters ifwithPoolLease()fails before actually attaching the lease. A one-line comment explaining that edge case would help future readers, since it's not obvious why a discard follows a call that (in the common path) already finalized the lease itself.🤖 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/queue/src/QueuePoolProxy.php` around lines 152 - 168, In the recovery path around withPoolLease(), add a concise comment immediately before $lease->discard() explaining that release(0) usually finalizes the lease, while discard() safely handles failures that occur before the lease is attached.src/broadcasting/src/BroadcastManager.php (1)
280-289: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the duplicated pool-definition construction.
Arr::except($config, ['pool'])followed bypoolDefinition($config['driver'], $config['pool'] ?? [], $constructionConfig)is repeated identically inresolve()andpurge(). Consider a small private helper (e.g.poolDefinitionForConfig(array $config): array{PoolDefinition, array}or similar) to keep the two call sites from drifting.♻️ Suggested consolidation
+ /** + * Build the construction config and pool definition for a broadcaster config. + * + * `@return` array{0: array, 1: PoolDefinition} + */ + protected function poolDefinitionForConfig(array $config): array + { + $constructionConfig = Arr::except($config, ['pool']); + + return [ + $constructionConfig, + $this->poolDefinition($config['driver'], $config['pool'] ?? [], $constructionConfig), + ]; + }Also applies to: 494-501
🤖 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/broadcasting/src/BroadcastManager.php` around lines 280 - 289, Extract the repeated pool setup from resolve() and purge() into a private helper, such as poolDefinitionForConfig(), that derives constructionConfig with Arr::except and builds the pool definition using the driver and optional pool configuration. Update both call sites to reuse the helper while preserving the existing createPoolProxy and doResolve behavior.src/http/src/Client/PendingRequest.php (1)
1834-1846: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument the two-step layer-merge semantics.
mergeOptionLayers()relies on a subtle interaction:array_merge_recursiveaccumulates mergeable keys (e.g. headers) across layers, thenarray_replace_recursiveagainst the raw layer overrides same-key values rather than duplicating them. This correctly avoids turning connection-level headers into multi-value arrays when a later layer redefines the same header, but the reasoning isn't obvious from the code. A short inline comment explaining why both calls are needed (and that same-key values override across layers while distinct keys accumulate) would help future maintainers avoid "simplifying" this into a bug.🤖 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/http/src/Client/PendingRequest.php` around lines 1834 - 1846, Add a concise inline comment in mergeOptionLayers() documenting that array_merge_recursive accumulates distinct mergeable options across layers, while array_replace_recursive applies the raw layer so later same-key values override instead of becoming duplicated arrays; preserve the existing two-step merge behavior.tests/Mail/MailManagerTest.php (1)
559-604: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor duplication between the two purge/forget tests.
testPurgeInvalidatesACachedTransportPoolandtestForgetIsCacheOnlyAndUncachedPurgeDerivesThePoolIdentityrepeat the samemail.mailers.smtpsetup and pool-lookup boilerplate. Could extract a small helper, but this is optional given the tests are otherwise clear and independent.🤖 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 `@tests/Mail/MailManagerTest.php` around lines 559 - 604, Optionally extract the repeated SMTP configuration, MailManager/transport creation, PoolFactory lookup, and pool identity setup from testPurgeInvalidatesACachedTransportPool and testForgetIsCacheOnlyAndUncachedPurgeDerivesThePoolIdentity into a small private test helper, then reuse it while keeping both tests independent and behavior unchanged.src/filesystem/src/FilesystemManager.php (1)
763-769: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse a named default for cached static state.
Define
DEFAULT_S3_ARGUMENT_NAMES, initialize$s3ArgumentNamesfrom it, and reset to that constant influshState().As per coding guidelines,
src/**/src/**.php: “reset static defaults usingDEFAULT_*class constants when the initial value and reset value match.”🤖 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/filesystem/src/FilesystemManager.php` around lines 763 - 769, Define a DEFAULT_S3_ARGUMENT_NAMES class constant in FilesystemManager, initialize the static $s3ArgumentNames property from that constant, and update flushState() to reset it to the same constant instead of null.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/filesystem/src/FileResponseBuilder.php`:
- Around line 74-96: Update build() to detect HEAD requests before registering
or executing the streaming callback, returning a response with the existing
headers and status but no body. Preserve the current streaming behavior for
non-HEAD requests, including resolver invocation and stream cleanup.
---
Nitpick comments:
In `@src/broadcasting/src/BroadcastManager.php`:
- Around line 280-289: Extract the repeated pool setup from resolve() and
purge() into a private helper, such as poolDefinitionForConfig(), that derives
constructionConfig with Arr::except and builds the pool definition using the
driver and optional pool configuration. Update both call sites to reuse the
helper while preserving the existing createPoolProxy and doResolve behavior.
In `@src/filesystem/src/FilesystemManager.php`:
- Around line 763-769: Define a DEFAULT_S3_ARGUMENT_NAMES class constant in
FilesystemManager, initialize the static $s3ArgumentNames property from that
constant, and update flushState() to reset it to the same constant instead of
null.
In `@src/http/src/Client/PendingRequest.php`:
- Around line 1834-1846: Add a concise inline comment in mergeOptionLayers()
documenting that array_merge_recursive accumulates distinct mergeable options
across layers, while array_replace_recursive applies the raw layer so later
same-key values override instead of becoming duplicated arrays; preserve the
existing two-step merge behavior.
In `@src/queue/src/QueuePoolProxy.php`:
- Around line 152-168: In the recovery path around withPoolLease(), add a
concise comment immediately before $lease->discard() explaining that release(0)
usually finalizes the lease, while discard() safely handles failures that occur
before the lease is attached.
In `@tests/Mail/MailManagerTest.php`:
- Around line 559-604: Optionally extract the repeated SMTP configuration,
MailManager/transport creation, PoolFactory lookup, and pool identity setup from
testPurgeInvalidatesACachedTransportPool and
testForgetIsCacheOnlyAndUncachedPurgeDerivesThePoolIdentity into a small private
test helper, then reuse it while keeping both tests independent and behavior
unchanged.
🪄 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: a8f469e1-aa8d-4d43-aba6-d63d16763514
📒 Files selected for processing (150)
docs/plans/2026-07-10-object-pool-lifecycle-and-client-pooled-filesystems.mdsrc/boost/docs-ported.mdsrc/boost/docs/filesystem.mdsrc/boost/docs/http-client.mdsrc/boost/docs/mail.mdsrc/boost/docs/notifications.mdsrc/boost/docs/object-pools.mdsrc/boost/docs/queues.mdsrc/broadcasting/src/BroadcastManager.phpsrc/broadcasting/src/BroadcastPoolProxy.phpsrc/broadcasting/src/Broadcasters/Broadcaster.phpsrc/contracts/src/Pool/PoolInterface.phpsrc/database/src/Pool/DbPool.phpsrc/database/src/Pool/PoolFactory.phpsrc/database/src/Pool/PooledConnection.phpsrc/engine/src/Channel.phpsrc/filesystem/src/AwsS3V3Adapter.phpsrc/filesystem/src/ClientPooledFilesystem.phpsrc/filesystem/src/Concerns/InteractsWithPooledFilesystem.phpsrc/filesystem/src/FileResponseBuilder.phpsrc/filesystem/src/FilesystemAdapter.phpsrc/filesystem/src/FilesystemManager.phpsrc/filesystem/src/FilesystemPoolProxy.phpsrc/filesystem/src/GoogleCloudStorageAdapter.phpsrc/filesystem/src/LeasedStream.phpsrc/filesystem/src/LocalFilesystemAdapter.phpsrc/filesystem/src/ScopedCloudFilesystemProxy.phpsrc/filesystem/src/ScopedFilesystemProxy.phpsrc/foundation/config/broadcasting.phpsrc/foundation/config/filesystems.phpsrc/foundation/config/queue.phpsrc/foundation/src/Testing/Concerns/InteractsWithTestCaseLifecycle.phpsrc/http/src/Client/ClientPoolProxy.phpsrc/http/src/Client/Factory.phpsrc/http/src/Client/PendingRequest.phpsrc/http/src/Client/ReservedOptions.phpsrc/mail/src/MailManager.phpsrc/mail/src/TransportPoolProxy.phpsrc/notifications/src/ChannelManager.phpsrc/notifications/src/NotificationPoolProxy.phpsrc/object-pool/README.mdsrc/object-pool/src/Channel.phpsrc/object-pool/src/Contracts/Factory.phpsrc/object-pool/src/Contracts/ObjectPool.phpsrc/object-pool/src/Contracts/RecycleStrategy.phpsrc/object-pool/src/Contracts/Recycler.phpsrc/object-pool/src/Contracts/TimeStrategy.phpsrc/object-pool/src/Lease.phpsrc/object-pool/src/ObjectPool.phpsrc/object-pool/src/ObjectPoolServiceProvider.phpsrc/object-pool/src/ObjectRecycler.phpsrc/object-pool/src/PoolDefinition.phpsrc/object-pool/src/PoolErrorReporter.phpsrc/object-pool/src/PoolFingerprint.phpsrc/object-pool/src/PoolManager.phpsrc/object-pool/src/PoolOption.phpsrc/object-pool/src/PoolOptions.phpsrc/object-pool/src/PoolProxy.phpsrc/object-pool/src/PoolRecycler.phpsrc/object-pool/src/SimpleObjectPool.phpsrc/object-pool/src/Strategies/TimeStrategy.phpsrc/object-pool/src/Traits/HasPoolProxy.phpsrc/pool/src/Channel.phpsrc/pool/src/Connection.phpsrc/pool/src/ConstantFrequency.phpsrc/pool/src/Pool.phpsrc/pool/src/PoolOption.phpsrc/queue/src/Jobs/BeanstalkdJob.phpsrc/queue/src/Jobs/DatabaseJob.phpsrc/queue/src/Jobs/FakeJob.phpsrc/queue/src/Jobs/Job.phpsrc/queue/src/Jobs/RedisJob.phpsrc/queue/src/Jobs/SqsJob.phpsrc/queue/src/Jobs/SyncJob.phpsrc/queue/src/QueueManager.phpsrc/queue/src/QueuePoolProxy.phpsrc/queue/src/SyncQueue.phpsrc/redis/src/Pool/PoolFactory.phpsrc/redis/src/Pool/RedisPool.phpsrc/redis/src/RedisConnection.phpsrc/sentry/config/sentry.phpsrc/sentry/src/SentryServiceProvider.phpsrc/sentry/src/Transport/HttpPoolTransport.phpsrc/sentry/src/Transport/Pool.phpsrc/support/src/Facades/Broadcast.phpsrc/support/src/Facades/Http.phpsrc/support/src/Facades/Mail.phpsrc/support/src/Facades/Notification.phpsrc/support/src/Facades/Queue.phpsrc/support/src/Facades/Storage.phpsrc/testing/src/PHPUnit/AfterEachTestSubscriber.phptests/Broadcasting/BroadcastPoolProxyTest.phptests/Database/PoolFactoryTest.phptests/Filesystem/AwsS3V3AdapterTest.phptests/Filesystem/ClientPooledFilesystemTest.phptests/Filesystem/FileResponseBuilderTest.phptests/Filesystem/FilesystemAdapterTest.phptests/Filesystem/FilesystemManagerTest.phptests/Filesystem/FilesystemPoolProxyTest.phptests/Filesystem/GoogleCloudStorageAdapterTest.phptests/Filesystem/LeasedStreamTest.phptests/Filesystem/ScopedFilesystemProxyTest.phptests/Http/HttpClientTest.phptests/Http/HttpConnectionTest.phptests/Integration/Broadcasting/BroadcastManagerTest.phptests/Integration/Cache/CacheFunnelTestCase.phptests/Integration/Database/PooledConnectionTest.phptests/Integration/Database/Sqlite/DbPoolHeartbeatTest.phptests/Integration/Database/Sqlite/InMemorySqliteSharedPdoTest.phptests/Integration/Database/Sqlite/PoolConnectionManagementTest.phptests/Integration/Database/Sqlite/QueryDurationThresholdPooledTest.phptests/Integration/Engine/HttpClientConnectionTest.phptests/Integration/Horizon/Feature/Listeners/StoreTagsForFailedTest.phptests/Integration/Horizon/IntegrationTestCase.phptests/Mail/MailManagerTest.phptests/Mail/MailSesV2TransportTest.phptests/Notifications/NotificationChannelManagerTest.phptests/ObjectPool/ChannelTest.phptests/ObjectPool/Fixtures/FooPool.phptests/ObjectPool/HasPoolProxyTest.phptests/ObjectPool/LeaseTest.phptests/ObjectPool/ObjectPoolTest.phptests/ObjectPool/ObjectRecyclerTest.phptests/ObjectPool/PoolDefinitionTest.phptests/ObjectPool/PoolErrorReporterTest.phptests/ObjectPool/PoolFingerprintTest.phptests/ObjectPool/PoolManagerTest.phptests/ObjectPool/PoolOptionsTest.phptests/ObjectPool/PoolProxyTest.phptests/ObjectPool/PoolRecyclerTest.phptests/ObjectPool/SimpleObjectPoolTest.phptests/ObjectPool/TimeStrategyTest.phptests/Pool/ChannelTest.phptests/Pool/ConnectionTest.phptests/Pool/FrequencyTest.phptests/Pool/PoolNonCoroutineTest.phptests/Pool/PoolOptionTest.phptests/Pool/PoolTest.phptests/Queue/FakeJobTest.phptests/Queue/PooledJobWorkerTest.phptests/Queue/QueueBeanstalkdJobTest.phptests/Queue/QueueManagerTest.phptests/Queue/QueuePoolProxyTest.phptests/Queue/QueueSqsJobTest.phptests/Redis/PoolFactoryTest.phptests/Redis/RedisConnectionTest.phptests/Redis/RedisPoolHeartbeatTest.phptests/Sentry/ConfigTest.phptests/Sentry/FlushLifecycleTest.phptests/Sentry/HttpPoolTransportTest.php
💤 Files with no reviewable changes (15)
- tests/ObjectPool/Fixtures/FooPool.php
- src/notifications/src/NotificationPoolProxy.php
- src/object-pool/src/Strategies/TimeStrategy.php
- src/object-pool/src/Contracts/RecycleStrategy.php
- src/http/src/Client/ClientPoolProxy.php
- src/object-pool/src/Contracts/TimeStrategy.php
- src/object-pool/src/ObjectRecycler.php
- tests/ObjectPool/TimeStrategyTest.php
- tests/ObjectPool/ObjectRecyclerTest.php
- src/sentry/config/sentry.php
- src/object-pool/src/PoolOption.php
- src/pool/src/Connection.php
- src/redis/src/RedisConnection.php
- src/database/src/Pool/PooledConnection.php
- src/notifications/src/ChannelManager.php
Enforce strong If-Range entity-tag comparison, preserve HEAD body suppression, and reject empty non-EOF reads instead of allowing response streams to spin indefinitely. Also fail fast when a non-seekable ranged stream makes no positioning progress, and add regressions for weak validators, truncated ranges, stalled streams, resource closure, and HEAD response emission.
Launch watcher-owned subprocesses with argv arrays so paths and arguments remain literal and process termination targets the real child. Escape every path that must still cross the find drivers' shell boundary. Give fswatch deterministic process and pipe ownership, make driver shutdown explicit and idempotent, and distinguish child exit from read failure without hot spinning. Replace FindNewer's shared predictable anchors with per-driver atomic reference files. Defer cleanup across yielding scans, reject unsafe immediate restarts, and remove every owned file without throwing from destructor paths. Document the list-shaped server command configuration and cover process cleanup, shell metacharacters, reference ownership, stop races, and failure diagnostics.
Turn Filesystem::replace() into a checked write, chmod, and rename transaction. Exact byte-count validation prevents partial content from replacing a valid target, while write-before-chmod supports restrictive final modes and keeps incomplete data private. Normalize tempnam warnings at cache and Testbench boundaries so framework-owned diagnostics remain reachable. Build temporary Blade views through a private checked write followed by an atomic rename, with primary-preserving cleanup on every failure. Add regressions for restrictive modes, missing and unwritable destinations, fallback containment, and temporary-file cleanup.
Explain the layered HTTP option merge semantics and the queue lease discard backstop where the local code is otherwise easy to misread during future maintenance. Document that nullable lazy caches and callback slots use null as a structural sentinel, so static-state cleanup should not manufacture DEFAULT constants for values that are not configurable defaults.
Record the final filesystem response, watcher lifecycle, shell-boundary, reference-file ownership, and transactional temporary-file decisions as implemented. Keep the bug inventory, design rationale, regression matrix, and implementation order aligned with the finished code rather than preserving discarded intermediate ideas.
|
@greptileai @coderabbitai Thanks. I audited every finding against the implementation and pushed the follow-up as five focused commits. Resolved:
Reviewed but intentionally unchanged:
The same-family audit also found and fixed issues beyond the review lines: non-seekable range stalls, fswatch process and pipe leaks and hot spinning, shell-interpreted watcher commands and unescaped find paths, shared and leaked FindNewer reference files including a stop-during-yield race, unchecked tempnam diagnostics, partial atomic replacements, and partial temporary Blade writes. The main review summaries contained no other actionable nitpicks or outside-diff findings. Verification is green from the exact pushed state: formatting, PHPStan, the full parallel suite, Testbench and integration checks, and dogfood. A second full code review found no remaining issues. |
|
Thanks for the thorough follow-up. The resolutions are all sound:
The additional audit findings (fswatch leaks, shell-interpreted watcher commands, unescaped find paths, FindNewer reference file races, tempnam diagnostics, partial atomic writes) are the kind of thing that's easy to miss when reviewing against a specific diff boundary. Good catch on those. Everything looks good. Happy to approve. |
|
Tip For best results, initiate chat on the files or code changes.
On the intentionally-unchanged items, your reasoning holds up:
Thanks also for flagging and fixing the additional same-family issues found during the audit (fswatch leaks/spinning, unescaped find paths, FindNewer races, tempnam diagnostics, atomic/Blade write partial-write issues) — those are exactly the kind of latent bugs that a systematic audit like this surfaces. Given formatting, PHPStan, the full parallel suite, Testbench/integration checks, and dogfood are all green from the pushed state, this looks ready from my side. (=^-ω-^=) ✏️ Learnings added
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/watcher/src/Driver/FindNewerDriver.php`:
- Around line 52-65: Update the scanning flow in the relevant method to record
the next cutoff before calling scan(): touch the alternate reference file first,
then run scan(), and advance the scan index only after scanning completes.
Preserve the existing changed-file handling while ensuring changes occurring
during the scan remain eligible for the next scan.
🪄 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: b473db39-2119-4017-8234-8b89266a8d90
📒 Files selected for processing (26)
AGENTS.mddocs/plans/2026-07-10-object-pool-lifecycle-and-client-pooled-filesystems.mdsrc/boost/docs/mail.mdsrc/filesystem/src/FileResponseBuilder.phpsrc/filesystem/src/Filesystem.phpsrc/filesystem/src/FilesystemAdapter.phpsrc/foundation/src/Console/ConfigCacheCommand.phpsrc/foundation/src/Console/RouteCacheCommand.phpsrc/foundation/src/Testing/Concerns/InteractsWithViews.phpsrc/http/src/Client/PendingRequest.phpsrc/queue/src/QueuePoolProxy.phpsrc/watcher/README.mdsrc/watcher/config/watcher.phpsrc/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/ServerRestartStrategy.phptests/Filesystem/FileResponseBuilderTest.phptests/Filesystem/FilesystemAdapterTest.phptests/Filesystem/FilesystemTest.phptests/Watcher/Driver/FindDriverTest.phptests/Watcher/Driver/FindNewerDriverTest.phptests/Watcher/Driver/FswatchDriverTest.phptests/Watcher/ServerRestartStrategyTest.php
✅ Files skipped from review due to trivial changes (4)
- AGENTS.md
- src/watcher/README.md
- src/watcher/src/Driver/DriverInterface.php
- docs/plans/2026-07-10-object-pool-lifecycle-and-client-pooled-filesystems.md
🚧 Files skipped from review as they are similar to previous changes (6)
- src/boost/docs/mail.md
- src/queue/src/QueuePoolProxy.php
- src/filesystem/src/FilesystemAdapter.php
- src/filesystem/src/FileResponseBuilder.php
- tests/Filesystem/FilesystemAdapterTest.php
- src/http/src/Client/PendingRequest.php
Record the alternate reference timestamp before each scan and rotate reference roles after every successful pass, including quiet scans. This preserves at-least-once detection for files changed after find has already passed their path instead of advancing both cutoffs beyond the change. Observe stop requests after both overridable I/O boundaries so shutdown during a reference update skips the scan, while shutdown during a scan skips rotation and publication. Add deterministic logical-clock and channel-driven regressions for late changes, quiet rotation, scan recovery, and both shutdown windows.
Make the object-pool and connection-pool channel wrappers enforce their terminal lifecycle directly. A push after close now returns false without retaining data in the local queue, matching the boolean API and engine-channel failure semantics. Keep both package implementations synchronized and add mirrored regressions proving rejected values are neither counted nor retrievable after closure.
Preserve recurring Timer execution after callback failures while ensuring the failure is visible. Timer now reports through its configured logger and falls back to the PHP error log when no logger is available; one-shot timer behavior remains unchanged. Apply the same logger-or-error-log rule to heartbeat failures caught inside KeepaliveConnection after clearing the failed connection. Add regressions for both Timer reporting routes, continued execution after failure, and logger-less heartbeat cleanup and diagnostics.
Record the final find-newer cutoff rotation, closed-channel rejection, recurring timer reporting, and keepalive fallback behavior as implemented. Keep the verified bug inventory, lifecycle rationale, regression matrix, and implementation order aligned with the finished code, including both watcher shutdown windows and the deliberate Timer::after distinction.
Describe Hypervel's client-pooled cloud filesystem design, borrow-scoped raw access, and dynamic scoped filesystem decorators in the component README. Record the remaining owner review of Pusher and Ably broadcaster state while preserving their current pooling behavior.
|
@greptileai @coderabbitai Follow-up fixes are pushed. CodeRabbit cutoff finding:
Greptile observations:
The same-family audit found and fixed three additional issues:
The implementation plan and ancillary component documentation were updated in 5ff105f and 92ec1a4. Validation is green from the pushed state: formatting, PHPStan, the complete parallel suite, Testbench and integration checks, and dogfood. A fresh full code review completed with zero findings. |
|
Tip For best results, initiate chat on the files or code changes.
Appreciate the continued rigor — green formatting, PHPStan, full parallel suite, Testbench/integration checks, dogfood, and a fresh zero-finding review gives strong confidence in this state. (=^・ω・^=) ✏️ Learnings added
|
|
All three batches look correct from the commit details. 7844d31 — Both 3dd76ec — Recording the alternate reference before the scan and rotating after every successful pass (including quiet ones) is the right fix for the at-least-once window. The two shutdown checkpoints — after the reference update and after the scan — cover both TOCTOU windows cleanly. The logical-clock regressions are the right tool here since they don't carry timing assumptions. dcb3917 — The audit catches (stop-during-reference-update, silent timer failures, silent heartbeat failures) are exactly the class of issue that surfaces in long-lived workers under load but is hard to reproduce in short test runs — good to have all three nailed with deterministic regressions rather than timing-sensitive ones. Nothing outstanding from my side. The PR is in good shape. |
Summary
Hypervel's existing pooling architecture did not model resource identity, object ownership, deferred lifetimes, invalidation, or coroutine/non-coroutine interaction rigorously enough for long-lived workers. Pools were primarily cached wrappers around factories, which allowed several classes of correctness problems: equivalent dynamic resources could collide, forgotten resources could leave stale pools behind, the same object could occupy multiple pool slots, and a proxy could return a stream, promise, iterator, or queue job after already releasing the resource that still backed it.
This PR rebuilds the pooling foundation around explicit invariants: immutable pool definitions, construction fingerprints, tracked ownership, exactly-once leases, deterministic closure, per-operation pool resolution, and bounded idle reclamation. The lower-level database and Redis connection pools adopt the same ownership and terminal lifecycle model.
Existing consumers are migrated to the corrected architecture. That migration also enables general-purpose dynamic resource use cases: S3 and GCS pool SDK clients independently of bucket-specific adapter stacks, on-demand mailers can opt into pooling, and scoped filesystem decorators can resolve a prefix per operation. These capabilities broaden Hypervel's support for dynamically constructed resources without coupling pools to application-level context or policy.
For more details, see:
docs/plans/2026-07-10-object-pool-lifecycle-and-client-pooled-filesystems.mdBackground
Pooling in a coroutine framework has a stricter contract than retaining a collection of reusable objects. A pool must know which objects it owns, which are currently borrowed, when a result still depends on a borrowed resource, and how callers recover when a pool is purged or evicted while work is in flight.
The previous implementation did not encode these properties directly:
These were architectural issues rather than isolated call-site bugs. Fixing them locally would preserve the same failure modes for the next pooled consumer, so this PR makes the lifecycle rules part of the pool API itself.
Pool model
Definitions and identity
Managed pools are registered through an immutable
PoolDefinitioncontaining:PoolOptions.Automatic fingerprints are produced from a canonical, type-tagged representation of the exact input used to construct the pooled resource. Map ordering does not affect identity, while different scalar types, enum types, list ordering, and resource types remain distinct. Configuration that cannot be canonicalized must declare an explicit fingerprint rather than silently converging.
PoolManager::getOrCreate()reuses a pool only when its resource type, fingerprint, and options match. Conflicting definitions fail immediately with a diagnostic explaining the mismatch. Closed registrations self-heal by detaching the closed instance and constructing a replacement.Explicit pool names remain available, but they do not bypass construction-equivalence checks. Automatic and explicitly named identities occupy separate namespaces.
Ownership and capacity
Pools now track every managed object by identity and separately track borrowed objects and in-flight creation slots. This makes the following invariants enforceable:
Checkout uses one monotonic deadline across waiting, creation, and expired-object replacement. Duration arithmetic saturates safely for very large finite values instead of overflowing nanosecond calculations.
Leases and deferred results
Leaseis the exactly-once finalization primitive for borrowed objects. A lease either releases a healthy object or discards a suspect object, and abandoned leases finalize defensively during destruction without allowing cleanup exceptions to escape.Consumer proxies no longer expose generic public magic forwarding. They enumerate operations whose results are fully consumed during the borrow and use explicit lease-aware implementations for deferred work. Cleanup preserves the primary operation exception when finalization also fails.
Filesystem streams are wrapped by
LeasedStream, which retains the client lease until the stream is explicitly closed or destroyed. Streamed file responses acquire their stream only when emission begins and close it in afinallyblock. Queue jobs retain their backend lease until their terminal delete, release, or bury operation completes.Lifecycle and maintenance
Pools have an explicit, idempotent terminal
close()operation. Closure rejects new borrows, wakes parked waiters, destroys idle objects, and causes late releases to be destroyed. Registries detach a pool before closing it so teardown that yields cannot expose a closing instance to another resolver.The old strategy and ratio-based recycler has been replaced with direct lifecycle policies:
max_lifetimeexpires individual objects absolutely;max_idle_timetrims idle objects down to the retention floor; andidle_ttlremoves an entirely unused managed pool.Maintenance requeues do not update user-activity timestamps. This prevents maintenance itself from keeping idle objects and pools alive forever. Maintenance and destructor-only reporting paths are no-throw and preserve bookkeeping even when resource cleanup fails.
The object and connection pool channels now use one canonical queue in all execution modes. A separate coalesced state signal wakes coroutine waiters without making storage dependent on the caller's coroutine state.
Connection pools
The lower-level
Hypervel\Poolimplementation now follows the same ownership, capacity, wait, and terminal-close semantics as the general object pool.Database and Redis registries remove an exact pool instance before closing it. Heartbeat checks use the centralized destruction and requeue paths, do not reset idle clocks merely by probing a connection, and discard connections whose health check throws. Database closure also clears the shared in-memory SQLite PDO at the correct terminal boundary.
Connection pool options now reject invalid capacities, non-finite or non-positive durations, undocumented sentinel values, malformed event lists, and unknown keys before those values reach live timing or capacity arithmetic.
Cloud filesystems
Built-in S3 and GCS disks now pool SDK clients rather than complete filesystem adapters. Client construction input is selected explicitly and used both for fingerprinting and for constructing the client. Bucket, root, visibility, prefix, read-only, and response behavior remain per-disk adapter state and do not split a pool unnecessarily.
This means disks using the same credentials, region, endpoint, and client options can share connection resources while targeting different buckets or prefixes. Different credentials or client configuration produce different pools automatically. Repeated
Storage::build()calls with equivalent configurations converge safely without a caller-provided identity key.Every operation builds a cheap adapter stack around the borrowed client. Callback state is applied to that stack, so temporary URL and serving callbacks cannot leak between disks. Raw internals are available only through borrow-scoped
withClient(),withAdapter(), andwithDriver()callbacks.The filesystem response path was also made range-aware and lease-safe. It now validates byte-range syntax, handles open-ended and suffix ranges correctly, caps emitted bytes, preserves a body containing
"0", fails on read errors instead of spinning or truncating silently, and closes streams under both successful and failed output.S3 ranged reads preserve sibling
@httpoptions, while GCS now follows the common throw/null behavior and shared Flysystem wrapping path. Direct filesystem paths are normalized before prefixing, and temporary URL callback registration supports static, first-class, and bound closure forms.Dynamic scoped filesystems
ScopedFilesystemProxyandScopedCloudFilesystemProxyadd a strict dynamic-prefix boundary around an existing disk. The prefix resolver runs once per path operation, which allows request-scoped user, workspace, project, or sandbox prefixes without mutating manager configuration or process-global state.The decorators fail closed when the resolved prefix is empty unless root passthrough was explicitly enabled. They normalize prefixes, reject traversal and control characters, validate the complete
putFileAs()destination before performing I/O, strip returned paths defensively, and reject unknown forwarding that could bypass the scope.Configuration and capability inspection methods that do not cross a path boundary pass through without requiring a request context.
Consumer migrations
Mail
Named poolable mailers retain their existing default pooling behavior.
MailManager::build()is direct by default and can opt into pooling withpool: true, an empty array, or a pool options array.Mail pool identity is derived from the complete transport construction input, including service credential fallbacks. Composite failover and round-robin transports fingerprint their recursively resolved children, so rotating a child credential produces a new pool. Nested composite transports remain direct within the pooled outer transport.
This supports dynamic provider credentials and subaccounts while retaining connection reuse, configured capacity limits, and idle eviction. The same work fixes round-robin
retry_afterselection and global addresses without a display name.Queues
Pooled Beanstalkd and SQS jobs retain a lease on the queue backend after
pop(). The lease is released only after a successful terminal backend operation; a failed backend operation discards the connection so a potentially desynchronized client is never reused.Connection names are proxy state reapplied on every borrow, allowing equivalent logical connections to converge without leaking names. Job connection and queue names are total strings throughout the hierarchy, including framework fakes and synchronous jobs. Pool configuration metadata is no longer passed to connectors as resource construction input.
Broadcasting
Broadcaster pool identity is based on resource construction config rather than the logical connection name. Authenticated-user resolver callbacks are written on every borrow, including
null, preventing callback state from leaking between proxies sharing a pool. Custom contract-only broadcasters remain usable unless an operation specifically requires a Hypervel base-broadcaster capability.Reverb continues to resolve directly through its concurrency-safe shared SDK client. Existing Pusher and Ably pooling behavior is unchanged by this PR.
Notifications
Notification manager pooling has been removed. The Slack router is stateless and container-shared; the actual channel is resolved during send. Pooling that router managed no connection or mutable resource and could return the same auto-singleton object for multiple pool slots. Direct resolution preserves behavior and removes the ownership violation.
Custom notification channels that genuinely own reusable resources can use the general object-pool primitives within the channel implementation.
HTTP
HTTP client object pooling has been replaced with handler-level reuse. A Guzzle client is a stateless option container, while the low-level handler owns reusable cURL handles and keep-alive state.
Named connections now retain one low-level transport handler and immutable option presets. Every pending request creates a fresh middleware stack around that handler and owns its own cookie jar. This preserves transport reuse while preventing the first request's middleware or cookies from leaking into later requests.
Option layers have explicit precedence from factory globals through connection presets, per-call overrides, fluent request options, and explicit client or handler overrides. Reserved settings such as
handler,cookies, and obsoletepooloptions are rejected at the boundary that owns them.Sentry
Sentry's standalone transport pool adopts the rebuilt ownership contract and exposes only options it can enforce without managed maintenance. A transport that throws during send is discarded rather than returned to the next borrower as healthy.
API and configuration notes
PoolOptionswithmin_retained_objects,max_objects,wait_timeout,max_lifetime,max_idle_time, andidle_ttl.pool.namefor a readable explicit identity andpool.fingerprintto declare construction equivalence for configurations containing non-canonicalizable objects or callables.__call()behavior is intentionally removed where an unknown return value could outlive the borrow.Hypervel 0.4 is unreleased, so this PR favors a coherent final architecture over compatibility shims for the previous internal pooling model.
Performance characteristics
The new ownership checks are constant-time object identity lookups on pool boundaries. Pool definitions and fingerprints are computed when proxies are constructed, not on every resource operation.
Cloud filesystem pooling now retains only the expensive SDK clients. Bucket and prefix adapter stacks are rebuilt per operation; they are small, stateless wrappers relative to the client and keep disk-specific state out of the shared pool. Equivalent dynamic disks therefore reuse connection resources without retaining a complete driver pool for every bucket or prefix.
HTTP retains connection reuse without borrowing clients from an object pool. Fresh middleware stacks and cookie jars isolate request state while the shared transport handler continues to own reusable connections and concurrent transfer machinery.
Maintenance is bounded by the number of idle objects currently present and no longer keeps resources alive by updating activity clocks during inspection.
Testing
Coverage includes pool identity and fingerprint canonicalization, ownership violations, concurrent yielding factories, cross-mode release and borrow behavior, waiter wakeups, terminal closure, late releases, idle trimming and eviction, cleanup failure reporting, and duration overflow boundaries.
Consumer tests cover cloud client convergence and separation, dynamic credentials and buckets, leased streams and streamed responses, range handling, scoped-path containment, on-demand mail pooling, composite transport fingerprints, queue job terminal ordering, failed-backend discard behavior, broadcaster callback isolation, direct notification routing, HTTP middleware and cookie isolation, handler reuse, and database and Redis teardown under coroutine execution.
The complete formatting, static analysis, parallel test, secondary package, and package dogfood gates pass.
Summary by CodeRabbit
New Features
close/closed checks) and safer lease cleanup.Bug Fixes
Documentation
min_retained_objects, plus idle trimming controls).