Skip to content

Commit 16ff9ad

Browse files
committed
Use round robin for happy eyeballs DNS responses (load balancing)
The happy eyeballs algorithms tries to connect over both IPv6 and IPv4 at the same time. Accordingly, the hostname has to be resolved for both address families which both may potentially contain any number of records (load balancing). This changeset randomizes the order of returned IP addresses per address family. This means that if multiple records are returned, it will try to connect to a random one from this list instead of always trying the first. This allows the load to be distributed more evenly across all returned IP addresses. This can be used as a very basic DNS load balancing mechanism.
1 parent e2b96b2 commit 16ff9ad

3 files changed

+59
-50
lines changed

src/HappyEyeBallsConnectionBuilder.php

+1
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ public function hasBeenResolved()
316316
*/
317317
public function mixIpsIntoConnectQueue(array $ips)
318318
{
319+
\shuffle($ips);
319320
$this->ipsCount += \count($ips);
320321
$connectQueueStash = $this->connectQueue;
321322
$this->connectQueue = array();

tests/HappyEyeBallsConnectionBuilderTest.php

+58-6
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,8 @@ public function testConnectWillStartConnectingWithAlternatingIPv6AndIPv4WhenReso
302302
$connector->expects($this->exactly(4))->method('connect')->withConsecutive(
303303
array('tcp://[::1]:80?hostname=reactphp.org'),
304304
array('tcp://127.0.0.1:80?hostname=reactphp.org'),
305-
array('tcp://[::2]:80?hostname=reactphp.org'),
306-
array('tcp://127.0.0.2:80?hostname=reactphp.org')
305+
array('tcp://[::1]:80?hostname=reactphp.org'),
306+
array('tcp://127.0.0.1:80?hostname=reactphp.org')
307307
)->willReturnOnConsecutiveCalls(
308308
$deferred->promise(),
309309
$deferred->promise(),
@@ -316,8 +316,8 @@ public function testConnectWillStartConnectingWithAlternatingIPv6AndIPv4WhenReso
316316
array('reactphp.org', Message::TYPE_AAAA),
317317
array('reactphp.org', Message::TYPE_A)
318318
)->willReturnOnConsecutiveCalls(
319-
\React\Promise\resolve(array('::1', '::2')),
320-
\React\Promise\resolve(array('127.0.0.1', '127.0.0.2'))
319+
\React\Promise\resolve(array('::1', '::1')),
320+
\React\Promise\resolve(array('127.0.0.1', '127.0.0.1'))
321321
);
322322

323323
$uri = 'tcp://reactphp.org:80';
@@ -341,7 +341,7 @@ public function testConnectWillStartConnectingWithAttemptTimerWhenOnlyIpv6Resolv
341341
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
342342
$connector->expects($this->exactly(2))->method('connect')->withConsecutive(
343343
array('tcp://[::1]:80?hostname=reactphp.org'),
344-
array('tcp://[::2]:80?hostname=reactphp.org')
344+
array('tcp://[::1]:80?hostname=reactphp.org')
345345
)->willReturnOnConsecutiveCalls(
346346
\React\Promise\reject(new \RuntimeException()),
347347
new Promise(function () { })
@@ -352,7 +352,7 @@ public function testConnectWillStartConnectingWithAttemptTimerWhenOnlyIpv6Resolv
352352
array('reactphp.org', Message::TYPE_AAAA),
353353
array('reactphp.org', Message::TYPE_A)
354354
)->willReturnOnConsecutiveCalls(
355-
\React\Promise\resolve(array('::1', '::2')),
355+
\React\Promise\resolve(array('::1', '::1')),
356356
\React\Promise\reject(new \RuntimeException())
357357
);
358358

@@ -799,4 +799,56 @@ public function testCleanUpCancelsAllPendingConnectionAttemptsWithoutStartingNew
799799

800800
$builder->cleanUp();
801801
}
802+
803+
public function testMixIpsIntoConnectQueueSometimesAssignsInOriginalOrder()
804+
{
805+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
806+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
807+
$resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock();
808+
809+
$uri = 'tcp://reactphp.org:80/path?test=yes#start';
810+
$host = 'reactphp.org';
811+
$parts = parse_url($uri);
812+
813+
for ($i = 0; $i < 100; ++$i) {
814+
$builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts);
815+
$builder->mixIpsIntoConnectQueue(array('::1', '::2'));
816+
817+
$ref = new \ReflectionProperty($builder, 'connectQueue');
818+
$ref->setAccessible(true);
819+
$value = $ref->getValue($builder);
820+
821+
if ($value === array('::1', '::2')) {
822+
break;
823+
}
824+
}
825+
826+
$this->assertEquals(array('::1', '::2'), $value);
827+
}
828+
829+
public function testMixIpsIntoConnectQueueSometimesAssignsInReverseOrder()
830+
{
831+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
832+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
833+
$resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock();
834+
835+
$uri = 'tcp://reactphp.org:80/path?test=yes#start';
836+
$host = 'reactphp.org';
837+
$parts = parse_url($uri);
838+
839+
for ($i = 0; $i < 100; ++$i) {
840+
$builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts);
841+
$builder->mixIpsIntoConnectQueue(array('::1', '::2'));
842+
843+
$ref = new \ReflectionProperty($builder, 'connectQueue');
844+
$ref->setAccessible(true);
845+
$value = $ref->getValue($builder);
846+
847+
if ($value === array('::2', '::1')) {
848+
break;
849+
}
850+
}
851+
852+
$this->assertEquals(array('::2', '::1'), $value);
853+
}
802854
}

