Skip to content

Commit 128a9d1

Browse files
committed
Fix possible race condition when connector settles immediately
1 parent 2e150b4 commit 128a9d1

File tree

2 files changed

+33
-6
lines changed

2 files changed

+33
-6
lines changed

src/HappyEyeBallsConnectionBuilder.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ public function check($resolve, $reject)
146146
$ip = \array_shift($this->connectQueue);
147147

148148
$that = $this;
149-
$that->connectionPromises[$ip] = $this->attemptConnection($ip)->then(function ($connection) use ($that, $ip, $resolve) {
149+
$that->connectionPromises[$ip] = $this->attemptConnection($ip);
150+
$that->connectionPromises[$ip]->then(function ($connection) use ($that, $ip, $resolve) {
150151
unset($that->connectionPromises[$ip]);
151152

152153
$that->cleanUp();
@@ -180,7 +181,7 @@ public function check($resolve, $reject)
180181

181182
// Allow next connection attempt in 100ms: https://tools.ietf.org/html/rfc8305#section-5
182183
// Only start timer when more IPs are queued or when DNS query is still pending (might add more IPs)
183-
if (\count($this->connectQueue) > 0 || $this->resolved[Message::TYPE_A] === false || $this->resolved[Message::TYPE_AAAA] === false) {
184+
if ($this->nextAttemptTimer === null && (\count($this->connectQueue) > 0 || $this->resolved[Message::TYPE_A] === false || $this->resolved[Message::TYPE_AAAA] === false)) {
184185
$this->nextAttemptTimer = $this->loop->addTimer(self::CONNECTION_ATTEMPT_DELAY, function () use ($that, $resolve, $reject) {
185186
$that->nextAttemptTimer = null;
186187

tests/HappyEyeBallsConnectionBuilderTest.php

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,13 +261,12 @@ public function testConnectWillStartConnectingWithAttemptTimerWhenOnlyIpv6Resolv
261261
$loop->expects($this->once())->method('addTimer')->with(0.1, $this->anything())->willReturn($timer);
262262
$loop->expects($this->once())->method('cancelTimer')->with($timer);
263263

264-
$deferred = new Deferred();
265264
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
266265
$connector->expects($this->exactly(2))->method('connect')->withConsecutive(
267266
array('tcp://[::1]:80?hostname=reactphp.org'),
268267
array('tcp://[::2]:80?hostname=reactphp.org')
269268
)->willReturnOnConsecutiveCalls(
270-
$deferred->promise(),
269+
\React\Promise\reject(new \RuntimeException()),
271270
new Promise(function () { })
272271
);
273272

@@ -287,8 +286,6 @@ public function testConnectWillStartConnectingWithAttemptTimerWhenOnlyIpv6Resolv
287286
$builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts);
288287

289288
$builder->connect();
290-
291-
$deferred->reject(new \RuntimeException());
292289
}
293290

294291
public function testConnectWillStartConnectingAndWillStartNextConnectionWithoutNewAttemptTimerWhenNextAttemptTimerFiresAfterIpv4Rejected()
@@ -566,4 +563,33 @@ public function testAttemptConnectionWillConnectViaConnectorToGivenIpv6WithAllUr
566563

567564
$builder->attemptConnection('::1');
568565
}
566+
567+
public function testCheckCallsRejectFunctionImmediateWithoutLeavingDanglingPromiseWhenConnectorRejectsImmediately()
568+
{
569+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
570+
571+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
572+
$connector->expects($this->once())->method('connect')->with('tcp://[::1]:80/path?test=yes&hostname=reactphp.org#start')->willReturn(\React\Promise\reject(new \RuntimeException()));
573+
574+
$resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock();
575+
$resolver->expects($this->never())->method('resolveAll');
576+
577+
$uri = 'tcp://reactphp.org:80/path?test=yes#start';
578+
$host = 'reactphp.org';
579+
$parts = parse_url($uri);
580+
581+
$builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts);
582+
583+
$ref = new \ReflectionProperty($builder, 'connectQueue');
584+
$ref->setAccessible(true);
585+
$ref->setValue($builder, array('::1'));
586+
587+
$builder->check($this->expectCallableNever(), function () { });
588+
589+
$ref = new \ReflectionProperty($builder, 'connectionPromises');
590+
$ref->setAccessible(true);
591+
$promises = $ref->getValue($builder);
592+
593+
$this->assertEquals(array(), $promises);
594+
}
569595
}

0 commit comments

Comments
 (0)