Skip to content

Commit 72ddbd1

Browse files
committed
Merge 0.4 framework performance improvements into feature/ai
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.
2 parents b15a346 + f954dda commit 72ddbd1

66 files changed

Lines changed: 2895 additions & 108 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ Decide where state lives before writing code:
386386

387387
Classes that use static caching need a `flushState()` method for test cleanup — see "Static state and test cleanup" under Writing Tests.
388388

389+
- **Release idle pooled connections before unrelated long waits** — Holding connections while waiting for external services or child coroutines can exhaust pools and limit concurrency. Release them at the appropriate shared boundary, preserving active operations and session ownership; consider reacquisition costs rather than adding release calls indiscriminately. For database sessions, reuse `ConnectionResolver::releaseIdleConnections()`, which preserves transactions and explicit pins. See `Mailer::sendSymfonyMessage()` and `ConcurrentImportRunner::wait()` for examples.
389390
- **Treat coroutine cancellation separately from errors** — Code that owns coroutine cancellation must pass `CanceledException` through unchanged and clean up the children or resources it owns. Add this only where cancellation can actually happen and mishandling it causes a real bug—not to every `catch (Throwable)`.
390391
- **Contain cancellation in direct engine coroutines** — A callback passed directly to `Engine\Coroutine` must catch cancellation after cleanup so it cannot terminate the worker. The higher-level `Coroutine` class already provides this protection.
391392

‎docs/todo.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
## HTTP Client
4949

