Merge branch '4.3' into 4.4

* 4.3:
  [HttpClient] Don't read from the network faster than the CPU can deal with
  [DI] DecoratorServicePass should keep container.service_locator on the decorated definition
This commit is contained in:
Nicolas Grekas 2020-01-06 13:57:54 +01:00
commit fb2d2577d9
8 changed files with 129 additions and 164 deletions

View File

@ -78,8 +78,18 @@ class DecoratorServicePass implements CompilerPassInterface
if (isset($decoratingDefinitions[$inner])) {
$decoratingDefinition = $decoratingDefinitions[$inner];
$definition->setTags(array_merge($decoratingDefinition->getTags(), $definition->getTags()));
$decoratingDefinition->setTags([]);
$decoratingTags = $decoratingDefinition->getTags();
$resetTags = [];
if (isset($decoratingTags['container.service_locator'])) {
// container.service_locator has special logic and it must not be transferred out to decorators
$resetTags = ['container.service_locator' => $decoratingTags['container.service_locator']];
unset($decoratingTags['container.service_locator']);
}
$definition->setTags(array_merge($decoratingTags, $definition->getTags()));
$decoratingDefinition->setTags($resetTags);
$decoratingDefinitions[$inner] = $definition;
}

View File

@ -223,6 +223,25 @@ class DecoratorServicePassTest extends TestCase
$this->assertEquals(['bar' => ['attr' => 'baz']], $container->getDefinition('deco2')->getTags());
}
public function testProcessLeavesServiceLocatorTagOnOriginalDefinition()
{
$container = new ContainerBuilder();
$container
->register('foo')
->setTags(['container.service_locator' => [0 => []], 'bar' => ['attr' => 'baz']])
;
$container
->register('baz')
->setTags(['foobar' => ['attr' => 'bar']])
->setDecoratedService('foo')
;
$this->process($container);
$this->assertEquals(['container.service_locator' => [0 => []]], $container->getDefinition('baz.inner')->getTags());
$this->assertEquals(['bar' => ['attr' => 'baz'], 'foobar' => ['attr' => 'bar']], $container->getDefinition('baz')->getTags());
}
protected function process(ContainerBuilder $container)
{
$repeatedPass = new DecoratorServicePass();

View File

@ -116,7 +116,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
$url = implode('', $url);
if (!isset($options['normalized_headers']['user-agent'])) {
$options['normalized_headers']['user-agent'][] = $options['headers'][] = 'User-Agent: Symfony HttpClient/Curl';
$options['headers'][] = 'User-Agent: Symfony HttpClient/Curl';
}
$curlopts = [
@ -217,8 +217,8 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
$curlopts[CURLOPT_NOSIGNAL] = true;
}
if (!isset($options['normalized_headers']['accept-encoding']) && CURL_VERSION_LIBZ & self::$curlVersion['features']) {
$curlopts[CURLOPT_ENCODING] = 'gzip'; // Expose only one encoding, some servers mess up when more are provided
if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
$options['headers'][] = 'Accept-Encoding: gzip'; // Expose only one encoding, some servers mess up when more are provided
}
foreach ($options['headers'] as $header) {

View File

@ -77,7 +77,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
$options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
}
if ($gzipEnabled = \extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
// gzip is the most widely available algo, no need to deal with deflate
$options['headers'][] = 'Accept-Encoding: gzip';
}
@ -227,7 +227,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
$context = stream_context_create($context, ['notification' => $notification]);
self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy, $noProxy);
return new NativeResponse($this->multi, $context, implode('', $url), $options, $gzipEnabled, $info, $resolveRedirect, $onProgress, $this->logger);
return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolveRedirect, $onProgress, $this->logger);
}
/**

View File

@ -52,6 +52,7 @@ final class CurlResponse implements ResponseInterface
$this->id = $id = (int) $ch;
$this->logger = $logger;
$this->shouldBuffer = $options['buffer'] ?? true;
$this->timeout = $options['timeout'] ?? null;
$this->info['http_method'] = $method;
$this->info['user_data'] = $options['user_data'] ?? null;
@ -65,50 +66,25 @@ final class CurlResponse implements ResponseInterface
curl_setopt($ch, CURLOPT_PRIVATE, \in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], true) && 1.0 < (float) ($options['http_version'] ?? 1.1) ? 'H2' : 'H0'); // H = headers + retry counter
}
if (null === $content = &$this->content) {
$content = null === $options || true === $options['buffer'] ? fopen('php://temp', 'w+') : (\is_resource($options['buffer']) ? $options['buffer'] : null);
} else {
// Move the pushed response to the activity list
$buffer = $options['buffer'];
if ('H' !== curl_getinfo($ch, CURLINFO_PRIVATE)[0]) {
if ($options['buffer'] instanceof \Closure) {
try {
[$content, $buffer] = [null, $content];
[$content, $buffer] = [$buffer, $options['buffer']($headers)];
} catch (\Throwable $e) {
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = $e;
[$content, $buffer] = [$buffer, false];
}
}
if (ftell($content)) {
rewind($content);
$multi->handlesActivity[$id][] = stream_get_contents($content);
}
}
if (\is_resource($buffer)) {
$content = $buffer;
} elseif (true !== $buffer) {
$content = null;
}
}
curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger, &$content): int {
return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger, $content);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int {
return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger);
});
if (null === $options) {
// Pushed response: buffer until requested
curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use (&$content): int {
return fwrite($content, $data);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
$multi->handlesActivity[$id][] = $data;
curl_pause($ch, CURLPAUSE_RECV);
return \strlen($data);
});
return;
}
$this->inflate = !isset($options['normalized_headers']['accept-encoding']);
curl_pause($ch, CURLPAUSE_CONT);
if ($onProgress = $options['on_progress']) {
$url = isset($info['url']) ? ['url' => $info['url']] : [];
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
@ -128,33 +104,16 @@ final class CurlResponse implements ResponseInterface
});
}
curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use (&$content, $multi, $id): int {
curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
$multi->handlesActivity[$id][] = $data;
return null !== $content ? fwrite($content, $data) : \strlen($data);
return \strlen($data);
});
$this->initializer = static function (self $response) {
if (null !== $response->info['error']) {
throw new TransportException($response->info['error']);
}
$waitFor = curl_getinfo($ch = $response->handle, CURLINFO_PRIVATE);
if ('H' === $waitFor[0] || 'D' === $waitFor[0]) {
try {
foreach (self::stream([$response]) as $chunk) {
if ($chunk->isFirst()) {
break;
}
}
} catch (\Throwable $e) {
// Persist timeouts thrown during initialization
$response->info['error'] = $e->getMessage();
$response->close();
throw $e;
}
}
return 'H' === $waitFor[0] || 'D' === $waitFor[0];
};
// Schedule the request in a non-blocking way
@ -241,6 +200,7 @@ final class CurlResponse implements ResponseInterface
*/
private function close(): void
{
$this->inflate = null;
unset($this->multi->openHandles[$this->id], $this->multi->handlesActivity[$this->id]);
curl_setopt($this->handle, CURLOPT_PRIVATE, '_0');
@ -419,22 +379,6 @@ final class CurlResponse implements ResponseInterface
}
curl_setopt($ch, CURLOPT_PRIVATE, $waitFor);
try {
if (!$content && $options['buffer'] instanceof \Closure && $content = $options['buffer']($headers) ?: null) {
$content = \is_resource($content) ? $content : fopen('php://temp', 'w+');
}
if (null !== $info['error']) {
throw new TransportException($info['error']);
}
} catch (\Throwable $e) {
$multi->handlesActivity[$id] = $multi->handlesActivity[$id] ?? [new FirstChunk()];
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = $e;
return 0;
}
} elseif (null !== $info['redirect_url'] && $logger) {
$logger->info(sprintf('Redirecting: "%s %s"', $info['http_code'], $info['redirect_url']));
}

