Skip to content

Commit 1eb04e0

Browse files
authored
Merge pull request #180 from clue-labs/fallback-servers
2 parents ae37876 + 84d0c5f commit 1eb04e0

File tree

4 files changed

+526
-25
lines changed

4 files changed

+526
-25
lines changed

src/Query/FallbackExecutor.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace React\Dns\Query;
4+
5+
use React\Promise\Promise;
6+
7+
final class FallbackExecutor implements ExecutorInterface
8+
{
9+
private $executor;
10+
private $fallback;
11+
12+
public function __construct(ExecutorInterface $executor, ExecutorInterface $fallback)
13+
{
14+
$this->executor = $executor;
15+
$this->fallback = $fallback;
16+
}
17+
18+
public function query(Query $query)
19+
{
20+
$cancelled = false;
21+
$fallback = $this->fallback;
22+
$promise = $this->executor->query($query);
23+
24+
return new Promise(function ($resolve, $reject) use (&$promise, $fallback, $query, &$cancelled) {
25+
$promise->then($resolve, function (\Exception $e1) use ($fallback, $query, $resolve, $reject, &$cancelled, &$promise) {
26+
// reject if primary resolution rejected due to cancellation
27+
if ($cancelled) {
28+
$reject($e1);
29+
return;
30+
}
31+
32+
// start fallback query if primary query rejected
33+
$promise = $fallback->query($query)->then($resolve, function (\Exception $e2) use ($e1, $reject) {
34+
$append = $e2->getMessage();
35+
if (($pos = strpos($append, ':')) !== false) {
36+
$append = substr($append, $pos + 2);
37+
}
38+
39+
// reject with combined error message if both queries fail
40+
$reject(new \RuntimeException($e1->getMessage() . '. ' . $append));
41+
});
42+
});
43+
}, function () use (&$promise, &$cancelled) {
44+
// cancel pending query (primary or fallback)
45+
$cancelled = true;
46+
$promise->cancel();
47+
});
48+
}
49+
}

src/Resolver/Factory.php

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use React\Dns\Query\CachingExecutor;
1010
use React\Dns\Query\CoopExecutor;
1111
use React\Dns\Query\ExecutorInterface;
12+
use React\Dns\Query\FallbackExecutor;
1213
use React\Dns\Query\HostsFileExecutor;
1314
use React\Dns\Query\RetryExecutor;
1415
use React\Dns\Query\SelectiveTransportExecutor;
@@ -24,8 +25,9 @@ final class Factory
2425
*
2526
* As of v1.7.0 it's recommended to pass a `Config` object instead of a
2627
* single nameserver address. If the given config contains more than one DNS
27-
* nameserver, only the primary will be used at the moment. A future version
28-
* may take advantage of fallback DNS servers.
28+
* nameserver, all DNS nameservers will be used in order. The primary DNS
29+
* server will always be used first before falling back to the secondary or
30+
* tertiary DNS server.
2931
*
3032
* @param Config|string $config DNS Config object (recommended) or single nameserver address
3133
* @param LoopInterface $loop
@@ -45,8 +47,9 @@ public function create($config, LoopInterface $loop)
4547
*
4648
* As of v1.7.0 it's recommended to pass a `Config` object instead of a
4749
* single nameserver address. If the given config contains more than one DNS
48-
* nameserver, only the primary will be used at the moment. A future version
49-
* may take advantage of fallback DNS servers.
50+
* nameserver, all DNS nameservers will be used in order. The primary DNS
51+
* server will always be used first before falling back to the secondary or
52+
* tertiary DNS server.
5053
*
5154
* @param Config|string $config DNS Config object (recommended) or single nameserver address
5255
* @param LoopInterface $loop
@@ -109,12 +112,56 @@ private function decorateHostsFileExecutor(ExecutorInterface $executor)
109112
private function createExecutor($nameserver, LoopInterface $loop)
110113
{
111114
if ($nameserver instanceof Config) {
112-
$nameserver = \reset($nameserver->nameservers);
113-
if ($nameserver === false) {
115+
if (!$nameserver->nameservers) {
114116
throw new \UnderflowException('Empty config with no DNS servers');
115117
}
118+
119+
// Hard-coded to check up to 3 DNS servers to match default limits in place in most systems (see MAXNS config).
120+
// Note to future self: Recursion isn't too hard, but how deep do we really want to go?
121+
$primary = reset($nameserver->nameservers);
122+
$secondary = next($nameserver->nameservers);
123+
$tertiary = next($nameserver->nameservers);
124+
125+
if ($tertiary !== false) {
126+
// 3 DNS servers given => nest first with fallback for second and third
127+
return new CoopExecutor(
128+
new RetryExecutor(
129+
new FallbackExecutor(
130+
$this->createSingleExecutor($primary, $loop),
131+
new FallbackExecutor(
132+
$this->createSingleExecutor($secondary, $loop),
133+
$this->createSingleExecutor($tertiary, $loop)
134+
)
135+
)
136+
)
137+
);
138+
} elseif ($secondary !== false) {
139+
// 2 DNS servers given => fallback from first to second
140+
return new CoopExecutor(
141+
new RetryExecutor(
142+
new FallbackExecutor(
143+
$this->createSingleExecutor($primary, $loop),
144+
$this->createSingleExecutor($secondary, $loop)
145+
)
146+
)
147+
);
148+
} else {
149+
// 1 DNS server given => use single executor
150+
$nameserver = $primary;
151+
}
116152
}
117153

154+
return new CoopExecutor(new RetryExecutor($this->createSingleExecutor($nameserver, $loop)));
155+
}
156+
157+
/**
158+
* @param string $nameserver
159+
* @param LoopInterface $loop
160+
* @return ExecutorInterface
161+
* @throws \InvalidArgumentException for invalid DNS server address
162+
*/
163+
private function createSingleExecutor($nameserver, LoopInterface $loop)
164+
{
118165
$parts = \parse_url($nameserver);
119166

120167
if (isset($parts['scheme']) && $parts['scheme'] === 'tcp') {
@@ -128,9 +175,15 @@ private function createExecutor($nameserver, LoopInterface $loop)
128175
);
129176
}
130177

131-
return new CoopExecutor($executor);
178+
return $executor;
132179
}
133180

181+
/**
182+
* @param string $nameserver
183+
* @param LoopInterface $loop
184+
* @return TimeoutExecutor
185+
* @throws \InvalidArgumentException for invalid DNS server address
186+
*/
134187
private function createTcpExecutor($nameserver, LoopInterface $loop)
135188
{
136189
return new TimeoutExecutor(
@@ -140,17 +193,21 @@ private function createTcpExecutor($nameserver, LoopInterface $loop)
140193
);
141194
}
142195

196+
/**
197+
* @param string $nameserver
198+
* @param LoopInterface $loop
199+
* @return TimeoutExecutor
200+
* @throws \InvalidArgumentException for invalid DNS server address
201+
*/
143202
private function createUdpExecutor($nameserver, LoopInterface $loop)
144203
{
145-
return new RetryExecutor(
146-
new TimeoutExecutor(
147-
new UdpTransportExecutor(
148-
$nameserver,
149-
$loop
150-
),
151-
5.0,
204+
return new TimeoutExecutor(
205+
new UdpTransportExecutor(
206+
$nameserver,
152207
$loop
153-
)
208+
),
209+
5.0,
210+
$loop
154211
);
155212
}
156213
}

0 commit comments

Comments
 (0)