Skip to content

Commit e1793dd

Browse files
committed
dns: support max timeout
1 parent 6027371 commit e1793dd

File tree

5 files changed

+91
-12
lines changed

5 files changed

+91
-12
lines changed

doc/api/dns.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ Create a new resolver.
157157
default timeout.
158158
* `tries` {integer} The number of tries the resolver will try contacting
159159
each name server before giving up. **Default:** `4`
160+
* `maxTimeout` {integer} The max retry timeout, in milliseconds.
161+
**Default:** `0`, disabled.
160162

161163
### `resolver.cancel()`
162164

lib/internal/dns/utils.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
validateInt32,
2626
validateOneOf,
2727
validateString,
28+
validateUint32,
2829
} = require('internal/validators');
2930
let binding;
3031
function lazyBinding() {
@@ -49,6 +50,12 @@ function validateTimeout(options) {
4950
return timeout;
5051
}
5152

53+
function validateMaxTimeout(options) {
54+
const { maxTimeout = 0 } = { ...options };
55+
validateUint32(maxTimeout, 'options.maxTimeout', 0);
56+
return maxTimeout;
57+
}
58+
5259
function validateTries(options) {
5360
const { tries = 4 } = { ...options };
5461
validateInt32(tries, 'options.tries', 1);
@@ -67,17 +74,18 @@ class ResolverBase {
6774
constructor(options = undefined) {
6875
const timeout = validateTimeout(options);
6976
const tries = validateTries(options);
77+
const maxTimeout = validateMaxTimeout(options);
7078
// If we are building snapshot, save the states of the resolver along
7179
// the way.
7280
if (isBuildingSnapshot()) {
73-
this[kSnapshotStates] = { timeout, tries };
81+
this[kSnapshotStates] = { timeout, tries, maxTimeout };
7482
}
75-
this[kInitializeHandle](timeout, tries);
83+
this[kInitializeHandle](timeout, tries, maxTimeout);
7684
}
7785

78-
[kInitializeHandle](timeout, tries) {
86+
[kInitializeHandle](timeout, tries, maxTimeout) {
7987
const { ChannelWrap } = lazyBinding();
80-
this._handle = new ChannelWrap(timeout, tries);
88+
this._handle = new ChannelWrap(timeout, tries, maxTimeout);
8189
}
8290

8391
cancel() {
@@ -187,8 +195,8 @@ class ResolverBase {
187195
}
188196

189197
[kDeserializeResolver]() {
190-
const { timeout, tries, localAddress, servers } = this[kSnapshotStates];
191-
this[kInitializeHandle](timeout, tries);
198+
const { timeout, tries, maxTimeout, localAddress, servers } = this[kSnapshotStates];
199+
this[kInitializeHandle](timeout, tries, maxTimeout);
192200
if (localAddress) {
193201
const { ipv4, ipv6 } = localAddress;
194202
this._handle.setLocalAddress(ipv4, ipv6);

src/cares_wrap.cc

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -791,10 +791,12 @@ ChannelWrap::ChannelWrap(
791791
Environment* env,
792792
Local<Object> object,
793793
int timeout,
794-
int tries)
794+
int tries,
795+
int max_timeout)
795796
: AsyncWrap(env, object, PROVIDER_DNSCHANNEL),
796797
timeout_(timeout),
797-
tries_(tries) {
798+
tries_(tries),
799+
max_timeout_(max_timeout) {
798800
MakeWeak();
799801

800802
Setup();
@@ -808,13 +810,15 @@ void ChannelWrap::MemoryInfo(MemoryTracker* tracker) const {
808810

809811
void ChannelWrap::New(const FunctionCallbackInfo<Value>& args) {
810812
CHECK(args.IsConstructCall());
811-
CHECK_EQ(args.Length(), 2);
813+
CHECK_EQ(args.Length(), 3);
812814
CHECK(args[0]->IsInt32());
813815
CHECK(args[1]->IsInt32());
816+
CHECK(args[2]->IsInt32());
814817
const int timeout = args[0].As<Int32>()->Value();
815818
const int tries = args[1].As<Int32>()->Value();
819+
const int max_timeout = args[2].As<Int32>()->Value();
816820
Environment* env = Environment::GetCurrent(args);
817-
new ChannelWrap(env, args.This(), timeout, tries);
821+
new ChannelWrap(env, args.This(), timeout, tries, max_timeout);
818822
}
819823

820824
GetAddrInfoReqWrap::GetAddrInfoReqWrap(Environment* env,
@@ -879,9 +883,15 @@ void ChannelWrap::Setup() {
879883
}
880884

881885
/* We do the call to ares_init_option for caller. */
882-
const int optmask =
886+
int optmask =
883887
ARES_OPT_FLAGS | ARES_OPT_TIMEOUTMS |
884888
ARES_OPT_SOCK_STATE_CB | ARES_OPT_TRIES;
889+
890+
if (max_timeout_ > 0) {
891+
options.maxtimeout = max_timeout_;
892+
optmask |= ARES_OPT_MAXTIMEOUTMS;
893+
}
894+
885895
r = ares_init_options(&channel_, &options, optmask);
886896

887897
if (r != ARES_SUCCESS) {

src/cares_wrap.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ class ChannelWrap final : public AsyncWrap {
155155
Environment* env,
156156
v8::Local<v8::Object> object,
157157
int timeout,
158-
int tries);
158+
int tries,
159+
int max_timeout);
159160
~ChannelWrap() override;
160161

161162
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -190,6 +191,7 @@ class ChannelWrap final : public AsyncWrap {
190191
bool library_inited_ = false;
191192
int timeout_;
192193
int tries_;
194+
int max_timeout_;
193195
int active_query_count_ = 0;
194196
NodeAresTask::List task_list_;
195197
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
const common = require('../common');
3+
const dnstools = require('../common/dns');
4+
const dns = require('dns');
5+
const assert = require('assert');
6+
const dgram = require('dgram');
7+
8+
const server = dgram.createSocket('udp4');
9+
const nxdomain = 'nxdomain.org';
10+
const domain = 'example.org';
11+
const answers = [{ type: 'A', address: '1.2.3.4', ttl: 123, domain }];
12+
13+
server.on('message', common.mustCallAtLeast((msg, { address, port }) => {
14+
const parsed = dnstools.parseDNSPacket(msg);
15+
if (parsed.questions[0].domain === nxdomain) {
16+
return;
17+
}
18+
assert.strictEqual(parsed.questions[0].domain, domain);
19+
server.send(dnstools.writeDNSPacket({
20+
id: parsed.id,
21+
questions: parsed.questions,
22+
answers: answers,
23+
}), port, address);
24+
}), 1);
25+
26+
server.bind(0, common.mustCall(async () => {
27+
const address = server.address();
28+
// Test if the Resolver works as before.
29+
const resolver = new dns.promises.Resolver({ timeout: 1000, tries: 1, maxTimeouts: 1000 });
30+
resolver.setServers([`127.0.0.1:${address.port}`]);
31+
const res = await resolver.resolveAny('example.org');
32+
assert.strictEqual(res.length, 1);
33+
assert.strictEqual(res.length, answers.length);
34+
assert.strictEqual(res[0].address, answers[0].address);
35+
36+
// Test that maxTimeout is effective.
37+
// Without maxTimeout, the timeout will keep increasing when retrying.
38+
const timeout1 = await timeout(address, { timeout: 500, tries: 3 });
39+
// With maxTimeout, the timeout will always 500 when retrying.
40+
const timeout2 = await timeout(address, { timeout: 500, tries: 3, maxTimeout: 500 });
41+
console.log(`timeout1: ${timeout1}, timeout2: ${timeout2}`);
42+
assert.strictEqual(timeout1 !== undefined && timeout2 !== undefined, true);
43+
assert.strictEqual(timeout1 > timeout2, true);
44+
server.close();
45+
}));
46+
47+
async function timeout(address, options) {
48+
const start = Date.now();
49+
const resolver = new dns.promises.Resolver(options);
50+
resolver.setServers([`127.0.0.1:${address.port}`]);
51+
try {
52+
await resolver.resolveAny(nxdomain);
53+
} catch (e) {
54+
assert.strictEqual(e.code, 'ETIMEOUT');
55+
return Date.now() - start;
56+
}
57+
}

0 commit comments

Comments
 (0)