Skip to content

Release idle database sessions during external waits and improve HTTP reuse - #669

Merged
binaryfire merged 35 commits into
0.4from
improve/framework-performance
Oct 15, 2026
Merged

binaryfire merged 35 commits into
0.4from
improve/framework-performance

Conversation

@binaryfire

@binaryfire binaryfire commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

This PR lets concurrent work make better use of database and HTTP connections. A request that has finished its database work shouldn't occupy a database slot while waiting for mail, a lock, a process, or an external API. Likewise, a burst of HTTP requests shouldn't discard most of its keep-alive connections before the next burst.

The changes reuse the existing idle-session release mechanism, preserve active queries, transactions and explicit pins, and keep public method signatures unchanged. They also reduce copying when an HTTP stream is consumed a few bytes at a time.

Measurement setup

The tables below show before/after medians from ten alternating fresh-process pairs, with warmup and two measured samples per process for pooled-wait and HTTP-buffer cases. The Pusher connection-retention comparison uses five samples per process. Both sides use the same installed dependencies, PHP 8.4.25 and Swoole 6.3.0RC1, with CLI OPcache enabled and JIT disabled. Historical source snapshots isolate the changed classes without changing the checkout between runs.

Most pool-pressure cases use eight concurrent operations and two database slots. Lock/retry cases use sixteen and four; mail uses thirty-two and four. Each operation queries before its external wait. Separate comparisons give every caller a database slot and query again afterward to expose the cost of borrowing another session. Databases and services are local; no paid provider accounts are used.

CPU means CPU in the calling PHP process, excluding external servers and child processes. Database hold time is the total time a physical session remains borrowed per operation. These are controlled comparisons, not production throughput promises. Ranges and paired comparisons were checked as well as the medians shown here.

HTTP stream reads

CurlStreamingBody drains each received buffer once and keeps an offset into that string. Small reads no longer copy the entire unread remainder each time. EOF and close account for the pending bytes.

The existing HTTP transport harness measures this change independently of any AI response handling.

Bytewise stream consumption CPU per request, before After Requests/s, before After
Small paced stream: 32 × 128 bytes, 1 ms spacing 3.158 ms 2.925 ms 55.83 55.36
Unpaced 1 MiB: 256 × 4096 bytes 394.92 ms 219.61 ms 2.53 4.54

The large case reduces CPU by about 44% in paired comparisons. It deliberately stresses bytewise reads; it isn't a claim that all HTTP requests become that much faster. Heap peaks and connection counts are effectively unchanged, and the paced case remains limited by the sender.

HTTP connection reuse

The HTTP client's bounded synchronous cURL handler is extracted into a shared factory and used by framework-created Pusher, Scout and Slack clients. It retains up to 256 idle handles. Guzzle still selects the transport, and asynchronous, streamed and fallback requests retain their existing paths. Application-supplied clients remain application-owned.

The Pusher comparison uses sixteen simultaneous streams producing 576 individual events, with batching disabled on both sides:

Pusher receiver delay Calling-process CPU, before After New warm-run HTTPS connections, before After
5 ms 379.19 ms 311.82 ms 44 0
50 ms 350.42 ms 325.26 ms 13 0

This removes repeated connection setup. It doesn't make the receiver faster: elapsed time in the 50 ms case was 1958.65 → 1989.36 ms, with overlapping ranges. There is no broadcast batching in this PR.

Broadcast database sessions

Pusher, Ably and Mercure release idle database sessions before their transport waits. Each driver does this at its owning send boundary; Redis broadcasting isn't given an extra release step.

These cases exercise actual framework dispatch with a controlled 20 ms transport wait. They isolate database occupancy, rather than measuring provider throughput or HTTP reuse.

Dispatch under database pool pressure Operations/s, before After Database hold per operation, before After
Pusher 95.83 365.01 20.79 ms 0.33 ms
Ably 95.54 362.52 20.79 ms 0.38 ms
Mercure 95.21 357.71 20.92 ms 0.39 ms

With spare capacity and a later query, throughput stays within 1% in paired comparisons. CPU per operation changes from 307 → 335 µs for Pusher, 312 → 360 µs for Ably, and 374 → 386 µs for Mercure. Database hold per operation falls from 21.35 → 1.13 ms, 21.51 → 1.16 ms, and 21.74 → 1.25 ms respectively, with one extra checkout.

Scout clients and import waits

Scout's default Algolia, Meilisearch and Typesense clients use the shared retaining handler and idle-session release middleware. A custom Typesense client is left alone. The import runner returns each child's idle session before making its concurrency slot available, and returns the parent's idle session before joining the remaining children. Existing failure and cancellation ownership is preserved.

The transport measurements use the actual Guzzle clients configured by Scout against a local HTTPS origin with a 20 ms response delay. They don't include provider serialization or full indexing. The import measurement isolates the parent joining two 20 ms tasks.

Pool-pressure case Operations/s, before After Database hold per operation, before After
Algolia transport 87.17 323.20 22.83 ms 0.61 ms
Meilisearch transport 87.42 323.01 22.79 ms 0.60 ms
Typesense transport 87.03 324.93 22.89 ms 0.61 ms
Import parent join 95.93 360.88 20.76 ms 0.37 ms