View File

@ -94,6 +94,7 @@ class MockResponse implements ResponseInterface
*/
protected function close(): void
{
$this->inflate = null;
$this->body = [];
}
@ -105,22 +106,9 @@ class MockResponse implements ResponseInterface
$response = new self([]);
$response->requestOptions = $options;
$response->id = ++self::$idSequence;
if (!($options['buffer'] ?? null) instanceof \Closure) {
$response->content = true === ($options['buffer'] ?? true) ? fopen('php://temp', 'w+') : (\is_resource($options['buffer']) ? $options['buffer'] : null);
}
$response->shouldBuffer = $options['buffer'] ?? true;
$response->initializer = static function (self $response) {
if (null !== $response->info['error']) {
throw new TransportException($response->info['error']);
}
if (\is_array($response->body[0] ?? null)) {
foreach (self::stream([$response]) as $chunk) {
if ($chunk->isFirst()) {
break;
}
}
}
return \is_array($response->body[0] ?? null);
};
$response->info['redirect_count'] = 0;
@ -198,11 +186,6 @@ class MockResponse implements ResponseInterface
} else {
// Data or timeout chunk
$multi->handlesActivity[$id][] = $chunk;
if (\is_string($chunk) && null !== $response->content) {
// Buffer response body
fwrite($response->content, $chunk);
}
}
}
}

