[HttpClient] fix resetting DNS/etc when calling CurlHttpClient::reset()

This commit is contained in:
Nicolas Grekas 2022-01-13 16:11:29 +01:00
parent 9f5238d4f6
commit d4266464fe
4 changed files with 52 additions and 73 deletions

View File

@ -168,7 +168,6 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
if ($resolve && 0x072A00 > CurlClientState::$curlVersion['version_number']) {
// DNS cache removals require curl 7.42 or higher
// On lower versions, we have to create a new multi handle
$this->multi->reset();
}
@ -280,6 +279,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
if (!$pushedResponse) {
$ch = curl_init();
$this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, $url));
$curlopts += [\CURLOPT_SHARE => $this->multi->share];
}
foreach ($curlopts as $opt => $value) {
@ -306,9 +306,9 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of CurlResponse objects, "%s" given.', __METHOD__, \is_object($responses) ? \get_class($responses) : \gettype($responses)));
}
if (\is_resource($mh = $this->multi->handles[0] ?? null) || $mh instanceof \CurlMultiHandle) {
if (\is_resource($this->multi->handle) || $this->multi->handle instanceof \CurlMultiHandle) {
$active = 0;
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($mh, $active)) {
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->multi->handle, $active)) {
}
}

View File

@ -23,8 +23,10 @@ use Symfony\Component\HttpClient\Response\CurlResponse;
*/
final class CurlClientState extends ClientState
{
/** @var array<\CurlMultiHandle|resource> */
public $handles = [];
/** @var \CurlMultiHandle|resource */
public $handle;
/** @var \CurlShareHandle|resource */
public $share;
/** @var PushedResponse[] */
public $pushedResponses = [];
/** @var DnsCache */
@ -34,27 +36,23 @@ final class CurlClientState extends ClientState
public static $curlVersion;
private $maxHostConnections;
private $maxPendingPushes;
public function __construct(int $maxHostConnections, int $maxPendingPushes)
{
self::$curlVersion = self::$curlVersion ?? curl_version();
array_unshift($this->handles, $mh = curl_multi_init());
$this->handle = curl_multi_init();
$this->dnsCache = new DnsCache();
$this->maxHostConnections = $maxHostConnections;
$this->maxPendingPushes = $maxPendingPushes;
$this->reset();
// Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order
if (\defined('CURLPIPE_MULTIPLEX')) {
curl_multi_setopt($mh, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
curl_multi_setopt($this->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
}
if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS')) {
$maxHostConnections = curl_multi_setopt($mh, \CURLMOPT_MAX_HOST_CONNECTIONS, 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX) ? 0 : $maxHostConnections;
$maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX) ? 0 : $maxHostConnections;
}
if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
curl_multi_setopt($mh, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
curl_multi_setopt($this->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
}
// Skip configuring HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/77535
@ -67,17 +65,8 @@ final class CurlClientState extends ClientState
return;
}
// Clone to prevent a circular reference
$multi = clone $this;
$multi->handles = [$mh];
$multi->pushedResponses = &$this->pushedResponses;
$multi->logger = &$this->logger;
$multi->handlesActivity = &$this->handlesActivity;
$multi->openHandles = &$this->openHandles;
$multi->lastTimeout = &$this->lastTimeout;
curl_multi_setopt($mh, \CURLMOPT_PUSHFUNCTION, static function ($parent, $pushed, array $requestHeaders) use ($multi, $maxPendingPushes) {
return $multi->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes);
curl_multi_setopt($this->handle, \CURLMOPT_PUSHFUNCTION, function ($parent, $pushed, array $requestHeaders) use ($maxPendingPushes) {
return $this->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes);
});
}
@ -85,10 +74,7 @@ final class CurlClientState extends ClientState
{
foreach ($this->pushedResponses as $url => $response) {
$this->logger && $this->logger->debug(sprintf('Unused pushed response: "%s"', $url));
foreach ($this->handles as $mh) {
curl_multi_remove_handle($mh, $response->handle);
}
curl_multi_remove_handle($this->handle, $response->handle);
curl_close($response->handle);
}
@ -96,11 +82,14 @@ final class CurlClientState extends ClientState
$this->dnsCache->evictions = $this->dnsCache->evictions ?: $this->dnsCache->removals;
$this->dnsCache->removals = $this->dnsCache->hostnames = [];
if (\defined('CURLMOPT_PUSHFUNCTION')) {
curl_multi_setopt($this->handles[0], \CURLMOPT_PUSHFUNCTION, null);
}
$this->share = curl_share_init();
$this->__construct($this->maxHostConnections, $this->maxPendingPushes);
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS);
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION);
if (\defined('CURL_LOCK_DATA_CONNECT')) {
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT);
}
}
private function handlePush($parent, $pushed, array $requestHeaders, int $maxPendingPushes): int