Giving every caller a database slot exposes the HTTP reuse benefit:

Transport with spare capacity and a later query CPU per operation, before After New HTTPS connections per 16 operations, before After
Algolia 1488 µs 652 µs 10 0
Meilisearch 1418 µs 686 µs 10 0
Typesense 1460 µs 655 µs 10 0

The parent-join comparison has no HTTP reuse benefit. With spare capacity its CPU changes from 364 → 402 µs per operation, while throughput remains effectively unchanged and database hold falls from 21.75 → 1.32 ms.

Mail delivery

Mailer releases idle sessions after message preparation and MessageSending listeners, immediately before transport creation/send. This covers pooled mailers, direct mailers and custom transports without separate provider wrappers. SMTP connections already reuse correctly; this PR doesn't add another retention mechanism to mail.

These measurements use real SMTP delivery to a local receiver, with controlled acknowledgement delays:

Pool-pressure case Messages/s, before After Database hold per message, before After
SQLite, 5 ms acknowledgement 580.31 1345.38 6.75 ms 2.03 ms
SQLite, 50 ms acknowledgement 76.05 284.64 52.48 ms 1.53 ms
PostgreSQL, 5 ms acknowledgement 540.19 1206.18 7.25 ms 2.45 ms

With spare PostgreSQL capacity and another query after sending, throughput was 1315.71 → 1297.25 messages/s and CPU was 706 → 715 µs/message. Database hold fell from 20.98 → 8.23 ms. Transactions retain their sessions; their measured hold time remains unchanged.

Locks, rate limits and retry delays

Idle sessions are released immediately before positive waits in cache locks/funnels, Redis funnels/throttles, Saloon delays and the shared retry() helper. An operation that doesn't wait doesn't release. Retry callbacks still run in their existing order, so sessions borrowed by a delay callback can be returned before sleeping.

Wait under database pool pressure Operations/s, before After Database hold per operation, before After
Cache lock 170.2 571.8 22.81 ms 0.96 ms
Cache funnel 169.9 574.7 23.03 ms 1.00 ms
Redis funnel 165.1 562.9 23.73 ms 1.27 ms
Redis throttle 166.3 567.9 23.69 ms 1.19 ms
Saloon request delay 178.2 582.0 22.21 ms 0.85 ms
Saloon retry delay 172.8 524.1 22.84 ms 1.91 ms
Saloon rate limit 2.00 7.96 1002.43 ms 0.60 ms
Database-backed lock 1.00 3.97 1994.16 ms 80.79 ms
HTTP retry delay 165.8 535.8 24.04 ms 2.92 ms

Cache/Redis contention and ordinary delays use 20 ms waits. Saloon rate limits allow one request per second. The database lock uses a one-second TTL whose rounded expiry can produce waits approaching two seconds. Saloon and HTTP retry use HTTP fakes to isolate their wait logic.

The database lock has the largest measured reacquisition cost: with spare capacity, CPU rises from 18.22 → 19.05 ms/operation and borrows rise from one to about eighteen because each retry queries the database. It performs the same SQL and reduces database hold from 1885.68 → 206.62 ms. HTTP retry adds about 58 µs per operation in its spare-capacity case. These changes improve shared capacity, rather than making every uncontended operation faster.

Child tasks and process waits

The coroutine concurrency driver releases idle sessions before starting its tasks. Saloon releases before admitting requests and before its final join, including when a request generator fails. Process run/wait/waitUntil and positive graceful-stop waits release at entry. Already-finished processes and zero-grace stops don't release unnecessarily.

Pool-pressure case Operations/s, before After Database hold per operation, before After
Two concurrent tasks 96.22 363.95 20.68 ms 0.32 ms
Saloon fixed requests 46.47 167.25 42.88 ms 1.10 ms
Saloon querying generator 46.99 175.55 42.43 ms 2.86 ms
Process run 26.86 58.29 73.95 ms 4.88 ms
Process start/wait 26.93 57.67 73.77 ms 6.34 ms
Process start through graceful stop 62.25 153.99 27.23 ms 0.45 ms

Tasks and Saloon requests use controlled 20 ms waits; process cases launch real local processes. Under pool pressure, process run/wait uses more calling-process CPU as more processes run concurrently. With equal capacity, CPU is effectively unchanged; process-run throughput is about 3% lower in paired comparisons. The querying Saloon generator adds about 94 µs per operation with spare capacity because its intermediate queries require fresh borrows.

The recorded stop case resolved its database connection before starting the child, which already borrowed a slot. These figures include startup and shutdown. The reusable harness now defers resolution until readiness; that correction was smoke-tested, not remeasured.

Pooled filesystem operations

Both pooled filesystem wrappers release at shared dispatch boundaries, including stream acquisition and raw-access callbacks. Local path formatting and temporary-URL capability checks are excluded. URL generation remains covered because credentials and custom URL providers can perform remote work.

