Skip to content

Commit 3314505

Browse files
committed
Add Stopwatch on TraceableClient
1 parent 1560d7f commit 3314505

File tree

4 files changed

+178
-22
lines changed

4 files changed

+178
-22
lines changed

DependencyInjection/HttpClientPass.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1515
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\ContainerInterface;
1617
use Symfony\Component\DependencyInjection\Reference;
1718
use Symfony\Component\HttpClient\TraceableHttpClient;
1819

@@ -36,7 +37,7 @@ public function process(ContainerBuilder $container)
3637

3738
foreach ($container->findTaggedServiceIds($this->clientTag) as $id => $tags) {
3839
$container->register('.debug.'.$id, TraceableHttpClient::class)
39-
->setArguments([new Reference('.debug.'.$id.'.inner')])
40+
->setArguments([new Reference('.debug.'.$id.'.inner'), new Reference('debug.stopwatch', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)])
4041
->setDecoratedService($id);
4142
$container->getDefinition('data_collector.http_client')
4243
->addMethodCall('registerClient', [$id, new Reference('.debug.'.$id)]);

Response/TraceableResponse.php

Lines changed: 81 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
namespace Symfony\Component\HttpClient\Response;
1313

14+
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
1415
use Symfony\Component\HttpClient\Exception\ClientException;
1516
use Symfony\Component\HttpClient\Exception\RedirectionException;
1617
use Symfony\Component\HttpClient\Exception\ServerException;
1718
use Symfony\Component\HttpClient\TraceableHttpClient;
19+
use Symfony\Component\Stopwatch\StopwatchEvent;
1820
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
1921
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
2022
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
@@ -32,57 +34,98 @@ class TraceableResponse implements ResponseInterface, StreamableInterface
3234
private $client;
3335
private $response;
3436
private $content;
37+
private $event;
3538

36-
public function __construct(HttpClientInterface $client, ResponseInterface $response, &$content)
39+
public function __construct(HttpClientInterface $client, ResponseInterface $response, &$content, StopwatchEvent $event = null)
3740
{
3841
$this->client = $client;
3942
$this->response = $response;
4043
$this->content = &$content;
44+
$this->event = $event;
45+
}
46+
47+
public function __destruct()
48+
{
49+
try {
50+
$this->response->__destruct();
51+
} finally {
52+
if ($this->event && $this->event->isStarted()) {
53+
$this->event->stop();
54+
}
55+
}
4156
}
4257

4358
public function getStatusCode(): int
4459
{
45-
return $this->response->getStatusCode();
60+
try {
61+
return $this->response->getStatusCode();
62+
} finally {
63+
if ($this->event && $this->event->isStarted()) {
64+
$this->event->lap();
65+
}
66+
}
4667
}
4768

4869
public function getHeaders(bool $throw = true): array
4970
{
50-
return $this->response->getHeaders($throw);
71+
try {
72+
return $this->response->getHeaders($throw);
73+
} finally {
74+
if ($this->event && $this->event->isStarted()) {
75+
$this->event->lap();
76+
}
77+
}
5178
}
5279

5380
public function getContent(bool $throw = true): string
5481
{
55-
if (false === $this->content) {
56-
return $this->response->getContent($throw);
57-
}
82+
try {
83+
if (false === $this->content) {
84+
return $this->response->getContent($throw);
85+
}
5886

59-
$this->content = $this->response->getContent(false);
87+
$this->content = $this->response->getContent(false);
6088

61-
if ($throw) {
62-
$this->checkStatusCode($this->response->getStatusCode());
63-
}
89+
if ($throw) {
90+
$this->checkStatusCode($this->response->getStatusCode());
91+
}
6492

65-
return $this->content;
93+
return $this->content;
94+
} finally {
95+
if ($this->event && $this->event->isStarted()) {
96+
$this->event->stop();
97+
}
98+
}
6699
}
67100

68101
public function toArray(bool $throw = true): array
69102
{
70-
if (false === $this->content) {
71-
return $this->response->toArray($throw);
72-
}
103+
try {
104+
if (false === $this->content) {
105+
return $this->response->toArray($throw);
106+
}
73107

74-
$this->content = $this->response->toArray(false);
108+
$this->content = $this->response->toArray(false);
75109

76-
if ($throw) {
77-
$this->checkStatusCode($this->response->getStatusCode());
78-
}
110+
if ($throw) {
111+
$this->checkStatusCode($this->response->getStatusCode());
112+
}
79113

80-
return $this->content;
114+
return $this->content;
115+
} finally {
116+
if ($this->event && $this->event->isStarted()) {
117+
$this->event->stop();
118+
}
119+
}
81120
}
82121

83122
public function cancel(): void
84123
{
85124
$this->response->cancel();
125+
126+
if ($this->event && $this->event->isStarted()) {
127+
$this->event->stop();
128+
}
86129
}
87130

88131
public function getInfo(string $type = null)
@@ -129,9 +172,28 @@ public static function stream(HttpClientInterface $client, iterable $responses,
129172

130173
$traceableMap[$r->response] = $r;
131174
$wrappedResponses[] = $r->response;
175+
if ($r->event && !$r->event->isStarted()) {
176+
$r->event->start();
177+
}
132178
}
133179

134180
foreach ($client->stream($wrappedResponses, $timeout) as $r => $chunk) {
181+
if ($traceableMap[$r]->event && $traceableMap[$r]->event->isStarted()) {
182+
try {
183+
if ($chunk->isTimeout() || !$chunk->isLast()) {
184+
$traceableMap[$r]->event->lap();
185+
} else {
186+
$traceableMap[$r]->event->stop();
187+
}
188+
} catch (TransportExceptionInterface $e) {
189+
$traceableMap[$r]->event->stop();
190+
if ($chunk instanceof ErrorChunk) {
191+
$chunk->didThrow(false);
192+
} else {
193+
$chunk = new ErrorChunk($chunk->getOffset(), $e);
194+
}
195+
}
196+
}
135197
yield $traceableMap[$r] => $chunk;
136198
}
137199
}