View File

@ -150,7 +150,7 @@ final class CurlResponse implements ResponseInterface
// Schedule the request in a non-blocking way
$multi->lastTimeout = null;
$multi->openHandles[$id] = [$ch, $options];
curl_multi_add_handle($multi->handles[0], $ch);
curl_multi_add_handle($multi->handle, $ch);
$this->canary = new Canary(static function () use ($ch, $multi, $id) {
unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
@ -160,9 +160,7 @@ final class CurlResponse implements ResponseInterface
return;
}
foreach ($multi->handles as $mh) {
curl_multi_remove_handle($mh, $ch);
}
curl_multi_remove_handle($multi->handle, $ch);
curl_setopt_array($ch, [
\CURLOPT_NOPROGRESS => true,
\CURLOPT_PROGRESSFUNCTION => null,
@ -244,7 +242,7 @@ final class CurlResponse implements ResponseInterface
*/
private static function schedule(self $response, array &$runningResponses): void
{
if (isset($runningResponses[$i = (int) $response->multi->handles[0]])) {
if (isset($runningResponses[$i = (int) $response->multi->handle])) {
$runningResponses[$i][1][$response->id] = $response;
} else {
$runningResponses[$i] = [$response->multi, [$response->id => $response]];
@ -276,47 +274,39 @@ final class CurlResponse implements ResponseInterface
try {
self::$performing = true;
$active = 0;
while (\CURLM_CALL_MULTI_PERFORM === ($err = curl_multi_exec($multi->handle, $active))) {
}
foreach ($multi->handles as $i => $mh) {
$active = 0;
while (\CURLM_CALL_MULTI_PERFORM === ($err = curl_multi_exec($mh, $active))) {
if (\CURLM_OK !== $err) {
throw new TransportException(curl_multi_strerror($err));
}
while ($info = curl_multi_info_read($multi->handle)) {
if (\CURLMSG_DONE !== $info['msg']) {
continue;
}
$result = $info['result'];
$id = (int) $ch = $info['handle'];
$waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
if (\CURLM_OK !== $err) {
throw new TransportException(curl_multi_strerror($err));
}
if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /*CURLE_HTTP2*/ 16, /*CURLE_HTTP2_STREAM*/ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
curl_multi_remove_handle($multi->handle, $ch);
$waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter
curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
curl_setopt($ch, \CURLOPT_FORBID_REUSE, true);
while ($info = curl_multi_info_read($mh)) {
if (\CURLMSG_DONE !== $info['msg']) {
if (0 === curl_multi_add_handle($multi->handle, $ch)) {
continue;
}
$result = $info['result'];
$id = (int) $ch = $info['handle'];
$waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /*CURLE_HTTP2*/ 16, /*CURLE_HTTP2_STREAM*/ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
curl_multi_remove_handle($mh, $ch);
$waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter
curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
curl_setopt($ch, \CURLOPT_FORBID_REUSE, true);
if (0 === curl_multi_add_handle($mh, $ch)) {
continue;
}
}
if (\CURLE_RECV_ERROR === $result && 'H' === $waitFor[0] && 400 <= ($responses[(int) $ch]->info['http_code'] ?? 0)) {
$multi->handlesActivity[$id][] = new FirstChunk();
}
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) ? null : new TransportException(sprintf('%s for "%s".', curl_strerror($result), curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
}
if (!$active && 0 < $i) {
curl_multi_close($mh);
unset($multi->handles[$i]);
if (\CURLE_RECV_ERROR === $result && 'H' === $waitFor[0] && 400 <= ($responses[(int) $ch]->info['http_code'] ?? 0)) {
$multi->handlesActivity[$id][] = new FirstChunk();
}
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) ? null : new TransportException(sprintf('%s for "%s".', curl_strerror($result), curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
}
} finally {
self::$performing = false;
@ -335,7 +325,7 @@ final class CurlResponse implements ResponseInterface
$timeout = min($timeout, 0.01);
}
return curl_multi_select($multi->handles[array_key_last($multi->handles)], $timeout);
return curl_multi_select($multi->handle, $timeout);
}
/**

View File

@ -143,9 +143,9 @@ class CurlHttpClientTest extends HttpClientTestCase
$r = new \ReflectionProperty($httpClient, 'multi');
$r->setAccessible(true);
$clientState = $r->getValue($httpClient);
$initialHandleId = (int) $clientState->handles[0];
$initialShareId = $clientState->share;
$httpClient->reset();
self::assertNotSame($initialHandleId, (int) $clientState->handles[0]);
self::assertNotSame($initialShareId, $clientState->share);
}
public function testProcessAfterReset()