Pool-pressure case, local HTTPS with 20 ms delay Operations/s, before After Database hold per operation, before After
S3 SDK read 87.98 278.20 22.51 ms 1.64 ms
Custom whole-driver pool 87.25 323.99 22.82 ms 0.60 ms

The custom-driver case exercises the shared wrapper; it isn't an FTP/SFTP benchmark. With spare capacity and another query, CPU changes from 1498 → 1516 µs for S3 and 646 → 668 µs for the custom driver, with effectively unchanged throughput.

Socialite and Slack

Socialite adds release middleware to its default Guzzle stack, preserving its transport options and coroutine-owned client lifetime. Custom handlers and supplied clients remain untouched. The default Slack webhook client also gains bounded HTTP connection reuse.

Pool-pressure case, local HTTPS with 20 ms delay Operations/s, before After Database hold per operation, before After
Socialite 77.28 236.93 25.67 ms 2.09 ms
Slack webhook 86.87 318.55 22.87 ms 0.71 ms

With spare capacity, Slack opens zero new warm-run connections instead of ten per sixteen messages; CPU falls from 1520 → 699 µs/message. Socialite's connection count remains sixteen on both sides, matching its unchanged ownership model. Its paired spare-capacity CPU change is +0.9%; it doesn't gain cross-coroutine HTTP reuse.

Queue admission and SQS

Pooled queue dispatch releases idle database sessions before borrowing a backend connection. Framework-created SQS clients also release before SDK commands, covering the client used by jobs. Job connection ownership and terminal operations are unchanged.

Real SQS SDK publication to local HTTPS with 20 ms delay Operations/s, before After Database hold per operation, before After
Database pool pressure 89.42 294.74 22.17 ms 1.36 ms
Spare capacity, then another query 301.96 302.81 24.38 ms 3.18 ms

This measures publication through the queue proxy, not worker job throughput. Spare-capacity CPU is 1210 → 1211 µs per publication.

gRPC calls and retries

Native gRPC releases before initial and retry transport startup, and before positive retry delays. It doesn't add a release call to every streamed message.

Local native HTTP/2 call under database pool pressure Operations/s, before After Database hold per operation, before After
Unary call 90.73 329.04 21.97 ms 0.41 ms
Retry after an intervening database query 91.86 306.57 21.17 ms 0.74 ms

With spare capacity and another query, CPU changes from 483 → 484 µs for unary calls and 708 → 714 µs for retries; throughput remains effectively unchanged.

The recorded retry case includes initial call creation with an already resolved database connection. The reusable harness now resolves at the intervening query. The figures describe the original complete call/retry operation, not a new measurement of that harness correction.

Behavior, coverage and reproduction

Releasing an idle session doesn't add SQL. A later query borrows again, using the existing session configuration and sticky-read behavior. Transactions, active operations and explicit pins keep their sessions. Applications relying on raw session state should use the documented pinning APIs.

Release happens at owning operation boundaries, not every yield or stream chunk. A process output callback or stream consumer that queries the database can explicitly release before its next external wait. The database documentation lists automatic boundaries and custom-transport exceptions in one place.

Regression coverage checks ordinary, transactional and pinned execution, callback ordering, failure cleanup, custom clients and connection reuse. Single-slot database/process handshakes verify that another coroutine can make progress without relying on timing thresholds. Formatting, static analysis and affected package tests pass locally.

tests/Benchmarks/PooledConnections contains the reusable harness and local HTTPS/SMTP fixtures. Its README explains workloads, service setup, repeated comparisons and measurement limits. The existing tests/Benchmarks/HttpTransport harness covers the native stream buffer separately. Historical snapshots and raw measurement output aren't shipped as framework code.

Note

Release idle database sessions at external wait boundaries across the framework

Adds idle database-session release (ConnectionResolver::releaseIdleConnections) before external waits and provider calls throughout the framework, so one pooled connection can serve concurrent coroutines. Transactions and pinned sessions keep their borrowed connection.

  • Release boundaries added for HTTP, mail, broadcasting (Pusher, Ably, Mercure), Scout concurrent imports, Saloon waits and pools, queue admission and SQS, gRPC streams and retries, cache/Redis limiters and locks, processes, filesystem pooled operations, Slack, Socialite, and the global retry helper.
  • Moves Guzzle handler construction from Factory into the new HandlerFactory (HandlerFactory.php), which Scout, Socialite, Slack, and broadcasting now use to install the ReleaseIdleDatabaseConnections middleware.
  • Fixes CurlStreamingBody::read to preserve buffered bytes beyond the requested read length; eof stays false until retained bytes are consumed (CurlStreamingBody.php).
  • Adds a pooled-connection benchmark harness under tests/Benchmarks/PooledConnections plus new test suites covering release boundaries, and updates database.md documentation.
  • Behavioral Change: HandlerFactory::create now throws InvalidArgumentException for max_host_connections/max_total_connections, while BroadcastManager's Pusher driver instead accepts those options and passes them to the handler; release failures during Saloon orchestration and Scout import waits propagate through existing failure/cancellation paths.

Macroscope summarized beab2a4.

