Skip to content

Commit f7e745e

Browse files
[HttpClient] fix monitoring timeouts when other streams are active
1 parent d850d6e commit f7e745e

File tree

6 files changed

+28
-24
lines changed

6 files changed

+28
-24
lines changed

Internal/NativeClientState.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ final class NativeClientState extends ClientState
2828
public $responseCount = 0;
2929
/** @var string[] */
3030
public $dnsCache = [];
31-
/** @var resource[] */
32-
public $handles = [];
3331
/** @var bool */
3432
public $sleep = false;
3533

Response/NativeResponse.php

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,6 @@ private static function schedule(self $response, array &$runningResponses): void
220220
*/
221221
private static function perform(ClientState $multi, array &$responses = null): void
222222
{
223-
// List of native handles for stream_select()
224-
if (null !== $responses) {
225-
$multi->handles = [];
226-
}
227-
228223
foreach ($multi->openHandles as $i => [$h, $buffer, $onProgress]) {
229224
$hasActivity = false;
230225
$remaining = &$multi->openHandles[$i][3];
@@ -291,8 +286,6 @@ private static function perform(ClientState $multi, array &$responses = null): v
291286
$multi->handlesActivity[$i][] = $e;
292287
unset($multi->openHandles[$i]);
293288
$multi->sleep = false;
294-
} elseif (null !== $responses) {
295-
$multi->handles[] = $h;
296289
}
297290
}
298291

@@ -307,7 +300,7 @@ private static function perform(ClientState $multi, array &$responses = null): v
307300
}
308301
}
309302

310-
if (\count($multi->handles) >= $multi->maxHostConnections) {
303+
if (\count($multi->openHandles) >= $multi->maxHostConnections) {
311304
return;
312305
}
313306

@@ -318,10 +311,6 @@ private static function perform(ClientState $multi, array &$responses = null): v
318311
$multi->sleep = false;
319312
self::perform($multi);
320313

321-
if (null !== $response->handle) {
322-
$multi->handles[] = $response->handle;
323-
}
324-
325314
break;
326315
}
327316
}
@@ -335,7 +324,8 @@ private static function perform(ClientState $multi, array &$responses = null): v
335324
private static function select(ClientState $multi, float $timeout): int
336325
{
337326
$_ = [];
327+
$handles = array_column($multi->openHandles, 0);
338328

339-
return (!$multi->sleep = !$multi->sleep) ? -1 : stream_select($multi->handles, $_, $_, (int) $timeout, (int) (1E6 * ($timeout - (int) $timeout)));
329+
return (!$multi->sleep = !$multi->sleep) ? -1 : stream_select($handles, $_, $_, (int) $timeout, (int) (1E6 * ($timeout - (int) $timeout)));
340330
}
341331
}

Response/ResponseTrait.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ public static function stream(iterable $responses, float $timeout = null): \Gene
316316
}
317317

318318
$lastActivity = microtime(true);
319-
$isTimeout = false;
319+
$enlapsedTimeout = 0;
320320

321321
while (true) {
322322
$hasActivity = false;
@@ -338,15 +338,15 @@ public static function stream(iterable $responses, float $timeout = null): \Gene
338338
} elseif (!isset($multi->openHandles[$j])) {
339339
unset($responses[$j]);
340340
continue;
341-
} elseif ($isTimeout) {
341+
} elseif ($enlapsedTimeout >= $timeoutMax) {
342342
$multi->handlesActivity[$j] = [new ErrorChunk($response->offset, sprintf('Idle timeout reached for "%s".', $response->getInfo('url')))];
343343
} else {
344344
continue;
345345
}
346346

347347
while ($multi->handlesActivity[$j] ?? false) {
348348
$hasActivity = true;
349-
$isTimeout = false;
349+
$enlapsedTimeout = 0;
350350

351351
if (\is_string($chunk = array_shift($multi->handlesActivity[$j]))) {
352352
if (null !== $response->inflate && false === $chunk = @inflate_add($response->inflate, $chunk)) {
@@ -379,7 +379,7 @@ public static function stream(iterable $responses, float $timeout = null): \Gene
379379
}
380380
} elseif ($chunk instanceof ErrorChunk) {
381381
unset($responses[$j]);
382-
$isTimeout = true;
382+
$enlapsedTimeout = $timeoutMax;
383383
} elseif ($chunk instanceof FirstChunk) {
384384
if ($response->logger) {
385385
$info = $response->getInfo();
@@ -447,10 +447,11 @@ public static function stream(iterable $responses, float $timeout = null): \Gene
447447
continue;
448448
}
449449

450-
switch (self::select($multi, $timeoutMin)) {
451-
case -1: usleep(min(500, 1E6 * $timeoutMin)); break;
452-
case 0: $isTimeout = microtime(true) - $lastActivity > $timeoutMax; break;
450+
if (-1 === self::select($multi, min($timeoutMin, $timeoutMax - $enlapsedTimeout))) {
451+
usleep(min(500, 1E6 * $timeoutMin));
453452
}
453+
454+
$enlapsedTimeout = microtime(true) - $lastActivity;
454455
}
455456
}
456457
}

Tests/CurlHttpClientTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ public function testHttp2PushVulcainWithUnusedResponse()
112112
$this->assertSame($expected, $logger->logs);
113113
}
114114

115+
public function testTimeoutIsNotAFatalError()
116+
{
117+
if ('\\' === \DIRECTORY_SEPARATOR) {
118+
$this->markTestSkipped('Too transient on Windows');
119+
}
120+
121+
parent::testTimeoutIsNotAFatalError();
122+
}
123+
115124
private function getVulcainClient(): CurlHttpClient
116125
{
117126
if (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304) {

Tests/HttpClientTestCase.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,17 @@ public function testToStream404()
7272
$this->assertSame($response, stream_get_meta_data($stream)['wrapper_data']->getResponse());
7373
$this->assertSame(404, $response->getStatusCode());
7474

75-
$this->expectException(ClientException::class);
7675
$response = $client->request('GET', 'http://localhost:8057/404');
77-
$stream = $response->toStream();
76+
$this->expectException(ClientException::class);
77+
$response->toStream();
7878
}
7979

8080
public function testNonBlockingStream()
8181
{
8282
$client = $this->getHttpClient(__FUNCTION__);
8383
$response = $client->request('GET', 'http://localhost:8057/timeout-body');
8484
$stream = $response->toStream();
85+
usleep(10000);
8586

8687
$this->assertTrue(stream_set_blocking($stream, false));
8788
$this->assertSame('<1>', fread($stream, 8192));
@@ -99,6 +100,7 @@ public function testTimeoutIsNotAFatalError()
99100
$response = $client->request('GET', 'http://localhost:8057/timeout-body', [
100101
'timeout' => 0.25,
101102
]);
103+
$this->assertSame(200, $response->getStatusCode());
102104

103105
try {
104106
$response->getContent();

Tests/MockHttpClientTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ protected function getHttpClient(string $testCase): HttpClientInterface
6969
$this->markTestSkipped("MockHttpClient doesn't unzip");
7070
break;
7171

72+
case 'testTimeoutWithActiveConcurrentStream':
73+
$this->markTestSkipped('Real transport required');
74+
break;
75+
7276
case 'testDestruct':
7377
$this->markTestSkipped("MockHttpClient doesn't timeout on destruct");
7478
break;

0 commit comments

Comments
 (0)