tests/HappyEyeBallsConnectorTest.php

-44
Original file line numberDiff line numberDiff line change
@@ -270,50 +270,6 @@ public function testCancelDuringTcpConnectionCancelsTcpConnectionIfGivenIp()
270270
$this->loop->run();
271271
}
272272

273-
/**
274-
* @dataProvider provideIpvAddresses
275-
*/
276-
public function testShouldConnectOverIpv4WhenIpv6LookupFails(array $ipv6, array $ipv4)
277-
{
278-
$this->resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive(
279-
array($this->equalTo('example.com'), Message::TYPE_AAAA),
280-
array($this->equalTo('example.com'), Message::TYPE_A)
281-
)->willReturnOnConsecutiveCalls(
282-
Promise\reject(new \Exception('failure')),
283-
Promise\resolve($ipv4)
284-
);
285-
$this->tcp->expects($this->exactly(1))->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=example.com'))->willReturn(Promise\resolve($this->connection));
286-
287-
$promise = $this->connector->connect('example.com:80');;
288-
$resolvedConnection = Block\await($promise, $this->loop);
289-
290-
self::assertSame($this->connection, $resolvedConnection);
291-
}
292-
293-
/**
294-
* @dataProvider provideIpvAddresses
295-
*/
296-
public function testShouldConnectOverIpv6WhenIpv4LookupFails(array $ipv6, array $ipv4)
297-
{
298-
if (count($ipv6) === 0) {
299-
$ipv6[] = '1:2:3:4';
300-
}
301-
302-
$this->resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive(
303-
array($this->equalTo('example.com'), Message::TYPE_AAAA),
304-
array($this->equalTo('example.com'), Message::TYPE_A)
305-
)->willReturnOnConsecutiveCalls(
306-
Promise\resolve($ipv6),
307-
Promise\reject(new \Exception('failure'))
308-
);
309-
$this->tcp->expects($this->exactly(1))->method('connect')->with($this->equalTo('[1:2:3:4]:80?hostname=example.com'))->willReturn(Promise\resolve($this->connection));
310-
311-
$promise = $this->connector->connect('example.com:80');;
312-
$resolvedConnection = Block\await($promise, $this->loop);
313-
314-
self::assertSame($this->connection, $resolvedConnection);
315-
}
316-
317273
/**
318274
* @internal
319275
*/

0 commit comments

Comments
 (0)