Drain received data into a bounded pending string and advance an offset for small reads, instead of copying the remaining BufferStream contents on every read.

Keep network advancement and pause/resume tied to exhaustion of both buffers. Account for pending bytes when reporting EOF and release them when the body closes. Cover mixed read sizes, zero-length reads and receive-buffer bounds.

Validated with the HTTP suite and the existing native streaming lifecycle tests.
Extract the HTTP factory’s synchronous idle-handle retention into a Guzzle handler factory so SDK clients can reuse the same transport policy. Keep the default of 256 idle handles with the implementation that owns it.

Preserve Guzzle handler selection, transport-sharing validation, asynchronous routing and the Guzzle 7 TLS fallback. The HTTP factory continues to add native incremental streaming separately.

Validated with HTTP connection reuse, option validation and streaming tests, plus static analysis.
Move the existing pre-send database lease release into a dedicated invokable middleware. Keep PendingRequest’s protected stack-builder method as its delegate.

Other framework-created SDK clients can now return idle pooled database sessions before external waits without duplicating the release policy. Active transactions and explicitly pinned sessions retain their physical connection.

Validated with database lease lifecycle and HTTP request tests.
Build Pusher and Reverb’s shared synchronous transport with the common retaining handler factory. This avoids reconnecting most requests after a burst exceeds Guzzle’s default three idle handles.

Keep custom handlers and coroutine-owned asynchronous transports unchanged. Add a synchronized two-burst regression that checks socket reuse without timing assertions.

The regression fails with the previous handler selection and passes with the shared retaining factory. Broadcasting tests and static analysis pass.
Release idle database leases before each Pusher channel chunk, after Ably message preparation and before Mercure acquires its HTTP client. Declare the database package as a direct broadcasting dependency.

Release at the transport boundary so direct broadcaster calls and preparation callbacks are covered. Preserve transactions and pinned sessions; leave the Redis driver unchanged.

Add regressions for multi-request Pusher broadcasts, Ably preparation and Mercure client acquisition. The tests fail without these release points and pass alongside the broadcasting suite.
Construct default Algolia, Meilisearch and Typesense HTTP clients with the common retaining Guzzle handler and database-release middleware. Preserve explicitly supplied Typesense clients and the existing SDK request configuration.

Place Meilisearch retries outside lease release so each attempt returns sessions acquired by response instrumentation before waiting again. Keep the same middleware when retries are disabled.

Cover default-client lease behavior, transaction and session pins, retry release and concurrent connection reuse. The retention regression fails when the helper uses the default Guzzle handler.
Return idle database sessions when a child import operation finishes, before its concurrency slot can be reused. Keep the parent’s session while it produces chunks and release it before the final join.

Capture child cleanup failures through the existing failure reporting. If parent release is cancelled, cancel active children and rethrow without draining, matching WaitConcurrent. Join existing work before propagating ordinary parent cleanup failures.

Use synchronized regressions to cover parent ownership, child slot reuse, failed children, cancellation and ordinary cleanup errors. Database-vector admission remains with the vector engine rather than adding unused behavior to the current database engine.
Update the existing upstream max_idle_handles follow-up to name HandlerFactory and its HTTP, Pusher and Scout consumers.

When supported Guzzle versions include the native option, replace the shared composition once while retaining the current connection option and default. Keep this in the existing follow-up rather than creating separate tasks per consumer.
Mail preparation and MessageSending listeners can leave a database session
borrowed while the mailer waits for transport capacity or delivery. Return
idle sessions after envelope construction and before calling the transport,
using the same release policy as outgoing framework HTTP requests.

Keep the boundary in Mailer so named pooled transports, on-demand mailers,
queued mailables and mail notifications share it. Transactions and explicit
session pins retain their connections. No transport wrappers, special cases
for local transports, or public signature changes are needed. Declare the
database package as a direct mail dependency.

Add a regression through a named poolable transport: a sending listener
borrows a session, transport construction sees it released, and sending
preserves transaction and pinned ownership. The test fails on the original
code. Formatting, both PHPStan configurations and the affected mail and
notification suites pass.

Paired local SMTP measurements show materially shorter database holds and
higher throughput when database capacity is scarce. A query after sending
requires another pool checkout; this does not add an application SQL query.
The database guide previously described only the HTTP client, leaving users
of mail, broadcasting and Scout without the complete session-ownership
contract. List each automatic release boundary in one place, including
concurrent Scout import completion and the final wait for remaining chunks.

Name the Typesense custom-client exception and keep HTTP streaming, fake
and custom-client details explicitly scoped to HTTP. Point the Laravel
porting guide to the shared list so applications know when temporary tables,
session locks and retained raw PDOs require withPinnedSession. Active
transactions remain protected automatically.

Check the list against the actual driver, mailer and import-runner call
sites. The documentation changes pass whitespace checks and accompany the
verified framework and mail release changes.
Make resource ownership during external waits an explicit coroutine design consideration. Prefer the shared release boundary, preserve active operations and pinned sessions, and weigh reacquisition costs instead of inserting release calls everywhere.