View File

@ -32,7 +32,6 @@ final class NativeResponse implements ResponseInterface
private $onProgress;
private $remaining;
private $buffer;
private $inflate;
private $multi;
private $debugBuffer;
private $shouldBuffer;
@ -40,7 +39,7 @@ final class NativeResponse implements ResponseInterface
/**
* @internal
*/
public function __construct(NativeClientState $multi, $context, string $url, $options, bool $gzipEnabled, array &$info, callable $resolveRedirect, ?callable $onProgress, ?LoggerInterface $logger)
public function __construct(NativeClientState $multi, $context, string $url, array $options, array &$info, callable $resolveRedirect, ?callable $onProgress, ?LoggerInterface $logger)
{
$this->multi = $multi;
$this->id = (int) $context;
@ -51,28 +50,17 @@ final class NativeResponse implements ResponseInterface
$this->info = &$info;
$this->resolveRedirect = $resolveRedirect;
$this->onProgress = $onProgress;
$this->content = true === $options['buffer'] ? fopen('php://temp', 'w+') : (\is_resource($options['buffer']) ? $options['buffer'] : null);
$this->shouldBuffer = $options['buffer'] instanceof \Closure ? $options['buffer'] : null;
$this->inflate = !isset($options['normalized_headers']['accept-encoding']);
$this->shouldBuffer = $options['buffer'] ?? true;
// Temporary resources to dechunk/inflate the response stream
// Temporary resource to dechunk the response stream
$this->buffer = fopen('php://temp', 'w+');
$this->inflate = $gzipEnabled ? inflate_init(ZLIB_ENCODING_GZIP) : null;
$info['user_data'] = $options['user_data'];
++$multi->responseCount;
$this->initializer = static function (self $response) {
if (null !== $response->info['error']) {
throw new TransportException($response->info['error']);
}
if (null === $response->remaining) {
foreach (self::stream([$response]) as $chunk) {
if ($chunk->isFirst()) {
break;
}
}
}
return null === $response->remaining;
};
}
@ -169,7 +157,7 @@ final class NativeResponse implements ResponseInterface
stream_set_blocking($h, false);
$this->context = $this->resolveRedirect = null;
// Create dechunk and inflate buffers
// Create dechunk buffers
if (isset($this->headers['content-length'])) {
$this->remaining = (int) $this->headers['content-length'][0];
} elseif ('chunked' === ($this->headers['transfer-encoding'][0] ?? null)) {
@ -179,27 +167,6 @@ final class NativeResponse implements ResponseInterface
$this->remaining = -2;
}
if ($this->inflate && 'gzip' !== ($this->headers['content-encoding'][0] ?? null)) {
$this->inflate = null;
}
try {
if (null !== $this->shouldBuffer && null === $this->content && $this->content = ($this->shouldBuffer)($this->headers) ?: null) {
$this->content = \is_resource($this->content) ? $this->content : fopen('php://temp', 'w+');
}
if (null !== $this->info['error']) {
throw new TransportException($this->info['error']);
}
} catch (\Throwable $e) {
$this->close();
$this->multi->handlesActivity[$this->id] = [new FirstChunk()];
$this->multi->handlesActivity[$this->id][] = null;
$this->multi->handlesActivity[$this->id][] = $e;
return;
}
$this->multi->handlesActivity[$this->id] = [new FirstChunk()];
if ('HEAD' === $context['http']['method'] || \in_array($this->info['http_code'], [204, 304], true)) {
@ -209,7 +176,7 @@ final class NativeResponse implements ResponseInterface
return;
}
$this->multi->openHandles[$this->id] = [$h, $this->buffer, $this->inflate, $this->content, $this->onProgress, &$this->remaining, &$this->info];
$this->multi->openHandles[$this->id] = [$h, $this->buffer, $this->onProgress, &$this->remaining, &$this->info];
}
/**
@ -249,15 +216,15 @@ final class NativeResponse implements ResponseInterface
$multi->handles = [];
}
foreach ($multi->openHandles as $i => [$h, $buffer, $inflate, $content, $onProgress]) {
foreach ($multi->openHandles as $i => [$h, $buffer, $onProgress]) {
$hasActivity = false;
$remaining = &$multi->openHandles[$i][5];
$info = &$multi->openHandles[$i][6];
$remaining = &$multi->openHandles[$i][3];
$info = &$multi->openHandles[$i][4];
$e = null;
// Read incoming buffer and write it to the dechunk one
try {
while ($remaining && '' !== $data = (string) fread($h, 0 > $remaining ? 16372 : $remaining)) {
if ($remaining && '' !== $data = (string) fread($h, 0 > $remaining ? 16372 : $remaining)) {
fwrite($buffer, $data);
$hasActivity = true;
$multi->sleep = false;
@ -285,16 +252,8 @@ final class NativeResponse implements ResponseInterface
rewind($buffer);
ftruncate($buffer, 0);
if (null !== $inflate && false === $data = @inflate_add($inflate, $data)) {
$e = new TransportException('Error while processing content unencoding.');
}
if ('' !== $data && null === $e) {
if (null === $e) {
$multi->handlesActivity[$i][] = $data;
if (null !== $content && \strlen($data) !== fwrite($content, $data)) {
$e = new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($data)));
}
}
}

View File

@ -43,11 +43,6 @@ trait ResponseTrait
*/
private $initializer;
/**
* @var resource A php://temp stream typically
*/
private $content;
private $info = [
'response_headers' => [],
'http_code' => 0,
@ -59,6 +54,9 @@ trait ResponseTrait
private $handle;
private $id;
private $timeout = 0;
private $inflate;
private $shouldBuffer;
private $content;
private $finalInfo;
private $offset = 0;
private $jsonData;
@ -69,8 +67,7 @@ trait ResponseTrait
public function getStatusCode(): int
{
if ($this->initializer) {
($this->initializer)($this);
$this->initializer = null;
self::initialize($this);
}
return $this->info['http_code'];
@ -82,8 +79,7 @@ trait ResponseTrait
public function getHeaders(bool $throw = true): array
{
if ($this->initializer) {
($this->initializer)($this);
$this->initializer = null;
self::initialize($this);
}
if ($throw) {
@ -99,8 +95,7 @@ trait ResponseTrait
public function getContent(bool $throw = true): string
{
if ($this->initializer) {
($this->initializer)($this);
$this->initializer = null;
self::initialize($this);
}
if ($throw) {
@ -231,6 +226,30 @@ trait ResponseTrait
*/
abstract protected static function select(ClientState $multi, float $timeout): int;
private static function initialize(self $response): void
{
if (null !== $response->info['error']) {
throw new TransportException($response->info['error']);
}
try {
if (($response->initializer)($response)) {
foreach (self::stream([$response]) as $chunk) {
if ($chunk->isFirst()) {
break;
}
}
}
} catch (\Throwable $e) {
// Persist timeouts thrown during initialization
$response->info['error'] = $e->getMessage();
$response->close();
throw $e;
}
$response->initializer = null;
}
private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers, string &$debug = ''): void
{
foreach ($responseHeaders as $h) {
@ -276,8 +295,7 @@ trait ResponseTrait
private function doDestruct()
{
if ($this->initializer && null === $this->info['error']) {
($this->initializer)($this);
$this->initializer = null;
self::initialize($this);
$this->checkStatusCode();
}
}
@ -329,6 +347,16 @@ trait ResponseTrait
$isTimeout = false;
if (\is_string($chunk = array_shift($multi->handlesActivity[$j]))) {
if (null !== $response->inflate && false === $chunk = @inflate_add($response->inflate, $chunk)) {
$multi->handlesActivity[$j] = [null, new TransportException('Error while processing content unencoding.')];
continue;
}
if ('' !== $chunk && null !== $response->content && \strlen($chunk) !== fwrite($response->content, $chunk)) {
$multi->handlesActivity[$j] = [null, new TransportException('Failed writing %d bytes to the response buffer.', \strlen($chunk))];
continue;
}
$response->offset += \strlen($chunk);
$chunk = new DataChunk($response->offset, $chunk);
} elseif (null === $chunk) {
@ -356,6 +384,28 @@ trait ResponseTrait
$response->logger->info(sprintf('Response: "%s %s"', $info['http_code'], $info['url']));
}
$response->inflate = \extension_loaded('zlib') && $response->inflate && 'gzip' === ($response->headers['content-encoding'][0] ?? null) ? inflate_init(ZLIB_ENCODING_GZIP) : null;
if ($response->shouldBuffer instanceof \Closure) {
try {
$response->shouldBuffer = ($response->shouldBuffer)($response->headers);
if (null !== $response->info['error']) {
throw new TransportException($response->info['error']);
}
} catch (\Throwable $e) {
$response->close();
$multi->handlesActivity[$j] = [null, $e];
}
}
if (true === $response->shouldBuffer) {
$response->content = fopen('php://temp', 'w+');
} elseif (\is_resource($response->shouldBuffer)) {
$response->content = $response->shouldBuffer;
}
$response->shouldBuffer = null;
yield $response => $chunk;
if ($response->initializer && null === $response->info['error']) {