Comparison

spec/net_resolvers_service_spec.lua @ 12401:c029ddcad258

net.resolvers.service: Honour record 'weight' when picking SRV targets #NotHappyEyeballs
author Matthew Wild <mwild1@gmail.com>
date Thu, 17 Mar 2022 18:20:26 +0000
comparison
equal deleted inserted replaced
12400:728d1c1dc7db 12401:c029ddcad258
1 local set = require "util.set";
2
3 insulate("net.resolvers.service", function ()
4 local adns = {
5 resolver = function ()
6 return {
7 lookup = function (_, cb, qname, qtype, qclass)
8 if qname == "_xmpp-server._tcp.example.com"
9 and (qtype or "SRV") == "SRV"
10 and (qclass or "IN") == "IN" then
11 cb({
12 { -- 60+35+60
13 srv = { target = "xmpp0-a.example.com", port = 5228, priority = 0, weight = 60 };
14 };
15 {
16 srv = { target = "xmpp0-b.example.com", port = 5216, priority = 0, weight = 35 };
17 };
18 {
19 srv = { target = "xmpp0-c.example.com", port = 5200, priority = 0, weight = 0 };
20 };
21 {
22 srv = { target = "xmpp0-d.example.com", port = 5256, priority = 0, weight = 120 };
23 };
24
25 {
26 srv = { target = "xmpp1-a.example.com", port = 5273, priority = 1, weight = 30 };
27 };
28 {
29 srv = { target = "xmpp1-b.example.com", port = 5274, priority = 1, weight = 30 };
30 };
31
32 {
33 srv = { target = "xmpp2.example.com", port = 5275, priority = 2, weight = 0 };
34 };
35 });
36 elseif qname == "_xmpp-server._tcp.single.example.com"
37 and (qtype or "SRV") == "SRV"
38 and (qclass or "IN") == "IN" then
39 cb({
40 {
41 srv = { target = "xmpp0-a.example.com", port = 5269, priority = 0, weight = 0 };
42 };
43 });
44 elseif qname == "_xmpp-server._tcp.half.example.com"
45 and (qtype or "SRV") == "SRV"
46 and (qclass or "IN") == "IN" then
47 cb({
48 {
49 srv = { target = "xmpp0-a.example.com", port = 5269, priority = 0, weight = 0 };
50 };
51 {
52 srv = { target = "xmpp0-b.example.com", port = 5270, priority = 0, weight = 1 };
53 };
54 });
55 elseif qtype == "A" then
56 local l = qname:match("%-(%a)%.example.com$") or "1";
57 local d = ("%d"):format(l:byte())
58 cb({
59 {
60 a = "127.0.0."..d;
61 };
62 });
63 elseif qtype == "AAAA" then
64 local l = qname:match("%-(%a)%.example.com$") or "1";
65 local d = ("%04d"):format(l:byte())
66 cb({
67 {
68 aaaa = "fdeb:9619:649e:c7d9::"..d;
69 };
70 });
71 else
72 cb(nil);
73 end
74 end;
75 };
76 end;
77 };
78 package.loaded["net.adns"] = mock(adns);
79 local resolver = require "net.resolvers.service";
80 math.randomseed(os.time());
81 it("works for 99% of deployments", function ()
82 -- Most deployments only have a single SRV record, let's make
83 -- sure that works okay
84
85 local expected_targets = set.new({
86 -- xmpp0-a
87 "tcp4 127.0.0.97 5269";
88 "tcp6 fdeb:9619:649e:c7d9::0097 5269";
89 });
90 local received_targets = set.new({});
91
92 local r = resolver.new("single.example.com", "xmpp-server");
93 local done = false;
94 local function handle_target(...)
95 if ... == nil then
96 done = true;
97 -- No more targets
98 return;
99 end
100 received_targets:add(table.concat({ ... }, " ", 1, 3));
101 end
102 r:next(handle_target);
103 while not done do
104 r:next(handle_target);
105 end
106
107 -- We should have received all expected targets, and no unexpected
108 -- ones:
109 assert.truthy(set.xor(received_targets, expected_targets):empty());
110 end);
111
112 it("supports A/AAAA fallback", function ()
113 -- Many deployments don't have any SRV records, so we should
114 -- fall back to A/AAAA records instead when that is the case
115
116 local expected_targets = set.new({
117 -- xmpp0-a
118 "tcp4 127.0.0.97 5269";
119 "tcp6 fdeb:9619:649e:c7d9::0097 5269";
120 });
121 local received_targets = set.new({});
122
123 local r = resolver.new("xmpp0-a.example.com", "xmpp-server", "tcp", { default_port = 5269 });
124 local done = false;
125 local function handle_target(...)
126 if ... == nil then
127 done = true;
128 -- No more targets
129 return;
130 end
131 received_targets:add(table.concat({ ... }, " ", 1, 3));
132 end
133 r:next(handle_target);
134 while not done do
135 r:next(handle_target);
136 end
137
138 -- We should have received all expected targets, and no unexpected
139 -- ones:
140 assert.truthy(set.xor(received_targets, expected_targets):empty());
141 end);
142
143
144 it("works", function ()
145 local expected_targets = set.new({
146 -- xmpp0-a
147 "tcp4 127.0.0.97 5228";
148 "tcp6 fdeb:9619:649e:c7d9::0097 5228";
149 "tcp4 127.0.0.97 5273";
150 "tcp6 fdeb:9619:649e:c7d9::0097 5273";
151
152 -- xmpp0-b
153 "tcp4 127.0.0.98 5274";
154 "tcp6 fdeb:9619:649e:c7d9::0098 5274";
155 "tcp4 127.0.0.98 5216";
156 "tcp6 fdeb:9619:649e:c7d9::0098 5216";
157
158 -- xmpp0-c
159 "tcp4 127.0.0.99 5200";
160 "tcp6 fdeb:9619:649e:c7d9::0099 5200";
161
162 -- xmpp0-d
163 "tcp4 127.0.0.100 5256";
164 "tcp6 fdeb:9619:649e:c7d9::0100 5256";
165
166 -- xmpp2
167 "tcp4 127.0.0.49 5275";
168 "tcp6 fdeb:9619:649e:c7d9::0049 5275";
169
170 });
171 local received_targets = set.new({});
172
173 local r = resolver.new("example.com", "xmpp-server");
174 local done = false;
175 local function handle_target(...)
176 if ... == nil then
177 done = true;
178 -- No more targets
179 return;
180 end
181 received_targets:add(table.concat({ ... }, " ", 1, 3));
182 end
183 r:next(handle_target);
184 while not done do
185 r:next(handle_target);
186 end
187
188 -- We should have received all expected targets, and no unexpected
189 -- ones:
190 assert.truthy(set.xor(received_targets, expected_targets):empty());
191 end);
192
193 it("balances across weights correctly #slow", function ()
194 -- This mimics many repeated connections to 'example.com' (mock
195 -- records defined above), and records the port number of the
196 -- first target. Therefore it (should) only return priority
197 -- 0 records, and the input data is constructed such that the
198 -- last two digits of the port number represent the percentage
199 -- of times that record should (on average) be picked first.
200
201 -- To prevent random test failures, we test across a handful
202 -- of fixed (randomly selected) seeds.
203 for _, seed in ipairs({ 8401877, 3943829, 7830992 }) do
204 math.randomseed(seed);
205
206 local results = {};
207 local function run()
208 local run_results = {};
209 local r = resolver.new("example.com", "xmpp-server");
210 local function record_target(...)
211 if ... == nil then
212 -- No more targets
213 return;
214 end
215 run_results = { ... };
216 end
217 r:next(record_target);
218 return run_results[3];
219 end
220
221 for _ = 1, 1000 do
222 local port = run();
223 results[port] = (results[port] or 0) + 1;
224 end
225
226 local ports = {};
227 for port in pairs(results) do
228 table.insert(ports, port);
229 end
230 table.sort(ports);
231 for _, port in ipairs(ports) do
232 --print("PORT", port, tostring((results[port]/1000) * 100).."% hits (expected "..tostring(port-5200).."%)");
233 local hit_pct = (results[port]/1000) * 100;
234 local expected_pct = port - 5200;
235 --print(hit_pct, expected_pct, math.abs(hit_pct - expected_pct));
236 assert.is_true(math.abs(hit_pct - expected_pct) < 5);
237 end
238 --print("---");
239 end
240 end);
241 end);