Point to the mailer and concurrent Scout import runner as concrete examples of the existing database release policy.
Return idle sessions before sleeping between failed lock and funnel acquisition attempts. A request waiting for another lock owner no longer occupies a database pool slot throughout the backoff.

Reuse the existing session ownership checks so transactions and explicit pins remain attached. Cover repeated database-backed attempts with faked sleeps, including ordinary, transactional and pinned execution. Immediate acquisitions retain their existing path.
Return database sessions before Redis funnels and throttles sleep between acquisition attempts. Redis commands already return their own pooled connections; the remaining idle database session should not restrict unrelated requests during the wait.

Declare the database package used directly by these boundaries and verify that ordinary sessions are released while transactions and explicit pins remain owned. Keep successful acquisition and protected work unchanged.
Release idle sessions before positive request and retry delays and before waiting for rate-limit admission. Database work in request middleware no longer reserves a pool slot while the request is deliberately paused.

Use the existing resolver rather than introducing a sleep hook or changing request policies. Declare the direct database dependency and cover all three wait paths through public sends, preserving transaction and pinned-session ownership.
Return idle sessions after the retry decision and delay callbacks have finished, immediately before a positive sleep. This also covers HTTP response listeners that query the database after the transport has released its earlier session.

Keep backoff arrays, scalar and callback delays, CarbonInterval conversion, cancellation and retry exhaustion unchanged. Add a regression beside the HTTP session-release tests that covers listener and delay-callback queries in ordinary, transactional and explicitly pinned scopes.
Add cache locks and funnels, Redis limiters, Saloon delays and the retry helper to the canonical list of automatic release boundaries. Applications using session-specific state can use the adjacent pinning guidance without having to infer behavior from individual packages.

Keep the explanation centralized with database ownership documentation and the existing porting-guide link.
Return the parent coroutine’s idle database sessions before starting and joining concurrent tasks. This lets children use a bounded pool without waiting for a session held by their waiting parent, while transactions and explicit pins retain ownership.

Declare the direct database dependency and cover parent-to-child progress with a single available database connection.
Release before child admission and before the final join, including when request generation fails. Preserve the first orchestration error, join children after ordinary release failures, and cancel without draining when the parent is canceled.

Cover querying generators with one database slot and a generator that fails after its final query while a child still needs that slot.
Release sessions before real synchronous runs, asynchronous waits, and positive graceful-stop waits on running processes. Keep fake execution, immediate stops, finished-process cleanup, transactions, and pinned sessions unchanged.

Add real subprocess handshakes proving another coroutine can query a single-slot database pool during run, wait, waitUntil, and graceful shutdown. The tests require concurrent progress rather than relying on timing assertions. Declare the direct database dependency.
Describe automatic release before concurrent tasks, Saloon admission and joins, and process waits. Explain that output callbacks which query the database can explicitly release their idle session before returning to a long wait.
Release idle sessions before remote-capable operations, stream acquisition and raw accessor callbacks in both filesystem proxies. Share the method classification while preserving local path formatting and capability checks without an unnecessary release.

Keep URL operations covered because signing and custom URL providers can contact remote services. Declare the direct database dependency and cover operation, stream and accessor behavior alongside local path preservation.
Attach the shared release middleware to the default stack after Guzzle constructs the client, preserving client-level connection options.

Keep coroutine-owned client lifetime and custom handlers or explicitly supplied clients unchanged. Document why constructing a replacement handler would break supported Guzzle configuration.
Build the default webhook client with the shared bounded retaining handler and database release middleware. Release occurs after notification preparation and before transport work.

Preserve existing timeouts and contextual client replacement. The worker-owned webhook channel can reuse connections across concurrent notification bursts.
Release sessions before borrowing a pooled queue backend so publishers do not occupy database capacity while waiting for a queue connection.

Install the existing AWS initialize middleware on framework-created SQS clients, covering commands issued by queues and their jobs. Preserve backend lease ownership through terminal job operations and leave injected clients unchanged.
Return idle sessions at the shared transport-start boundary for initial and repeated attempts, and before positive retry backoff.

Keep existing cancellation and failure propagation intact without adding per-message hooks. Declare the direct database dependency used by these boundaries.
Verify default Socialite requests, Slack notification preparation, SQS commands and queue admission return idle sessions at their transport boundaries. Preserve explicit Socialite handler ownership.

Cover gRPC startup and prove another coroutine can query a single-slot database pool during retry backoff. Reuse existing transport fixtures and cancel the retry through a bounded handshake rather than waiting for its delay.
Describe automatic release for pooled filesystems, default Socialite and Slack clients, queue admission, SQS commands and gRPC attempts and retry delays.

Clarify that later stream consumption, raw callbacks and caller-owned transports may need an explicit release after performing their own database work.
Provide loopback HTTPS and SMTP origins with controlled response delays so pooled-session measurements need no external provider accounts. Expose accepted connection IDs and SMTP delivery counters to verify completed work and connection reuse.
Measure database session occupancy, checkout latency, throughput and calling-process CPU across framework waits and external transports. Use real pooled SQLite sessions, select only the requested workload, and verify operation completion and lease cleanup.

