Repository navigation
Release idle database sessions during external waits and improve HTTP reuse - #669
Conversation
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.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@cubic-dev-ai review |
@binaryfire I have started the AI code review. It will take a few minutes to complete. |
[High impact] Adds database connection release at multiple framework boundaries. The public constant compatibility break still needs to be fixed before merging. SummaryThis 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:
No new findings were accepted. Reviews (2) · Last reviewed commit: "Record framework connection reuse and wa..." · Reviewed by Greptile |
There was a problem hiding this comment.
8 issues found across 61 files
Confidence score: 2/5
HandlerFactory.phpsends synchronous requests through a separateCurlHandler, so the configured connection cap does not apply to them. Keep$upstreamas the synchronous handler when either cap is set.- Database sessions can remain borrowed after process output callbacks in
PendingProcess.phpandInvokedProcess.php, or when a database-backed lock times out inLock.php. Release idle sessions after callbacks and on the timeout exit. - The new tests do not reach their intended paths:
ScoutServiceProviderTest.phpcallsParallel::wait()outside a coroutine, andLimiterConnectionReleaseTest.phptimes 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_HANDLESAPI. Keep a compatibility alias onFactory.
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
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.
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.
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
CurlStreamingBodydrains 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.
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:
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.
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.
Giving every caller a database slot exposes the HTTP reuse benefit:
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
Mailerreleases idle sessions after message preparation andMessageSendinglisteners, 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:
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.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.
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.
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.
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.
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.
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/PooledConnectionscontains the reusable harness and local HTTPS/SMTP fixtures. Its README explains workloads, service setup, repeated comparisons and measurement limits. The existingtests/Benchmarks/HttpTransportharness 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.retryhelper.Factoryinto the newHandlerFactory(HandlerFactory.php), which Scout, Socialite, Slack, and broadcasting now use to install theReleaseIdleDatabaseConnectionsmiddleware.CurlStreamingBody::readto preserve buffered bytes beyond the requested read length;eofstays false until retained bytes are consumed (CurlStreamingBody.php).tests/Benchmarks/PooledConnectionsplus new test suites covering release boundaries, and updates database.md documentation.HandlerFactory::createnow throwsInvalidArgumentExceptionformax_host_connections/max_total_connections, whileBroadcastManager'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.