Tests/TraceableHttpClientTest.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
namespace Symfony\Component\HttpClient\Tests;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpClient\Exception\ClientException;
1516
use Symfony\Component\HttpClient\MockHttpClient;
1617
use Symfony\Component\HttpClient\NativeHttpClient;
1718
use Symfony\Component\HttpClient\Response\MockResponse;
1819
use Symfony\Component\HttpClient\TraceableHttpClient;
20+
use Symfony\Component\Stopwatch\Stopwatch;
1921
use Symfony\Contracts\HttpClient\HttpClientInterface;
2022
use Symfony\Contracts\HttpClient\Test\TestHttpServer;
2123

@@ -115,4 +117,92 @@ public function testStream()
115117
$this->assertGreaterThan(1, \count($chunks));
116118
$this->assertSame('Symfony is awesome!', implode('', $chunks));
117119
}
120+
121+
public function testStopwatch()
122+
{
123+
$sw = new Stopwatch(true);
124+
$sut = new TraceableHttpClient(new NativeHttpClient(), $sw);
125+
$response = $sut->request('GET', 'http://localhost:8057');
126+
127+
$response->getStatusCode();
128+
$response->getHeaders();
129+
$response->getContent();
130+
131+
$this->assertArrayHasKey('__root__', $sections = $sw->getSections());
132+
$this->assertCount(1, $events = $sections['__root__']->getEvents());
133+
$this->assertArrayHasKey('GET http://localhost:8057', $events);
134+
$this->assertCount(3, $events['GET http://localhost:8057']->getPeriods());
135+
$this->assertGreaterThan(0.0, $events['GET http://localhost:8057']->getDuration());
136+
}
137+
138+
public function testStopwatchError()
139+
{
140+
$sw = new Stopwatch(true);
141+
$sut = new TraceableHttpClient(new NativeHttpClient(), $sw);
142+
$response = $sut->request('GET', 'http://localhost:8057/404');
143+
144+
try {
145+
$response->getContent();
146+
$this->fail('Response should have thrown an exception');
147+
} catch (ClientException $e) {
148+
// no-op
149+
}
150+
151+
$this->assertArrayHasKey('__root__', $sections = $sw->getSections());
152+
$this->assertCount(1, $events = $sections['__root__']->getEvents());
153+
$this->assertArrayHasKey('GET http://localhost:8057/404', $events);
154+
$this->assertCount(1, $events['GET http://localhost:8057/404']->getPeriods());
155+
}
156+
157+
public function testStopwatchStream()
158+
{
159+
$sw = new Stopwatch(true);
160+
$sut = new TraceableHttpClient(new NativeHttpClient(), $sw);
161+
$response = $sut->request('GET', 'http://localhost:8057');
162+
163+
$chunkCount = 0;
164+
foreach ($sut->stream([$response]) as $chunk) {
165+
++$chunkCount;
166+
}
167+
168+
$this->assertArrayHasKey('__root__', $sections = $sw->getSections());
169+
$this->assertCount(1, $events = $sections['__root__']->getEvents());
170+
$this->assertArrayHasKey('GET http://localhost:8057', $events);
171+
$this->assertGreaterThanOrEqual($chunkCount, \count($events['GET http://localhost:8057']->getPeriods()));
172+
}
173+
174+
public function testStopwatchStreamError()
175+
{
176+
$sw = new Stopwatch(true);
177+
$sut = new TraceableHttpClient(new NativeHttpClient(), $sw);
178+
$response = $sut->request('GET', 'http://localhost:8057/404');
179+
180+
try {
181+
$chunkCount = 0;
182+
foreach ($sut->stream([$response]) as $chunk) {
183+
++$chunkCount;
184+
}
185+
$this->fail('Response should have thrown an exception');
186+
} catch (ClientException $e) {
187+
// no-op
188+
}
189+
190+
$this->assertArrayHasKey('__root__', $sections = $sw->getSections());
191+
$this->assertCount(1, $events = $sections['__root__']->getEvents());
192+
$this->assertArrayHasKey('GET http://localhost:8057/404', $events);
193+
$this->assertGreaterThanOrEqual($chunkCount, \count($events['GET http://localhost:8057/404']->getPeriods()));
194+
}
195+
196+
public function testStopwatchDestruct()
197+
{
198+
$sw = new Stopwatch(true);
199+
$sut = new TraceableHttpClient(new NativeHttpClient(), $sw);
200+
$sut->request('GET', 'http://localhost:8057');
201+
202+
$this->assertArrayHasKey('__root__', $sections = $sw->getSections());
203+
$this->assertCount(1, $events = $sections['__root__']->getEvents());
204+
$this->assertArrayHasKey('GET http://localhost:8057', $events);
205+
$this->assertCount(1, $events['GET http://localhost:8057']->getPeriods());
206+
$this->assertGreaterThan(0.0, $events['GET http://localhost:8057']->getDuration());
207+
}
118208
}

TraceableHttpClient.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Psr\Log\LoggerInterface;
1616
use Symfony\Component\HttpClient\Response\ResponseStream;
1717
use Symfony\Component\HttpClient\Response\TraceableResponse;
18+
use Symfony\Component\Stopwatch\Stopwatch;
1819
use Symfony\Contracts\HttpClient\HttpClientInterface;
1920
use Symfony\Contracts\HttpClient\ResponseInterface;
2021
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
@@ -27,10 +28,12 @@ final class TraceableHttpClient implements HttpClientInterface, ResetInterface,
2728
{
2829
private $client;
2930
private $tracedRequests = [];
31+
private $stopwatch;
3032

31-
public function __construct(HttpClientInterface $client)
33+
public function __construct(HttpClientInterface $client, Stopwatch $stopwatch = null)
3234
{
3335
$this->client = $client;
36+
$this->stopwatch = $stopwatch;
3437
}
3538

3639
/**
@@ -62,7 +65,7 @@ public function request(string $method, string $url, array $options = []): Respo
6265
}
6366
};
6467

65-
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $content);
68+
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $content, null === $this->stopwatch ? null : $this->stopwatch->start("$method $url", 'http_client'));
6669
}
6770

6871
/**

0 commit comments

Comments
 (0)