Skip to content

Commit 469bbb3

Browse files
committed
fix(async): MultiRunner burn CPU
Fixes #55.
1 parent 3b201af commit 469bbb3

File tree

4 files changed

+117
-5
lines changed

4 files changed

+117
-5
lines changed

composer.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
],
1717
"prefer-stable": true,
1818
"minimum-stability": "dev",
19+
"config": {
20+
"secure-http": true
21+
},
1922
"require": {
2023
"php": "^7.1",
2124
"ext-curl": "*",
@@ -30,7 +33,8 @@
3033
"guzzlehttp/psr7": "^1.0",
3134
"php-http/client-integration-tests": "^2.0",
3235
"phpunit/phpunit": "^7.5",
33-
"zendframework/zend-diactoros": "^2.0"
36+
"zendframework/zend-diactoros": "^2.0",
37+
"donatj/mock-webserver": "^2.0"
3438
},
3539
"autoload": {
3640
"psr-4": {

phpunit.xml.dist

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
<directory>tests/Unit</directory>
2424
</testsuite>
2525

26+
<testsuite name="Integration">
27+
<directory>tests/Integration</directory>
28+
</testsuite>
29+
2630
<testsuite name="Functional">
2731
<directory>tests/Functional</directory>
2832
<!-- Exclude till https://github.com/php-http/message/issues/105 resolved. -->

src/MultiRunner.php

+17-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
*/
1515
class MultiRunner
1616
{
17+
/**
18+
* Timeout for curl_multi_select in seconds.
19+
*/
20+
private const SELECT_TIMEOUT = 0.1;
21+
1722
/**
1823
* cURL multi handle.
1924
*
@@ -84,8 +89,16 @@ public function remove(PromiseCore $core): void
8489
public function wait(PromiseCore $targetCore = null): void
8590
{
8691
do {
87-
$status = curl_multi_exec($this->multiHandle, $active);
88-
curl_multi_select($this->multiHandle, 0.1);
92+
if (curl_multi_select($this->multiHandle, self::SELECT_TIMEOUT) === -1) {
93+
// See https://bugs.php.net/bug.php?id=61141
94+
usleep(250);
95+
}
96+
97+
do {
98+
$status = curl_multi_exec($this->multiHandle, $active);
99+
// TODO CURLM_CALL_MULTI_PERFORM never returned since cURL 7.20.
100+
} while ($status === CURLM_CALL_MULTI_PERFORM);
101+
89102
$info = curl_multi_info_read($this->multiHandle);
90103
if ($info !== false) {
91104
$core = $this->findCoreByHandle($info['handle']);
@@ -96,7 +109,7 @@ public function wait(PromiseCore $targetCore = null): void
96109
continue;
97110
}
98111

99-
if ($info['result'] === CURLE_OK) {
112+
if ($info['result'] === CURLM_OK) {
100113
$core->fulfill();
101114
} else {
102115
$error = curl_error($core->getHandle());
@@ -109,7 +122,7 @@ public function wait(PromiseCore $targetCore = null): void
109122
return;
110123
}
111124
}
112-
} while ($status === CURLM_CALL_MULTI_PERFORM || $active);
125+
} while ($active);
113126
}
114127

115128
/**

tests/Integration/MultiRunnerTest.php

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Http\Client\Curl\Tests\Integration;
6+
7+
use donatj\MockWebServer\MockWebServer;
8+
use Http\Client\Curl\MultiRunner;
9+
use Http\Client\Curl\PromiseCore;
10+
use PHPUnit\Framework\TestCase;
11+
12+
/**
13+
* Tests for Http\Client\Curl\MultiRunner.
14+
*
15+
* @covers \Http\Client\Curl\MultiRunner
16+
*/
17+
class MultiRunnerTest extends TestCase
18+
{
19+
/**
20+
* Test HTTP server.
21+
*
22+
* @var MockWebServer
23+
*/
24+
private static $server;
25+
26+
/**
27+
* Prepare environment for all tests.
28+
*/
29+
public static function setUpBeforeClass(): void
30+
{
31+
parent::setUpBeforeClass();
32+
33+
self::$server = new MockWebServer();
34+
self::$server->start();
35+
}
36+
37+
/**
38+
* Cleanup environment after all tests.
39+
*/
40+
public static function tearDownAfterClass(): void
41+
{
42+
self::$server->stop();
43+
44+
parent::tearDownAfterClass();
45+
}
46+
47+
public function testWait(): void
48+
{
49+
$runner = new MultiRunner();
50+
51+
$handle = $this->createCurlHandle('/');
52+
$core1 = $this->createConfiguredMock(PromiseCore::class, ['getHandle' => $handle]);
53+
$core1
54+
->expects(self::once())
55+
->method('fulfill');
56+
57+
$handle = $this->createCurlHandle('/');
58+
$core2 = $this->createConfiguredMock(PromiseCore::class, ['getHandle' => $handle]);
59+
$core2
60+
->expects(self::once())
61+
->method('fulfill');
62+
63+
$runner->add($core1);
64+
$runner->add($core2);
65+
66+
$runner->wait($core1);
67+
$runner->wait($core2);
68+
}
69+
70+
/**
71+
* Create cURL handle with given parameters.
72+
*
73+
* @param string $url Request URL relative to server root.
74+
*
75+
* @return resource
76+
*/
77+
private function createCurlHandle(string $url)
78+
{
79+
$handle = curl_init();
80+
self::assertNotFalse($handle);
81+
82+
curl_setopt_array(
83+
$handle,
84+
[
85+
CURLOPT_URL => self::$server->getServerRoot() . $url
86+
]
87+
);
88+
89+
return $handle;
90+
}
91+
}

0 commit comments

Comments
 (0)