5050
- Before the Hypervel 0.4 release, require Guzzle 8 across the root and split packages, remove Guzzle 7 compatibility branches and its CI job, and update the related Promises/PSR-7 constraints and documentation. Recheck consumer compatibility and retain applicable tests on Guzzle 8.
51-
- If [Guzzle #3935](https://github.com/guzzle/guzzle/pull/3935) merges, once the minimum supported Guzzle versions include it, pass `max_idle_handles` directly to `Utils::chooseHandler()` and remove Hypervel's custom idle-handle composition, preserving the connection option and default of 256.
51+
- If [Guzzle #3935](https://github.com/guzzle/guzzle/pull/3935) merges, once the minimum supported Guzzle versions include it, pass `max_idle_handles` directly to `Utils::chooseHandler()` and replace the custom composition in `Http\Client\Guzzle\HandlerFactory`, preserving the connection option and default of 256 for the HTTP client, Pusher and Scout SDK clients.
5252
- Recheck streaming performance once a build containing the Swoole native `curl_multi_select()` fix is available. The affected hook can consume ready response data internally and then wait for another event or its timeout before returning. Hypervel therefore sometimes delays an SSE event or completion by up to one second, even though the data has arrived. This requires no Hypervel workaround.
5353
- Run `Hypervel\Tests\Http\Client\CurlStreamingHandlerTest::testNativeWaitReportsDataArrivingBetweenPerformAndSelect` against the patched extension with its temporary version-gate skip removed locally. Verify both mid-stream readiness and completion, plus the existing quiet-provider timeout/cancellation and cleanup tests. Update the committed version gate only after identifying the actual fixed release; the current gate does not establish that a later release is fixed.
5454
- In an idle window, use `tests/Benchmarks/HttpTransport` and its README to repeat alternating paired measurements before/after high-water buffering on the same patched runtime. Keep dependencies, TLS verification, idle-retention bound, PHP/OPcache/JIT/GC settings and request concurrency identical. Cover paced events, unpaced bulk responses and slow consumers, then repeat sustained concurrent traffic. Check first-event and completion latency (including outliers), throughput, client CPU, memory, connection reuse and descriptor cleanup. Confirm the one-second stalls disappear without changing Hypervel's select interval or buffering strategy.

‎src/broadcasting/composer.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"hypervel/container": "^0.4",
3434
"hypervel/context": "^0.4",
3535
"hypervel/contracts": "^0.4",
36+
"hypervel/database": "^0.4",
3637
"hypervel/foundation": "^0.4",
3738
"hypervel/http": "^0.4",
3839
"hypervel/object-pool": "^0.4",

‎src/broadcasting/src/BroadcastManager.php‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use Hypervel\Contracts\Queue\Factory as Queue;
3737
use Hypervel\Contracts\Redis\Factory as RedisFactory;
3838
use Hypervel\Foundation\Http\Middleware\PreventRequestForgery;
39+
use Hypervel\Http\Client\Guzzle\HandlerFactory;
3940
use Hypervel\Http\Request;
4041
use Hypervel\ObjectPool\Concerns\HasPoolProxy;
4142
use Hypervel\Queue\Attributes\Connection as ConnectionAttribute;
@@ -413,22 +414,14 @@ public function pusher(array $config): Pusher
413414
);
414415

415416
if (! isset($clientOptions['handler'])) {
416-
foreach (['max_host_connections', 'max_total_connections'] as $option) {
417-
if (isset($clientOptions[$option])) {
418-
throw new InvalidArgumentException(
419-
"The [{$option}] broadcasting client option requires a shared multi-handler, which is unsafe across coroutines. Bound coroutine concurrency or use rate limiting instead."
420-
);
421-
}
422-
}
423-
424-
$handlerOptions = Arr::only($clientOptions, ['transport_sharing']);
417+
$handlerOptions = Arr::only($clientOptions, ['transport_sharing', 'max_host_connections', 'max_total_connections']);
425418
unset($clientOptions['transport_sharing']);
426419

427420
if (($clientOptions['multiplex'] ?? null) === Multiplexing::NONE) {
428421
$handlerOptions['multiplex'] = Multiplexing::NONE;
429422
}
430423

431-
$sharedHandler = Utils::chooseHandler($handlerOptions)(...);
424+
$sharedHandler = HandlerFactory::create($handlerOptions)(...);
432425

433426
// Async transfers may share a multi-handler only within their owning coroutine.
434427
$clientOptions['handler'] = HandlerStack::create(

‎src/broadcasting/src/Broadcasters/AblyBroadcaster.php‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Ably\Models\Message as AblyMessage;
1010
use Hypervel\Broadcasting\BroadcastException;
1111
use Hypervel\Contracts\Container\Container;
12+
use Hypervel\Database\ConnectionResolver;
1213
use Hypervel\Http\Request;
1314
use Hypervel\Support\Str;
1415
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@@ -126,8 +127,11 @@ public function broadcast(array $channels, string $event, array $payload = []):
126127
$channel = $this->ably->channels->get($name);
127128

128129
try {
130+
$message = $this->buildAblyMessage($event, $payload);
131+
ConnectionResolver::releaseIdleConnections();
132+
129133
/* @phpstan-ignore arguments.count, argument.type (Ably declares two @method publish() overloads and PHPStan keeps only the last; publish(...$args) accepts a Message as its sole argument.) */
130-
$channel->publish($this->buildAblyMessage($event, $payload));
134+
$channel->publish($message);
131135
} finally {
132136
if ($channel->getCipherParams() === null) {
133137
$this->ably->channels->release($name);

‎src/broadcasting/src/Broadcasters/PusherBroadcaster.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Hypervel\Broadcasting\BroadcastException;
88
use Hypervel\Contracts\Container\Container;
9+
use Hypervel\Database\ConnectionResolver;
910
use Hypervel\Http\Request;
1011
use Hypervel\Support\Arr;
1112
use Hypervel\Support\Collection;
@@ -136,6 +137,7 @@ public function broadcast(array $channels, string $event, array $payload = []):
136137

137138
try {
138139
$channels->chunk(100)->each(function ($channels) use ($event, $payload, $parameters) {
140+
ConnectionResolver::releaseIdleConnections();
139141
$this->pusher->trigger($channels->toArray(), $event, $payload, $parameters);
140142
});
141143
} catch (ApiErrorException $e) {

‎src/broadcasting/src/Mercure/Hub.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Closure;
88
use Hypervel\Contracts\ObjectPool\Factory;
99
use Hypervel\Contracts\Routing\UrlGenerator;
10+
use Hypervel\Database\ConnectionResolver;
1011
use Hypervel\ObjectPool\PoolDefinition;
1112
use Hypervel\ObjectPool\PoolProxy;
1213
use Symfony\Component\Mercure\Hub as SymfonyHub;
@@ -101,6 +102,7 @@ public function publish(Update $update): string
101102
{
102103
$url = $this->getUrl();
103104
$publicUrl = $this->getPublicUrl();
105+
ConnectionResolver::releaseIdleConnections();
104106
$lease = $this->lease();
105107

106108
try {

‎src/cache/src/Limiters/ConcurrencyLimiter.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Hypervel\Contracts\Cache\RefreshableLock;
1010
use Hypervel\Contracts\Limiters\Lease;
1111
use Hypervel\Contracts\Limiters\LimiterTimeoutException;
12+
use Hypervel\Database\ConnectionResolver;
1213
use Hypervel\Support\Sleep;
1314
use Hypervel\Support\Str;
1415
use Swoole\Coroutine\CanceledException;
@@ -64,6 +65,7 @@ public function acquire(int $timeout, int $sleep = 250): Lease
6465
throw new LimiterTimeoutException;
6566
}
6667

68+
ConnectionResolver::releaseIdleConnections();
6769
Sleep::usleep($sleep * 1000);
6870
}
6971

‎src/cache/src/Lock.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Hypervel\Contracts\Cache\Lock as LockContract;
88
use Hypervel\Contracts\Cache\LockTimeoutException;
9+
use Hypervel\Database\ConnectionResolver;
910
use Hypervel\Support\CarbonImmutable;
1011
use Hypervel\Support\InteractsWithTime;
1112
use Hypervel\Support\Sleep;
@@ -105,6 +106,7 @@ public function block(int $seconds, ?callable $callback = null): mixed
105106
throw new LockTimeoutException;
106107
}
107108

109+
ConnectionResolver::releaseIdleConnections();
108110
Sleep::usleep($this->sleepMilliseconds * 1000);
109111
}
110112

‎src/concurrency/composer.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"hypervel/context": "^0.4",
3333
"hypervel/contracts": "^0.4",
3434
"hypervel/coroutine": "^0.4",
35+
"hypervel/database": "^0.4",
3536
"hypervel/log": "^0.4",
3637
"hypervel/process": "^0.4",
3738
"hypervel/support": "^0.4",

0 commit comments

Comments
 (0)