Support constrained pools, spare capacity with a later query, and transaction controls. Local fixtures and controlled waits keep comparisons reproducible without paid APIs; results distinguish connection borrows from SQL queries and measure transport reuse where observable.
Explain workload selection, local fixture setup, alternating before-and-after runs, and pool-pressure versus reacquisition comparisons. Document measurement boundaries, small-sample latency limits and the distinction between transport wiring and full provider operations.

Keep native HTTP buffer measurements in the existing HTTP transport harness and raw historical results outside the repository.
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

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

⚙️ Run configuration
  • Configuration used: Repository: hypervel/components/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0472f5bb-ae73-4b24-89a2-9fcc841af0a1

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: hypervel/components/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 55c36864-c5e2-4cf8-9a82-b6a210b9f4f4
📥 Commits

Reviewing files that changed from the base of the PR and between c5303a3 and 19a4333.

📒 Files selected for processing: 61
  • AGENTS.md
  • docs/todo.md
  • src/broadcasting/composer.json
  • src/broadcasting/src/BroadcastManager.php
  • src/broadcasting/src/Broadcasters/AblyBroadcaster.php
  • src/broadcasting/src/Broadcasters/PusherBroadcaster.php
  • src/broadcasting/src/Mercure/Hub.php
  • src/cache/src/Limiters/ConcurrencyLimiter.php
  • src/cache/src/Lock.php
  • src/concurrency/composer.json
  • src/concurrency/src/CoroutineDriver.php
  • src/docs/database.md
  • src/docs/porting-from-laravel.md
  • src/filesystem/composer.json
  • src/filesystem/src/ClientPooledFilesystem.php
  • src/filesystem/src/Concerns/InteractsWithPooledFilesystem.php
  • src/filesystem/src/FilesystemPoolProxy.php
  • src/grpc/composer.json
  • src/grpc/src/Client/Call.php
  • src/grpc/src/Client/Connection.php
  • src/http/src/Client/CurlStreamingBody.php
  • src/http/src/Client/Factory.php
  • src/http/src/Client/Guzzle/HandlerFactory.php
  • src/http/src/Client/Guzzle/ReleaseIdleDatabaseConnections.php
  • src/http/src/Client/PendingRequest.php
  • src/mail/composer.json
  • src/mail/src/Mailer.php
  • src/process/composer.json
  • src/process/src/InvokedProcess.php
  • src/process/src/PendingProcess.php
  • src/queue/src/Connectors/SqsConnector.php
  • src/queue/src/QueuePoolProxy.php
  • src/redis/composer.json
  • src/redis/src/Limiters/ConcurrencyLimiter.php
  • src/redis/src/Limiters/DurationLimiter.php
  • src/saloon/composer.json
  • src/saloon/src/Http/Pool.php
  • src/saloon/src/SaloonManager.php
  • src/scout/src/Console/ConcurrentImportRunner.php
  • src/scout/src/ScoutServiceProvider.php
  • src/slack-notification-channel/src/SlackChannelServiceProvider.php
  • src/socialite/src/AbstractProvider.php
  • src/support/src/helpers.php
  • tests/Benchmarks/PooledConnections/Fixtures/server.php
  • tests/Benchmarks/PooledConnections/Fixtures/smtp-server.php
  • tests/Benchmarks/PooledConnections/README.md
  • tests/Benchmarks/PooledConnections/benchmark.php
  • tests/Broadcasting/BroadcastConnectionReleaseTest.php
  • tests/Broadcasting/PusherClientTest.php
  • tests/Cache/LockConnectionReleaseTest.php
  • tests/Concurrency/ConcurrencyTest.php
  • tests/Database/DatabaseConnectionLeaseTest.php
  • tests/Filesystem/ClientPooledFilesystemTest.php
  • tests/Filesystem/FilesystemPoolProxyTest.php
  • tests/Http/Client/CurlStreamingHandlerTest.php
  • tests/Integration/Mail/MailManagerTest.php
  • tests/Redis/LimiterConnectionReleaseTest.php
  • tests/Saloon/Feature/PoolTest.php
  • tests/Saloon/Feature/WaitConnectionReleaseTest.php
  • tests/Scout/Feature/ConcurrentImportConnectionTest.php
  • tests/Scout/Unit/ScoutServiceProviderTest.php

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

📝 Walkthrough

The PR releases idle database sessions at external-operation and wait boundaries while preserving active transactions and pinned sessions. It centralizes Guzzle handler setup, updates streamed-body reads, and adds integration tests, documentation, and pooled-connection benchmark tooling.

Changes: Pooled connection behavior

