Skip to content

Commit 2e150b4

Browse files
committed
Immediately try next connection when one attempt fails (happy eyeballs)
1 parent 3686e51 commit 2e150b4

File tree

2 files changed

+85
-1
lines changed

2 files changed

+85
-1
lines changed

src/HappyEyeBallsConnectionBuilder.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,21 @@ public function check($resolve, $reject)
152152
$that->cleanUp();
153153

154154
$resolve($connection);
155-
}, function (\Exception $e) use ($that, $ip, $reject) {
155+
}, function (\Exception $e) use ($that, $ip, $resolve, $reject) {
156156
unset($that->connectionPromises[$ip]);
157157

158158
$that->failureCount++;
159159

160+
// start next connection attempt immediately on error
161+
if ($that->connectQueue) {
162+
if ($that->nextAttemptTimer !== null) {
163+
$that->loop->cancelTimer($that->nextAttemptTimer);
164+
$that->nextAttemptTimer = null;
165+
}
166+
167+
$that->check($resolve, $reject);
168+
}
169+
160170
if ($that->hasBeenResolved() === false) {
161171
return;
162172
}

tests/HappyEyeBallsConnectionBuilderTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,80 @@ public function testConnectWillStartConnectingWithAttemptTimerButWithoutResoluti
217217
$deferred->reject(new \RuntimeException());
218218
}
219219

220+
public function testConnectWillStartConnectingWithAttemptTimerWhenIpv6AndIpv4ResolvesAndWillStartNextConnectionAttemptWithoutAttemptTimerImmediatelyWhenFirstConnectionAttemptFails()
221+
{
222+
$timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
223+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
224+
$loop->expects($this->once())->method('addTimer')->with(0.1, $this->anything())->willReturn($timer);
225+
$loop->expects($this->once())->method('cancelTimer')->with($timer);
226+
227+
$deferred = new Deferred();
228+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
229+
$connector->expects($this->exactly(2))->method('connect')->withConsecutive(
230+
array('tcp://[::1]:80?hostname=reactphp.org'),
231+
array('tcp://127.0.0.1:80?hostname=reactphp.org')
232+
)->willReturnOnConsecutiveCalls(
233+
$deferred->promise(),
234+
new Promise(function () { })
235+
);
236+
237+
$resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock();
238+
$resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive(
239+
array('reactphp.org', Message::TYPE_AAAA),
240+
array('reactphp.org', Message::TYPE_A)
241+
)->willReturnOnConsecutiveCalls(
242+
\React\Promise\resolve(array('::1')),
243+
\React\Promise\resolve(array('127.0.0.1'))
244+
);
245+
246+
$uri = 'tcp://reactphp.org:80';
247+
$host = 'reactphp.org';
248+
$parts = parse_url($uri);
249+
250+
$builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts);
251+
252+
$builder->connect();
253+
254+
$deferred->reject(new \RuntimeException());
255+
}
256+
257+
public function testConnectWillStartConnectingWithAttemptTimerWhenOnlyIpv6ResolvesAndWillStartNextConnectionAttemptWithoutAttemptTimerImmediatelyWhenFirstConnectionAttemptFails()
258+
{
259+
$timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
260+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
261+
$loop->expects($this->once())->method('addTimer')->with(0.1, $this->anything())->willReturn($timer);
262+
$loop->expects($this->once())->method('cancelTimer')->with($timer);
263+
264+
$deferred = new Deferred();
265+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
266+
$connector->expects($this->exactly(2))->method('connect')->withConsecutive(
267+
array('tcp://[::1]:80?hostname=reactphp.org'),
268+
array('tcp://[::2]:80?hostname=reactphp.org')
269+
)->willReturnOnConsecutiveCalls(
270+
$deferred->promise(),
271+
new Promise(function () { })
272+
);
273+
274+
$resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock();
275+
$resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive(
276+
array('reactphp.org', Message::TYPE_AAAA),
277+
array('reactphp.org', Message::TYPE_A)
278+
)->willReturnOnConsecutiveCalls(
279+
\React\Promise\resolve(array('::1', '::2')),
280+
\React\Promise\reject(new \RuntimeException())
281+
);
282+
283+
$uri = 'tcp://reactphp.org:80';
284+
$host = 'reactphp.org';
285+
$parts = parse_url($uri);
286+
287+
$builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts);
288+
289+
$builder->connect();
290+
291+
$deferred->reject(new \RuntimeException());
292+
}
293+
220294
public function testConnectWillStartConnectingAndWillStartNextConnectionWithoutNewAttemptTimerWhenNextAttemptTimerFiresAfterIpv4Rejected()
221295
{
222296
$timer = null;

0 commit comments

Comments
 (0)