Layer / File(s) Summary
Guzzle transport and HTTP clients
src/http/src/Client/*, src/http/src/Client/Guzzle/*, src/broadcasting/src/BroadcastManager.php, src/scout/src/ScoutServiceProvider.php, src/slack-notification-channel/src/SlackChannelServiceProvider.php, src/socialite/src/AbstractProvider.php, tests/Http/Client/*, tests/Scout/Unit/ScoutServiceProviderTest.php, tests/Broadcasting/PusherClientTest.php, docs/todo.md
HTTP clients use HandlerFactory and idle-connection middleware. Streamed-body reads retain unread bytes. Scout, Slack, Socialite, and Pusher use shared handler setup. Tests cover transport reuse and database-session release.
External-operation boundaries
src/broadcasting/*, src/filesystem/*, src/grpc/*, src/mail/*, src/process/*, src/queue/*, src/docs/*, AGENTS.md, tests/Broadcasting/BroadcastConnectionReleaseTest.php, tests/Database/DatabaseConnectionLeaseTest.php, tests/Filesystem/*, tests/Integration/Mail/MailManagerTest.php
Broadcasting, filesystem, gRPC, mail, process, and queue operations release idle database sessions at specified boundaries. Documentation describes automatic release and caller-managed cases. Tests check release behavior and preservation of transactions and pinned sessions.
Waits and concurrent work
src/cache/*, src/concurrency/*, src/redis/*, src/saloon/*, src/scout/src/Console/ConcurrentImportRunner.php, src/support/src/helpers.php, tests/Cache/*, tests/Concurrency/ConcurrencyTest.php, tests/Redis/*, tests/Saloon/Feature/*, tests/Scout/Feature/ConcurrentImportConnectionTest.php
Lock and limiter retries, coroutine tasks, Saloon request flows, retry delays, and Scout imports release idle sessions during waits or coordination. Tests cover session availability, cancellation, and orchestration behavior.
Pooled-connection benchmark
tests/Benchmarks/PooledConnections/*
Adds a benchmark CLI with workload fixtures, validation, connection and request measurements, and JSON output. The README documents running workloads and interpreting reported metrics.

Priority: ➖ Normal

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

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 19a43

This change returns idle database sessions to the pool before external waits, while keeping transactions and pinned sessions intact. It also shares HTTP connection reuse across framework clients. No concrete defect was found, and the change appears ready to merge after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 119 functions across 48 files. (13 skippe… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
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.
Title check Passed The title clearly summarizes both primary changes: releasing idle database sessions during waits and improving HTTP connection reuse.
Description check Passed The description is detailed and covers the problem, implementation scope, benchmark evidence, regression coverage, tradeoffs, and reported verification. It does not explicitly select a contribution ty…
Full details: Docstring Coverage

Explanation

Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 119 functions across 48 files. (13 skipped: 13 unsupported.)

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

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

❤️ Share

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

@binaryfire

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

@binaryfire

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 15, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@binaryfire I have started the AI code review. It will take a few minutes to complete.

Comment thread src/http/src/Client/Guzzle/HandlerFactory.php
@greptile-apps

greptile-apps Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[High impact] Adds database connection release at multiple framework boundaries.

The public constant compatibility break still needs to be fixed before merging.

Summary

This PR releases idle database sessions before external waits, shares bounded HTTP connection reuse across framework-created clients, and reduces copying during small HTTP stream reads.

Changes since the previous review:

  • Outbound sends return idle database sessions before waiting.
  • Lock and retry waits free database slots while they sleep.
  • Concurrent tasks and process waits return idle database sessions.
  • Pooled filesystem calls release database sessions before remote work.
  • Queue and gRPC calls free idle database sessions before waiting.
  • Framework HTTP clients keep more connections between bursts.
  • Small reads no longer copy the whole unread HTTP body.
  • Local benchmark files show how to measure connection waits and reuse.

No new findings were accepted.

Reviews (2) · Last reviewed commit: "Record framework connection reuse and wa..." · Reviewed by Greptile

Comment thread src/http/src/Client/Factory.php

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 61 files

Confidence score: 2/5

  • HandlerFactory.php sends synchronous requests through a separate CurlHandler, so the configured connection cap does not apply to them. Keep $upstream as the synchronous handler when either cap is set.
  • Database sessions can remain borrowed after process output callbacks in PendingProcess.php and InvokedProcess.php, or when a database-backed lock times out in Lock.php. Release idle sessions after callbacks and on the timeout exit.
  • The new tests do not reach their intended paths: ScoutServiceProviderTest.php calls Parallel::wait() outside a coroutine, and LimiterConnectionReleaseTest.php times out before its wait callback runs. Run the parallel bursts in a coroutine and set both limiter timeouts above the sleep interval.
  • Moving the constant breaks callers of the existing Factory::DEFAULT_MAX_IDLE_HANDLES API. Keep a compatibility alias on Factory.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/concurrency/src/CoroutineDriver.php">

<violation number="1" location="src/concurrency/src/CoroutineDriver.php:42">
P3: A call outside a coroutine releases its idle database lease here, then `Parallel::wait()` throws before starting any tasks. Validate the coroutine precondition before releasing so a failed call does not force later queries to reacquire a session.</violation>
</file>

<file name="tests/Scout/Unit/ScoutServiceProviderTest.php">

<violation number="1" location="tests/Scout/Unit/ScoutServiceProviderTest.php:77">
P2: These calls run from a normal PHPUnit test, but `Parallel::wait()` throws `RunningInNonCoroutineException` outside a coroutine; wrap both bursts in `Hypervel\Coroutine\run()`.</violation>
</file>

<file name="tests/Redis/LimiterConnectionReleaseTest.php">

<violation number="1" location="tests/Redis/LimiterConnectionReleaseTest.php:53">
P2: Both limiter calls use a timeout equal to the sleep interval, so the timeout guard throws on the first denied attempt before the wait callback runs. Use a timeout greater than the sleep interval in both calls so the test reaches the release behavior it intends to assert.</violation>
</file>

<file name="src/process/src/InvokedProcess.php">

<violation number="1" location="src/process/src/InvokedProcess.php:62">
P2: If an output callback queries the database, it can reacquire a session during `wait()` or `waitUntil()` and hold it for the remaining process wait. Release idle connections after each callback returns.</violation>
</file>

<file name="src/cache/src/Lock.php">

<violation number="1" location="src/cache/src/Lock.php:109">
P2: A timed-out database-backed lock throws before reaching this cleanup, leaving the final failed attempt's idle session borrowed while the caller handles the exception. Release idle connections on this exit too.</violation>
</file>

<file name="src/process/src/PendingProcess.php">

<violation number="1" location="src/process/src/PendingProcess.php:230">
P2: `Process::run()` invokes the output callback while the subprocess is still running; a callback query can reacquire a session that remains held during later waits. Release idle sessions after each callback too.</violation>
</file>

<file name="src/http/src/Client/Guzzle/HandlerFactory.php">

<violation number="1" location="src/http/src/Client/Guzzle/HandlerFactory.php:23">
P2: Moving this constant to `HandlerFactory` removes the existing public `Factory::DEFAULT_MAX_IDLE_HANDLES` API. Keep a compatibility alias on `Factory` so existing references do not fail with an undefined-class-constant error.</violation>

<violation number="2" location="src/http/src/Client/Guzzle/HandlerFactory.php:55">
P1: `Proxy::wrapSync` sends synchronous requests to the separate retaining `CurlHandler`, which receives neither connection cap. Keep `$upstream` as the synchronous handler whenever either cap is set so those requests remain subject to the configured limit.</violation>
</file>

Tip: Get a Slack DM when cubic finishes reviewing your PRs. Connect Slack

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/filesystem/composer.json
Comment thread src/http/src/Client/Guzzle/HandlerFactory.php
Comment thread src/process/src/InvokedProcess.php Outdated
Comment thread tests/Scout/Unit/ScoutServiceProviderTest.php
Comment thread tests/Redis/LimiterConnectionReleaseTest.php
Comment thread src/cache/src/Lock.php
Comment thread src/process/src/PendingProcess.php
Comment thread tests/Benchmarks/PooledConnections/benchmark.php Outdated
Comment thread src/http/src/Client/Guzzle/HandlerFactory.php
Comment thread src/concurrency/src/CoroutineDriver.php
Prevent direct HandlerFactory callers from silently bypassing Guzzle connection limits through the synchronous retaining handler. Reject those options instead of selecting a shared multi-handler that concurrent coroutines cannot safely drive.

Route default Pusher construction through the same check and preserve earlier HTTP registration validation. Cover both cap options directly and retain broadcasting validation coverage.
A graceful stop must still attempt terminal cleanup if returning an idle database session is canceled or throws. Attempt an immediate stop in that failure path and preserve the original release exception even if cleanup also fails.

Keep ordinary wait ownership unchanged. Add a regression proving cancellation identity and the zero-grace stop attempt through the real session-release path.
Resolving a database connection borrows a pool slot even before its first query. Delay resolution in process-stop and gRPC retry cases until their explicit query so startup does not accidentally retain a session.

Keep deliberate transaction ownership established before the operation and reuse the cached logical connection for subsequent queries. Smoke both affected cases with constrained capacity and a query afterward.
Preserve the isolated before-and-after stream-buffer comparison beside its reusable harness. Record measured revisions, runtime settings, workloads and CPU and throughput results without attributing AI response-lifecycle gains to the transport.

Explain the bytewise stress case and paced-stream limits so the figures remain reproducible evidence rather than general capacity claims.
Keep per-improvement before-and-after tables with the pooled connection harness, including controlled workloads, runtime settings and spare-capacity reacquisition costs. Preserve the distinction between transport reuse, database occupancy and complete application throughput.

Identify process-stop and gRPC retry figures as measurements taken before the harness resolution correction. Document their original boundaries instead of presenting smoke verification as a new benchmark run.
@binaryfire
binaryfire merged commit f954dda into 0.4 Oct 15, 2026
52 checks passed
binaryfire added a commit that referenced this pull request Oct 15, 2026
Bring in the shared HTTP handler and idle database session release improvements, process cleanup corrections, regression coverage and recorded benchmark results from PR #669.

Preserve the AI destination-policy middleware and existing uncommitted AI work. The receive-buffer source and regression test already matched the incoming framework versions. Verify the merged tree with formatting, static analysis and focused AI streaming, Bedrock HTTP and remote-file tests.
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