dnsruby 1.73.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -4
  3. data/DNSSEC +3 -2
  4. data/EXAMPLES +1 -1
  5. data/README.md +370 -64
  6. data/RELEASE_NOTES.md +314 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/dnsruby.gemspec +6 -4
  10. data/lib/dnsruby/cache.rb +279 -49
  11. data/lib/dnsruby/code_mapper.rb +35 -8
  12. data/lib/dnsruby/code_mappers.rb +63 -49
  13. data/lib/dnsruby/config.rb +77 -4
  14. data/lib/dnsruby/deadline_queue.rb +75 -0
  15. data/lib/dnsruby/dnssec.rb +148 -45
  16. data/lib/dnsruby/ipv6.rb +45 -5
  17. data/lib/dnsruby/key_cache.rb +202 -35
  18. data/lib/dnsruby/message/decoder.rb +85 -19
  19. data/lib/dnsruby/message/encoder.rb +27 -16
  20. data/lib/dnsruby/message/header.rb +8 -12
  21. data/lib/dnsruby/message/message.rb +53 -22
  22. data/lib/dnsruby/message/section.rb +12 -34
  23. data/lib/dnsruby/name.rb +5 -2
  24. data/lib/dnsruby/packet_sender.rb +664 -207
  25. data/lib/dnsruby/presentation.rb +147 -0
  26. data/lib/dnsruby/query_control.rb +132 -0
  27. data/lib/dnsruby/recursor.rb +798 -341
  28. data/lib/dnsruby/resolver.rb +472 -176
  29. data/lib/dnsruby/resource/CAA.rb +67 -16
  30. data/lib/dnsruby/resource/DLV.rb +6 -0
  31. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  32. data/lib/dnsruby/resource/DS.rb +27 -28
  33. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  34. data/lib/dnsruby/resource/IN.rb +21 -2
  35. data/lib/dnsruby/resource/MX.rb +2 -2
  36. data/lib/dnsruby/resource/NSEC.rb +6 -10
  37. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  38. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  39. data/lib/dnsruby/resource/OPT.rb +2 -3
  40. data/lib/dnsruby/resource/RR.rb +108 -65
  41. data/lib/dnsruby/resource/RRSIG.rb +5 -5
  42. data/lib/dnsruby/resource/RRSet.rb +38 -22
  43. data/lib/dnsruby/resource/SRV.rb +2 -2
  44. data/lib/dnsruby/resource/SVCB.rb +775 -0
  45. data/lib/dnsruby/resource/TSIG.rb +266 -231
  46. data/lib/dnsruby/resource/TXT.rb +34 -42
  47. data/lib/dnsruby/resource/domain_name.rb +1 -1
  48. data/lib/dnsruby/resource/generic.rb +31 -0
  49. data/lib/dnsruby/resource/generic_cache.rb +384 -0
  50. data/lib/dnsruby/resource/generic_constants.rb +79 -0
  51. data/lib/dnsruby/select_thread.rb +986 -291
  52. data/lib/dnsruby/serial_time.rb +25 -0
  53. data/lib/dnsruby/single_resolver.rb +25 -6
  54. data/lib/dnsruby/single_verifier.rb +1620 -530
  55. data/lib/dnsruby/transport_clock.rb +9 -0
  56. data/lib/dnsruby/update.rb +8 -6
  57. data/lib/dnsruby/validation_budget.rb +143 -0
  58. data/lib/dnsruby/validator_thread.rb +139 -58
  59. data/lib/dnsruby/version.rb +1 -1
  60. data/lib/dnsruby/zone_reader.rb +196 -129
  61. data/lib/dnsruby/zone_transfer.rb +157 -25
  62. data/lib/dnsruby.rb +7 -2
  63. data/test/ci_watchdog.rb +33 -0
  64. data/test/fixtures/org-ds-2026-11-05.zone +5 -0
  65. data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
  66. data/test/fork_test_helper.rb +37 -0
  67. data/test/local_dns_server.rb +57 -0
  68. data/test/soak_server.rb +152 -0
  69. data/test/socket_helpers.rb +26 -0
  70. data/test/spec_helper.rb +8 -13
  71. data/test/tc_attempt_configuration.rb +137 -0
  72. data/test/tc_caa.rb +2 -2
  73. data/test/tc_cache_keys.rb +36 -0
  74. data/test/tc_cancelled_send_capacity.rb +158 -0
  75. data/test/tc_dnssec_alias_validation.rb +289 -0
  76. data/test/tc_dnssec_budget.rb +489 -0
  77. data/test/tc_dnssec_cache_bounds.rb +197 -0
  78. data/test/tc_dnssec_cache_lifetime.rb +615 -0
  79. data/test/tc_dnssec_denial_proofs.rb +285 -0
  80. data/test/tc_dnssec_recursion_limits.rb +186 -0
  81. data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
  82. data/test/tc_dnssec_trust_anchors.rb +286 -0
  83. data/test/tc_dnssec_trust_races.rb +147 -0
  84. data/test/tc_dnssec_wildcard_proofs.rb +249 -0
  85. data/test/tc_ds.rb +5 -5
  86. data/test/tc_edns_retry_regressions.rb +255 -0
  87. data/test/tc_escapedchars.rb +8 -0
  88. data/test/tc_final_review_fixes.rb +167 -0
  89. data/test/tc_final_transport_contracts.rb +104 -0
  90. data/test/tc_generic_cache.rb +852 -0
  91. data/test/tc_header.rb +26 -0
  92. data/test/tc_ipv6.rb +120 -0
  93. data/test/tc_key_cache_identity.rb +382 -0
  94. data/test/tc_malformed_tcp.rb +712 -0
  95. data/test/tc_name.rb +99 -5
  96. data/test/tc_nameserver_bootstrap.rb +322 -0
  97. data/test/tc_nsec.rb +4 -45
  98. data/test/tc_nsec3.rb +17 -1
  99. data/test/tc_pipeline_retirement.rb +358 -0
  100. data/test/tc_pipeline_retry.rb +462 -0
  101. data/test/tc_pipeline_send.rb +663 -0
  102. data/test/tc_protocol_review_fixes.rb +95 -0
  103. data/test/tc_query_control.rb +90 -0
  104. data/test/tc_record_codec_contracts.rb +107 -0
  105. data/test/tc_record_regressions.rb +225 -0
  106. data/test/tc_recur.rb +1 -0
  107. data/test/tc_recursor_address_fallback.rb +169 -0
  108. data/test/tc_recursor_limits.rb +139 -0
  109. data/test/tc_release_auxiliary_fixes.rb +288 -0
  110. data/test/tc_release_configuration_fixes.rb +612 -0
  111. data/test/tc_release_dnssec_completion.rb +405 -0
  112. data/test/tc_release_dnssec_fixes.rb +305 -0
  113. data/test/tc_release_negative_cache.rb +327 -0
  114. data/test/tc_release_recursor_cache.rb +212 -0
  115. data/test/tc_release_review_transport.rb +372 -0
  116. data/test/tc_release_review_transport_native.rb +56 -0
  117. data/test/tc_release_transport.rb +906 -0
  118. data/test/tc_release_transport_fixes.rb +196 -0
  119. data/test/tc_release_zone_defaults.rb +157 -0
  120. data/test/tc_release_zone_ttls.rb +39 -0
  121. data/test/tc_res_config.rb +53 -2
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +34 -19
  124. data/test/tc_resolver_concurrency.rb +383 -0
  125. data/test/tc_resolver_configuration_contracts.rb +31 -0
  126. data/test/tc_resolver_validation.rb +211 -0
  127. data/test/tc_review_cache.rb +163 -0
  128. data/test/tc_review_code_mapper.rb +279 -0
  129. data/test/tc_review_control_deadlines.rb +169 -0
  130. data/test/tc_review_followup_transport.rb +583 -0
  131. data/test/tc_review_packet_contracts.rb +369 -0
  132. data/test/tc_review_presentation.rb +213 -0
  133. data/test/tc_review_record_compatibility.rb +101 -0
  134. data/test/tc_review_transport_fixes.rb +195 -0
  135. data/test/tc_review_transport_native.rb +415 -0
  136. data/test/tc_review_validation_priming.rb +178 -0
  137. data/test/tc_rr-opt.rb +5 -2
  138. data/test/tc_rr-unknown.rb +19 -0
  139. data/test/tc_rr.rb +14 -0
  140. data/test/tc_rrset_grouping.rb +50 -0
  141. data/test/tc_rrset_review_fixes.rb +58 -0
  142. data/test/tc_rrsig.rb +21 -1
  143. data/test/tc_scoped_ipv6.rb +153 -0
  144. data/test/tc_scoped_ipv6_select.rb +157 -0
  145. data/test/tc_scoped_ipv6_transport.rb +329 -0
  146. data/test/tc_selector_wakeup.rb +42 -0
  147. data/test/tc_soak.rb +118 -196
  148. data/test/tc_socket_helpers.rb +90 -0
  149. data/test/tc_supporting_lookup_limits.rb +252 -0
  150. data/test/tc_svcb.rb +1043 -0
  151. data/test/tc_svcb_signed_target.rb +145 -0
  152. data/test/tc_svcb_transport.rb +120 -0
  153. data/test/tc_tcp.rb +69 -43
  154. data/test/tc_tcp_pipelining.rb +95 -204
  155. data/test/tc_transport_fork.rb +179 -0
  156. data/test/tc_tsig.rb +2 -2
  157. data/test/tc_tsig_api.rb +251 -0
  158. data/test/tc_tsig_rfc8945.rb +357 -0
  159. data/test/tc_v2_release_dnssec.rb +442 -0
  160. data/test/tc_v2_release_transport.rb +304 -0
  161. data/test/tc_v2_zone_transfer_native.rb +58 -0
  162. data/test/tc_zone_reader.rb +217 -1
  163. data/test/tcp_pipelining_server.rb +150 -0
  164. data/test/ts_offline.rb +77 -6
  165. data/test/ts_online.rb +2 -1
  166. metadata +127 -14
  167. data/.coveralls.yml +0 -2
@@ -0,0 +1,906 @@
1
+ require_relative 'tc_pipeline_send'
2
+
3
+ class TestReleaseTransport < Minitest::Test
4
+ def test_initial_connect_does_not_hold_resolver_lock_and_close_cancels_it
5
+ isolated(<<~'RUBY')
6
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
7
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false,
8
+ use_tcp: true, tcp_pipelining: true)
9
+ socket = FrameSocket.new
10
+ entered, release = Queue.new, Queue.new
11
+ resolver.single_resolvers.first.define_singleton_method(:tcp_pipeline_socket) do |_|
12
+ entered << true
13
+ release.pop
14
+ [socket, true]
15
+ end
16
+ queue = Queue.new
17
+ sending = Thread.new { resolver.send_async(query, queue, :slow) }
18
+ entered.pop
19
+ # This is the actual callback entry used by the global selector.
20
+ Timeout.timeout(1) { SELECTOR.send_tick_to_observers }
21
+ Timeout.timeout(1) { resolver.close }
22
+ raise 'close did not complete exactly once' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::OtherResolvError)
23
+ release << true
24
+ sending.value
25
+ drain_errors
26
+ raise 'cancelled connect sent bytes' unless socket.written.empty?
27
+ raise 'cancelled connect retained socket' unless socket.closed?
28
+ raise 'cancelled connect recreated query' unless K.class_variable_get(:@@query_hash).empty?
29
+ raise 'duplicate completion' unless queue.empty?
30
+ RUBY
31
+ end
32
+
33
+ def test_close_before_initial_dispatch_cancels_reserved_attempt
34
+ isolated(<<~'RUBY')
35
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
36
+ writer = resolver.single_resolvers.first
37
+ original = writer.method(:prepare_send_async)
38
+ entered, release = Queue.new, Queue.new
39
+ writer.define_singleton_method(:prepare_send_async) do |*args|
40
+ operation = original.call(*args)
41
+ -> { entered << true; release.pop; operation.call }
42
+ end
43
+ writer.define_singleton_method(:send_async) { |*| raise 'dispatched after close' }
44
+ queue = Queue.new
45
+ sending = Thread.new { resolver.send_async(query, queue, :cancelled) }
46
+ entered.pop
47
+ resolver.close
48
+ release << true
49
+ sending.value
50
+ raise 'missing close completion' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::OtherResolvError)
51
+ raise 'retained reservation' unless K.class_variable_get(:@@query_hash).empty?
52
+ RUBY
53
+ end
54
+
55
+ def test_nonpipeline_connects_use_independent_workers
56
+ isolated(<<~'RUBY')
57
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: false,
58
+ packet_timeout: 2, dnssec: false)
59
+ entered, release, connected = Queue.new, Queue.new, Queue.new
60
+ first_socket, second_socket = FrameSocket.new, FrameSocket.new
61
+ calls = 0
62
+ Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
63
+ calls += 1
64
+ if calls == 1
65
+ entered << true
66
+ release.pop
67
+ first_socket
68
+ else
69
+ connected << true
70
+ second_socket
71
+ end
72
+ end
73
+ K.class_variable_set(:@@select_thread, Thread.current)
74
+ first = query('slow.example.')
75
+ task1 = writer.send_dnsruby(first.encode, first, :first, Queue.new, true)
76
+ entered.pop
77
+ second = query('healthy.example.')
78
+ task2 = writer.send_dnsruby(second.encode, second, :second, Queue.new, true)
79
+ Timeout.timeout(1) { connected.pop; task2.value }
80
+ raise 'healthy independent send never wrote' if second_socket.written.empty?
81
+ release << true
82
+ task1.value
83
+ [:first, :second].each { |id| SELECTOR.remove_id(id) }
84
+ RUBY
85
+ end
86
+
87
+ def test_explicit_close_retires_idle_streams_but_allows_shared_queries_to_finish
88
+ isolated(<<~'RUBY')
89
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
90
+ socket = FrameSocket.new
91
+ writer = sender(socket)
92
+ writer.use_tcp = true
93
+ first, second = 2.times.map { Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false, use_tcp: true) }
94
+ [first, second].each { |resolver| resolver.single_resolvers = [writer] }
95
+ a, b = query('first.example.'), query('second.example.')
96
+ a.header.id, b.header.id = 123, 124
97
+ qa, qb = Queue.new, Queue.new
98
+ first.send_async(a, qa, :first)
99
+ second.send_async(b, qb, :second)
100
+ first.close
101
+ raise 'shared live query socket closed' if socket.closed?
102
+ raise 'retiring stream remains reusable' if SELECTOR.reusable_socket?(socket)
103
+ response = b.clone
104
+ response.header.qr = true
105
+ SELECTOR.process_message(response, response.encode, socket)
106
+ SELECTOR.send_queued_validation_responses
107
+ raise 'shared query failed' unless qb.size == 1 && qb.pop.last.nil?
108
+ raise 'retired stream stayed open' unless socket.closed?
109
+ assert_clean(socket)
110
+ second.close
111
+ raise 'first close did not complete once' unless qa.size == 1
112
+ RUBY
113
+ end
114
+
115
+ def test_pipeline_collision_rotates_stream_without_modifying_raw_signed_requests
116
+ isolated(<<~'RUBY')
117
+ sockets = []
118
+ Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
119
+ FrameSocket.new.tap do |socket|
120
+ socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
121
+ sockets << socket
122
+ end
123
+ end
124
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: true,
125
+ use_tcp: true, dnssec: false)
126
+ # Identical question and ID, but different TSIG identities. Question-only
127
+ # demultiplexing cannot safely choose between these on the same stream.
128
+ requests = ['one', 'two'].map do |key_name|
129
+ query('same.example.').tap do |request|
130
+ request.header.id = 12345
131
+ request.sign!(key_name, 'MTIzNA==')
132
+ request.send_raw = true
133
+ end
134
+ end
135
+ wires = requests.map(&:encode)
136
+ requests.each_with_index { |request, index| writer.send_async(request, Queue.new, index) }
137
+ raise 'ID collision reused the same stream' unless sockets.size == 2
138
+ sockets.each_with_index do |socket, index|
139
+ raise 'signed/raw bytes changed' unless socket.written == [wires[index].bytesize].pack('n') + wires[index]
140
+ end
141
+ raise 'old stream closed before completion' if sockets.first.closed?
142
+ SELECTOR.remove_id(0)
143
+ raise 'collision stream not retired after completion' unless sockets.first.closed?
144
+ SELECTOR.remove_id(1)
145
+ writer.close
146
+ sockets.each { |socket| raise 'idle stream retained' unless socket.closed?; assert_clean(socket) }
147
+ RUBY
148
+ end
149
+
150
+ def test_close_marks_a_shared_pending_connect_for_retirement
151
+ isolated(<<~'RUBY')
152
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
153
+ entered, release = Queue.new, Queue.new
154
+ sockets = []
155
+ Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
156
+ socket = FrameSocket.new
157
+ socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
158
+ sockets << socket
159
+ if sockets.size == 1
160
+ entered << true
161
+ release.pop
162
+ end
163
+ socket
164
+ end
165
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true, tcp_pipelining: true, dnssec: false)
166
+ first, second = 2.times.map { Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false) }
167
+ [first, second].each { |resolver| resolver.single_resolvers = [writer] }
168
+ queue = Queue.new
169
+ before = query('before-close.example.')
170
+ sending = Thread.new { second.send_async(before, queue, :before) }
171
+ entered.pop
172
+ first.close
173
+ release << true
174
+ sending.value
175
+ answer = before.clone
176
+ answer.header.qr = true
177
+ SELECTOR.process_message(answer, answer.encode, sockets.first)
178
+ SELECTOR.send_queued_validation_responses
179
+ raise 'shared pre-close query failed' unless queue.size == 1 && queue.pop.last.nil?
180
+ raise 'pending connect escaped retirement' unless sockets.first.closed?
181
+ assert_clean(sockets.first)
182
+ after = query('after-close.example.')
183
+ second.send_async(after, queue, :after)
184
+ answer = after.clone
185
+ answer.header.qr = true
186
+ SELECTOR.process_message(answer, answer.encode, sockets.last)
187
+ SELECTOR.send_queued_validation_responses
188
+ raise 'fresh query failed' unless queue.size == 1 && queue.pop.last.nil?
189
+ raise 'fresh connection is not reusable' unless sockets.size == 2 && SELECTOR.reusable_socket?(sockets.last)
190
+ second.close
191
+ sockets.each { |socket| assert_clean(socket) }
192
+ RUBY
193
+ end
194
+
195
+ def test_pre_close_reservation_does_not_retire_a_newer_shared_pipeline
196
+ isolated(<<~'RUBY')
197
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
198
+ sockets = []
199
+ Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
200
+ FrameSocket.new.tap do |socket|
201
+ socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
202
+ sockets << socket
203
+ end
204
+ end
205
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true, tcp_pipelining: true, dnssec: false)
206
+ original = writer.method(:prepare_send_async)
207
+ entered, release = Queue.new, Queue.new
208
+ paused = false
209
+ writer.define_singleton_method(:prepare_send_async) do |*args|
210
+ operation = original.call(*args)
211
+ unless paused
212
+ paused = true
213
+ reserved = operation
214
+ operation = -> { entered << true; release.pop; reserved.call }
215
+ end
216
+ operation
217
+ end
218
+ first, second = 2.times.map { Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false) }
219
+ [first, second].each { |resolver| resolver.single_resolvers = [writer] }
220
+ queue = Queue.new
221
+ old = query('reserved-before-close.example.')
222
+ sending = Thread.new { second.send_async(old, queue, :old) }
223
+ entered.pop
224
+ first.close
225
+ fresh = query('created-after-close.example.')
226
+ second.send_async(fresh, queue, :fresh)
227
+ SELECTOR.send_tick_to_observers
228
+ release << true
229
+ sending.value
230
+ raise 'retired reservation reused fresh stream' unless sockets.size == 2
231
+ [fresh, old].zip(sockets).each do |request, socket|
232
+ answer = request.clone
233
+ answer.header.qr = true
234
+ SELECTOR.process_message(answer, answer.encode, socket)
235
+ end
236
+ SELECTOR.send_queued_validation_responses
237
+ raise 'shared queries did not finish' unless queue.size == 2 && 2.times.all? { queue.pop.last.nil? }
238
+ raise 'old one-use connection retained' unless sockets.last.closed?
239
+ raise 'new pipeline was retired' unless SELECTOR.reusable_socket?(sockets.first)
240
+ second.close
241
+ sockets.each { |socket| assert_clean(socket) }
242
+ RUBY
243
+ end
244
+
245
+ def test_closed_pipeline_accounting_stays_bounded
246
+ isolated(<<~'RUBY')
247
+ Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| FrameSocket.new }
248
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: true)
249
+ 100.times do
250
+ socket, fresh = writer.tcp_pipeline_socket(0)
251
+ raise 'closed connection reused' unless fresh
252
+ socket.close
253
+ end
254
+ raise 'closed sockets accumulated' unless writer.instance_variable_get(:@use_counts).size == 1
255
+ RUBY
256
+ end
257
+
258
+ def test_single_resolver_constructor_and_replacement_keep_pipeline_options
259
+ isolated(<<~'RUBY')
260
+ options = {tcp_pipelining: true, tcp_pipelining_max_queries: 5, server: '127.0.0.1'}.freeze
261
+ resolver = Dnsruby::SingleResolver.new(options)
262
+ 2.times do
263
+ writer = resolver.single_resolvers.first
264
+ raise 'pipeline option not forwarded' unless writer.tcp_pipelining && writer.tcp_pipelining_max_queries == 5
265
+ resolver.server = '127.0.0.2'
266
+ end
267
+ defaults = Dnsruby::SingleResolver.new(server: '127.0.0.1')
268
+ raise 'incorrect pipeline defaults' unless defaults.tcp_pipelining == false && defaults.tcp_pipelining_max_queries == :infinite
269
+ RUBY
270
+ end
271
+
272
+ def test_packet_timeout_does_not_create_nil_sender_and_close_is_safe
273
+ isolated(<<~'RUBY')
274
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
275
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false, retry_times: 1)
276
+ writer = resolver.single_resolvers.first
277
+ writer.define_singleton_method(:send_async) { |*| }
278
+ queue = Queue.new
279
+ resolver.send_async(query, queue, :timeout)
280
+ K.class_variable_get(:@@timeouts).transform_values! { Dnsruby::TransportClock.now - 1 }
281
+ K.class_variable_get(:@@timeouts).each { |id, deadline| SELECTOR.instance_variable_get(:@packet_deadlines)[id] = deadline }
282
+ SELECTOR.process_timeouts
283
+ raise 'packet timeout was not delivered' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
284
+ raise 'last sender moved beyond end of list' unless resolver.single_resolvers == [writer]
285
+ resolver.close
286
+ # Be tolerant of legacy callers holding or supplying the exposed array.
287
+ resolver.single_resolvers << nil
288
+ resolver.close
289
+ RUBY
290
+ end
291
+
292
+ def test_late_success_timeout_and_error_do_not_restore_replaced_sender
293
+ isolated(<<~'RUBY')
294
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
295
+ [:success, :timeout, :error].each do |outcome|
296
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false, retry_times: 1)
297
+ writer = resolver.single_resolvers.first
298
+ writer.define_singleton_method(:send_async) { |*| }
299
+ queue = Queue.new
300
+ resolver.send_async(query, queue, outcome)
301
+ pending = K.class_variable_get(:@@query_hash).values.find { |settings| settings.single_resolver.equal?(writer) }
302
+ resolver.nameserver = '127.0.0.2'
303
+ replacement = resolver.single_resolvers.first
304
+ case outcome
305
+ when :success
306
+ answer = pending.query.clone
307
+ answer.header.qr = true
308
+ SELECTOR.push_response_for_query(pending, answer, pending.query, writer)
309
+ SELECTOR.send_queued_responses
310
+ SELECTOR.send_queued_validation_responses
311
+ when :timeout
312
+ K.class_variable_get(:@@timeouts)[pending.client_query_id] = Dnsruby::TransportClock.now - 1
313
+ SELECTOR.instance_variable_get(:@packet_deadlines)[pending.client_query_id] = K.class_variable_get(:@@timeouts)[pending.client_query_id]
314
+ SELECTOR.process_timeouts
315
+ when :error
316
+ SELECTOR.fail_query_if_current(pending.client_query_id, pending.client_queue, pending,
317
+ Dnsruby::OtherResolvError.new('old server failed'))
318
+ SELECTOR.send_queued_exceptions
319
+ end
320
+ raise "#{outcome}: no completion" unless queue.size == 1
321
+ result = queue.pop
322
+ raise 'successful old response lost' if outcome == :success && result.last
323
+ raise "#{outcome}: stale sender restored" unless resolver.single_resolvers == [replacement]
324
+ raise "#{outcome}: config changed" unless resolver.config.nameserver == ['127.0.0.2']
325
+ resolver.close
326
+ end
327
+ RUBY
328
+ end
329
+
330
+ def test_timed_out_stream_cannot_accept_new_queries_and_existing_peers_drain
331
+ isolated(<<~'RUBY')
332
+ socket = FrameSocket.new
333
+ writer = sender(socket)
334
+ expired = settings(socket, :expired, query('expired.example.'), writer)
335
+ peer = settings(socket, :peer, query('peer.example.'), writer)
336
+ expired.query.header.id, peer.query.header.id = 7, 8
337
+ [expired, peer].each { |item| SELECTOR.add_to_select(item) }
338
+ # Both a partial prefix and a partial body must remain owned by this
339
+ # retiring stream until its already outstanding queries finish.
340
+ K.class_variable_get(:@@tcp_buffers)[socket] = [+"\0", 2, true]
341
+ K.class_variable_get(:@@timeouts)[:expired] = Dnsruby::TransportClock.now - 1
342
+ SELECTOR.instance_variable_get(:@packet_deadlines)[:expired] = K.class_variable_get(:@@timeouts)[:expired]
343
+ SELECTOR.process_timeouts
344
+ raise 'timeout missing' unless expired.client_queue.size == 1 &&
345
+ expired.client_queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
346
+ raise 'timeout killed unrelated peer' if socket.closed?
347
+ raise 'timed-out stream reusable' if SELECTOR.reusable_socket?(socket)
348
+ raise 'peer ownership lost' unless SELECTOR.query_settings_for_id(:peer).equal?(peer)
349
+ # Once the late frame has been consumed, a valid peer response can finish.
350
+ K.class_variable_get(:@@tcp_buffers).delete(socket)
351
+ response = peer.query.clone
352
+ response.header.qr = true
353
+ SELECTOR.process_message(response, response.encode, socket)
354
+ SELECTOR.send_queued_validation_responses
355
+ raise 'peer response lost' unless peer.client_queue.size == 2 &&
356
+ 2.times.all? { peer.client_queue.pop.last.nil? }
357
+ raise 'drained stream stayed open' unless socket.closed?
358
+ assert_clean(socket)
359
+ RUBY
360
+ end
361
+
362
+ def test_retirement_between_socket_selection_and_registration_retries_admission
363
+ isolated(<<~'RUBY')
364
+ [true, false].product([0, 60_001, [60_001, 60_002]]).each do |keep_peer, ports|
365
+ sockets, connection_ports = [], []
366
+ Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
367
+ connection_ports << args[3]
368
+ FrameSocket.new.tap do |socket|
369
+ socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
370
+ sockets << socket
371
+ end
372
+ end
373
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true,
374
+ tcp_pipelining: true, dnssec: false, packet_timeout: 5, src_port: ports)
375
+ requests = 3.times.map { |i| query("admission#{i}.example.").tap { |q| q.header.id = i } }
376
+ queues = 3.times.map { Queue.new }
377
+ writer.send_async(requests[0], queues[0], 0)
378
+ writer.send_async(requests[1], queues[1], 1) if keep_peer
379
+ old = sockets.first
380
+ selected, release = Queue.new, Queue.new
381
+ choose = writer.method(:tcp_pipeline_socket)
382
+ extra_retirements = 2
383
+ writer.define_singleton_method(:tcp_pipeline_socket) do |port|
384
+ result = choose.call(port)
385
+ if result.first.equal?(old)
386
+ selected << true
387
+ release.pop
388
+ elsif extra_retirements > 0
389
+ # Repeated admission races must not consume the source-port pool.
390
+ extra_retirements -= 1
391
+ SELECTOR.retire_socket(result.first)
392
+ end
393
+ result
394
+ end
395
+ sending = Thread.new { writer.send_async(requests[2], queues[2], 2) }
396
+ selected.pop
397
+ pending = SELECTOR.query_settings_for_id(2)
398
+ original_deadline = pending.endtime
399
+ old_size = old.written.bytesize
400
+ K.class_variable_get(:@@tcp_buffers)[old] = [+'partial', 1000, false]
401
+ K.class_variable_get(:@@timeouts)[0] = Dnsruby::TransportClock.now - 1
402
+ SELECTOR.instance_variable_get(:@packet_deadlines)[0] = K.class_variable_get(:@@timeouts)[0]
403
+ SELECTOR.process_timeouts
404
+ raise 'incorrect old stream lifetime' unless old.closed? == !keep_peer
405
+ release << true
406
+ sending.value
407
+ current = SELECTOR.query_settings_for_id(2)
408
+ raise 'registration used retired or closed stream' unless sockets.size == 4 &&
409
+ current.socket.equal?(sockets.last) && old.written.bytesize == old_size
410
+ allowed_ports = ports == 0 ? (50_000..65_535) : Array(ports)
411
+ raise 'admission retry escaped the configured source ports' unless
412
+ connection_ports.all? { |port| allowed_ports.include?(port) }
413
+ raise 'admission retry replaced ownership or deadline' unless
414
+ current.send_token.equal?(pending.send_token) && current.endtime == original_deadline
415
+ raise 'admission retry delivered a premature error' unless queues[2].empty?
416
+ raise 'retirement lost existing peer' if keep_peer && !SELECTOR.query_settings_for_id(1)
417
+ [2, (1 if keep_peer)].compact.each do |id|
418
+ item = SELECTOR.query_settings_for_id(id)
419
+ response = requests[id].clone
420
+ response.header.qr = true
421
+ SELECTOR.process_message(response, response.encode, item.socket)
422
+ end
423
+ SELECTOR.send_queued_validation_responses
424
+ raise 'fresh query did not complete' unless queues[2].size == 2 &&
425
+ 2.times.all? { queues[2].pop.last.nil? }
426
+ writer.close
427
+ sockets.each { |socket| raise 'socket retained' unless socket.closed?; assert_clean(socket) }
428
+ end
429
+ RUBY
430
+ end
431
+
432
+ def test_timeout_without_partial_bytes_rotates_before_reusing_a_header_id
433
+ isolated(<<~'RUBY')
434
+ sockets = []
435
+ Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
436
+ FrameSocket.new.tap do |socket|
437
+ socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
438
+ sockets << socket
439
+ end
440
+ end
441
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true,
442
+ tcp_pipelining: true, dnssec: false)
443
+ request = query
444
+ request.header.id = 123
445
+ writer.send_async(request, Queue.new, :old)
446
+ K.class_variable_get(:@@timeouts)[:old] = Dnsruby::TransportClock.now - 1
447
+ SELECTOR.instance_variable_get(:@packet_deadlines)[:old] = K.class_variable_get(:@@timeouts)[:old]
448
+ SELECTOR.process_timeouts
449
+ writer.send_async(request, Queue.new, :new)
450
+ raise 'late reply could reach reused ID' unless sockets.size == 2 && sockets.first.closed?
451
+ SELECTOR.remove_id(:new)
452
+ writer.close
453
+ sockets.each { |socket| assert_clean(socket) }
454
+ RUBY
455
+ end
456
+
457
+ def test_removed_sender_retries_use_connections_that_close_after_the_answer
458
+ isolated(<<~'RUBY')
459
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
460
+ sockets = []
461
+ Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
462
+ FrameSocket.new.tap do |socket|
463
+ socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
464
+ sockets << socket
465
+ end
466
+ end
467
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', use_tcp: true,
468
+ tcp_pipelining: true, dnssec: false, retry_times: 2, retry_delay: 10)
469
+ queue, request = Queue.new, query
470
+ resolver.send_async(request, queue, :retry)
471
+ old_writer = resolver.single_resolvers.first
472
+ resolver.nameserver = '127.0.0.2'
473
+ original = K.class_variable_get(:@@query_hash).values.first
474
+ K.class_variable_get(:@@timeouts)[original.client_query_id] = Dnsruby::TransportClock.now - 1
475
+ SELECTOR.instance_variable_get(:@packet_deadlines)[original.client_query_id] = K.class_variable_get(:@@timeouts)[original.client_query_id]
476
+ SELECTOR.process_timeouts
477
+ engine = resolver.instance_variable_get(:@resolver_ruby)
478
+ retry_plan = engine.instance_variable_get(:@timeouts)[:retry][1]
479
+ entry = retry_plan.values.first
480
+ raise 'original retry plan replaced' unless entry.first.equal?(old_writer)
481
+ retry_plan.clear
482
+ retry_plan[Dnsruby::TransportClock.now - 1] = entry
483
+ engine.send(:update_tick_deadline, :retry)
484
+ engine.tick
485
+ raise 'retry did not make a new connection' unless sockets.size == 2
486
+ response = request.clone
487
+ response.header.qr = true
488
+ SELECTOR.process_message(response, response.encode, sockets.last)
489
+ SELECTOR.send_queued_validation_responses
490
+ raise 'retry response lost' unless queue.size == 1 && queue.pop.last.nil?
491
+ raise 'old retry left an idle socket' unless sockets.all?(&:closed?)
492
+ resolver.close
493
+ sockets.each { |socket| assert_clean(socket) }
494
+ RUBY
495
+ end
496
+
497
+ def test_pipeline_capacity_changes_rotate_connections_and_preserve_every_peer
498
+ isolated(<<~'RUBY')
499
+ [[2, 3], [3, 2], [:infinite, 2], [2, :infinite], [3, 1]].each do |before, after|
500
+ sockets = []
501
+ Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
502
+ FrameSocket.new.tap do |socket|
503
+ socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
504
+ sockets << socket
505
+ end
506
+ end
507
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true,
508
+ tcp_pipelining: true, tcp_pipelining_max_queries: before, dnssec: false)
509
+ requests = 3.times.map { |i| query("capacity#{i}.example.").tap { |q| q.header.id = i } }
510
+ queues = 3.times.map { Queue.new }
511
+ writer.send_async(requests[0], queues[0], 0)
512
+ original = sockets.first
513
+ writer.tcp_pipelining_max_queries = after
514
+ [1, 2].each { |i| writer.send_async(requests[i], queues[i], i) }
515
+ raise 'old connection reused after capacity change' unless sockets.size >= 2 &&
516
+ !SELECTOR.reusable_socket?(original) && !original.closed?
517
+ # Out-of-order responses also exercise draining the retired generation.
518
+ [2, 0, 1].each do |id|
519
+ current = SELECTOR.query_settings_for_id(id)
520
+ response = requests[id].clone
521
+ response.header.qr = true
522
+ SELECTOR.process_message(response, response.encode, current.socket)
523
+ end
524
+ SELECTOR.send_queued_validation_responses
525
+ raise 'capacity change lost a response' unless queues.all? do |queue|
526
+ queue.size == 2 && 2.times.all? { queue.pop.last.nil? }
527
+ end
528
+ writer.close
529
+ sockets.each { |socket| raise 'socket open' unless socket.closed?; assert_clean(socket) }
530
+ end
531
+ RUBY
532
+ end
533
+
534
+ def test_tcp_read_waits_only_for_transient_errors_and_retires_fatal_errors
535
+ isolated(<<~'RUBY')
536
+ [IO::EAGAINWaitReadable, Errno::EINTR, Errno::ECONNRESET, IOError].each do |kind|
537
+ socket = FrameSocket.new
538
+ requests = 2.times.map { |i| settings(socket, i) }
539
+ requests.each { |item| SELECTOR.add_to_select(item) }
540
+ socket.define_singleton_method(:recv_nonblock) { |_| raise kind, 'injected read failure' }
541
+ raise 'unexpected bytes' unless SELECTOR.tcp_read(socket) == false
542
+ if [IO::EAGAINWaitReadable, Errno::EINTR].include?(kind)
543
+ raise 'transient read failed owners' if socket.closed? || requests.any? { |s| !s.client_queue.empty? }
544
+ requests.each { |item| SELECTOR.remove_id(item.client_query_id) }
545
+ else
546
+ raise 'fatal read left stream open' unless socket.closed?
547
+ requests.each do |item|
548
+ raise 'fatal read did not fail each owner once' unless item.client_queue.size == 1 &&
549
+ item.client_queue.pop.last.is_a?(Dnsruby::SocketEofResolvError)
550
+ end
551
+ assert_clean(socket)
552
+ end
553
+ end
554
+ RUBY
555
+ end
556
+
557
+ def test_random_source_port_collision_retries_before_registering_a_socket
558
+ isolated(<<~'RUBY')
559
+ [true, false].each do |pipeline|
560
+ calls = 0
561
+ socket = FrameSocket.new
562
+ Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
563
+ calls += 1
564
+ raise Errno::EADDRINUSE if calls == 1
565
+ socket
566
+ end
567
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true,
568
+ tcp_pipelining: pipeline, dnssec: false)
569
+ writer.define_singleton_method(:get_next_src_port) { 40000 + calls }
570
+ queue, request = Queue.new, query
571
+ writer.send_async(request, queue, :collision)
572
+ raise 'source port collision did not retry' unless calls == 2
573
+ raise 'retry did not send the complete frame' unless
574
+ socket.written == [request.encode.bytesize].pack('n') + request.encode
575
+ drain_errors
576
+ raise 'source port collision delivered an error' unless queue.empty?
577
+ SELECTOR.remove_id(:collision)
578
+ writer.close
579
+ assert_clean(socket)
580
+ end
581
+ RUBY
582
+ end
583
+
584
+ def test_random_udp_source_port_collision_retries_with_a_new_socket
585
+ isolated(<<~'RUBY')
586
+ sockets = []
587
+ UDPSocket.define_singleton_method(:new) do |*|
588
+ FrameSocket.new.tap do |socket|
589
+ first = sockets.empty?
590
+ socket.define_singleton_method(:bind) { |*| raise Errno::EADDRINUSE if first }
591
+ socket.define_singleton_method(:connect) { |*| }
592
+ sockets << socket
593
+ end
594
+ end
595
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false)
596
+ writer.define_singleton_method(:get_next_src_port) { 40000 + sockets.size }
597
+ queue, request = Queue.new, query
598
+ writer.send_async(request, queue, :collision)
599
+ raise 'failed UDP bind was not closed and retried' unless
600
+ sockets.size == 2 && sockets.first.closed? && sockets.first.written.empty?
601
+ raise 'UDP retry did not send the query' unless sockets.last.written == request.encode
602
+ drain_errors
603
+ raise 'UDP source port collision delivered an error' unless queue.empty?
604
+ SELECTOR.remove_id(:collision)
605
+ writer.close
606
+ sockets.each { |socket| assert_clean(socket) }
607
+ RUBY
608
+ end
609
+
610
+ def test_connect_timeouts_preserve_scheduled_retries_for_both_tcp_modes
611
+ isolated(<<~'RUBY')
612
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
613
+ [true, false].each do |pipeline|
614
+ calls = 0
615
+ Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| calls += 1; raise Errno::ETIMEDOUT }
616
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', use_tcp: true,
617
+ tcp_pipelining: pipeline, dnssec: false, retry_times: 2, retry_delay: 10)
618
+ queue = Queue.new
619
+ resolver.send_async(query, queue, :connect)
620
+ drain_errors
621
+ engine = resolver.instance_variable_get(:@resolver_ruby)
622
+ plan = engine.instance_variable_get(:@timeouts)[:connect][1]
623
+ raise 'connect timeout discarded scheduled retry' if plan.empty?
624
+ entry = plan.values.first
625
+ plan.clear
626
+ plan[Dnsruby::TransportClock.now - 1] = entry
627
+ engine.send(:update_tick_deadline, :connect)
628
+ engine.tick
629
+ drain_errors
630
+ raise 'connect timeout did not retry' unless calls == 2
631
+ raise 'wrong final connect error' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
632
+ resolver.close
633
+ end
634
+ RUBY
635
+ end
636
+
637
+ def test_worker_exceptions_reach_owner_without_reading_task_value
638
+ isolated(<<~'RUBY')
639
+ writer = sender(FrameSocket.new)
640
+ failure = RuntimeError.new('unexpected worker failure')
641
+ writer.define_singleton_method(:send_dnsruby_with_socket) { |*| raise failure }
642
+ K.class_variable_set(:@@select_thread, Thread.current)
643
+ queue = Queue.new
644
+ writer.send_dnsruby(query.encode, query, :worker, queue, true)
645
+ Timeout.timeout(1) do
646
+ Thread.pass while SELECTOR.query_settings_for_id(:worker)
647
+ end
648
+ drain_errors
649
+ raise 'worker error was swallowed' unless queue.size == 1 && queue.pop.last.equal?(failure)
650
+ raise 'worker error retained deadline' if K.class_variable_get(:@@timeouts).key?(:worker)
651
+ RUBY
652
+ end
653
+
654
+ def test_worker_failure_after_connect_releases_an_unregistered_socket
655
+ isolated(<<~'RUBY')
656
+ socket = FrameSocket.new
657
+ Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| socket }
658
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true, dnssec: false)
659
+ failure = RuntimeError.new('unexpected registration failure')
660
+ construct = K::QuerySettings.method(:new)
661
+ K::QuerySettings.define_singleton_method(:new) do |*args|
662
+ raise failure if args[5] # A connected socket, after the initial reservation.
663
+ construct.call(*args)
664
+ end
665
+ K.class_variable_set(:@@select_thread, Thread.current)
666
+ queue = Queue.new
667
+ writer.send_dnsruby(query.encode, query, :worker, queue, true)
668
+ Timeout.timeout(1) do
669
+ Thread.pass until socket.closed?
670
+ end
671
+ drain_errors
672
+ raise 'worker error was swallowed' unless queue.size == 1 && queue.pop.last.equal?(failure)
673
+ raise 'worker retained ownership' if SELECTOR.query_settings_for_id(:worker)
674
+ assert_clean(socket)
675
+ RUBY
676
+ end
677
+
678
+ def test_wall_clock_jumps_do_not_change_query_packet_retry_or_write_deadlines
679
+ isolated(<<~'RUBY')
680
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
681
+ clock = 1000.0
682
+ Dnsruby::TransportClock.define_singleton_method(:now) { clock }
683
+ original_time = Time.method(:now)
684
+ socket = FrameSocket.new
685
+ writer = sender(socket)
686
+ writer.use_tcp = true
687
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', use_tcp: true,
688
+ dnssec: false, packet_timeout: 5, query_timeout: 10, retry_times: 2, retry_delay: 3)
689
+ resolver.single_resolvers = [writer]
690
+ queue = Queue.new
691
+ resolver.send_async(query, queue, :clock)
692
+ # Initial retry keys precede tick only after monotonic time advances.
693
+ clock += 0.1
694
+ engine = resolver.instance_variable_get(:@resolver_ruby)
695
+ engine.tick
696
+ pending = K.class_variable_get(:@@query_hash).values.first
697
+ raise 'packet deadline used wall time' unless pending.endtime < 1010
698
+ engine_deadline, retries = engine.instance_variable_get(:@timeouts)[:clock]
699
+ raise 'query or retry deadline used wall time' unless engine_deadline == 1010 && retries.keys.all? { |t| t < 1010 }
700
+ [-86400, 86400].each do |jump|
701
+ Time.define_singleton_method(:now) { original_time.call + jump }
702
+ SELECTOR.process_timeouts
703
+ raise 'wall jump expired query' unless queue.empty?
704
+ extra = FrameSocket.new
705
+ writer.write_tcp_frame(extra, 'abc', clock + 1, K::SendToken.new)
706
+ raise 'wall jump prevented write' unless extra.written == "\0\3abc".b
707
+ end
708
+ clock = 1011
709
+ engine.tick
710
+ raise 'monotonic query deadline ignored' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
711
+ resolver.close
712
+ RUBY
713
+ end
714
+
715
+ def test_udp_close_and_timeout_between_ownership_check_and_write_cancel_the_datagram
716
+ isolated(<<~'RUBY')
717
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
718
+ lookup = SELECTOR.method(:query_settings_for_id)
719
+ [:close, :timeout].each do |cancellation|
720
+ socket = FrameSocket.new
721
+ connected, paused = false, false
722
+ socket.define_singleton_method(:bind) { |*| }
723
+ socket.define_singleton_method(:connect) { |*| connected = true }
724
+ UDPSocket.define_singleton_method(:new) { |*| socket }
725
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false,
726
+ udp_size: 512, packet_timeout: 1)
727
+ entered, release = Queue.new, Queue.new
728
+ SELECTOR.define_singleton_method(:query_settings_for_id) do |id|
729
+ current = lookup.call(id)
730
+ if connected && !paused
731
+ paused = true
732
+ entered << true
733
+ release.pop
734
+ end
735
+ current
736
+ end
737
+ queue = Queue.new
738
+ sending = Thread.new { resolver.send_async(query, queue, cancellation) }
739
+ entered.pop
740
+ if cancellation == :close
741
+ resolver.close
742
+ else
743
+ deadlines = K.class_variable_get(:@@timeouts)
744
+ deadlines.keys.each { |id| deadlines[id] = Dnsruby::TransportClock.now - 1 }
745
+ deadlines.each { |id, deadline| SELECTOR.instance_variable_get(:@packet_deadlines)[id] = deadline }
746
+ SELECTOR.process_timeouts
747
+ end
748
+ expected = cancellation == :close ? Dnsruby::OtherResolvError : Dnsruby::ResolvTimeout
749
+ raise 'missing cancellation completion' unless queue.size == 1 && queue.pop.last.is_a?(expected)
750
+ release << true
751
+ sending.value
752
+ drain_errors
753
+ raise 'cancelled UDP request sent bytes' unless socket.written.empty?
754
+ raise 'cancelled UDP socket leaked' unless socket.closed?
755
+ raise 'duplicate cancellation completion' unless queue.empty?
756
+ raise 'cancelled reservation retained' unless K.class_variable_get(:@@query_hash).empty?
757
+ raise 'cancelled queue retained' unless K.class_variable_get(:@@queries_by_queue).empty?
758
+ resolver.close
759
+ end
760
+ RUBY
761
+ end
762
+
763
+ def test_congested_udp_write_leaves_selector_and_remains_cancellable
764
+ isolated(<<~'RUBY')
765
+ socket = FrameSocket.new
766
+ socket.define_singleton_method(:bind) { |*| }
767
+ socket.define_singleton_method(:connect) { |*| }
768
+ socket.define_singleton_method(:write_nonblock) { |*| raise IO::EAGAINWaitWritable }
769
+ UDPSocket.define_singleton_method(:new) { |*| socket }
770
+ entered, release = Queue.new, Queue.new
771
+ IO.define_singleton_method(:select) do |readers, writers, *|
772
+ raise 'UDP wait blocked selector' if SELECTOR.selector_thread?
773
+ raise 'wrong writable socket' unless readers.nil? && writers == [socket]
774
+ entered << true
775
+ release.pop
776
+ [[], [socket], []]
777
+ end
778
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false)
779
+ K.class_variable_set(:@@select_thread, Thread.current)
780
+ request, queue = query, Queue.new
781
+ task = writer.send_dnsruby(request.encode, request, :congested, queue, false)
782
+ entered.pop
783
+ SELECTOR.remove_id(:congested)
784
+ release << true
785
+ task.value
786
+ drain_errors
787
+ raise 'cancelled congested UDP request sent bytes' unless socket.written.empty?
788
+ raise 'cancelled congested UDP socket leaked' unless socket.closed?
789
+ raise 'cancelled congested UDP reported another error' unless queue.empty?
790
+ RUBY
791
+ end
792
+
793
+ def test_udp_writable_wait_has_a_packet_deadline_and_never_splits_datagrams
794
+ isolated(<<~'RUBY')
795
+ socket = FrameSocket.new
796
+ socket.define_singleton_method(:bind) { |*| }
797
+ socket.define_singleton_method(:connect) { |*| }
798
+ socket.define_singleton_method(:write_nonblock) { |*| raise IO::EAGAINWaitWritable }
799
+ UDPSocket.define_singleton_method(:new) { |*| socket }
800
+ IO.define_singleton_method(:select) do |readers, writers, _, timeout|
801
+ raise 'missing packet deadline' unless timeout > 0 && timeout <= 1
802
+ nil
803
+ end
804
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', packet_timeout: 1, dnssec: false)
805
+ queue, request = Queue.new, query
806
+ writer.send_async(request, queue, :blocked)
807
+ drain_errors
808
+ raise 'UDP writable wait did not time out exactly once' unless
809
+ queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
810
+ raise 'UDP timeout retained socket or ownership' unless
811
+ socket.closed? && K.class_variable_get(:@@query_hash).empty?
812
+
813
+ partial = FrameSocket.new
814
+ partial.limit = 1
815
+ begin
816
+ writer.write_udp_packet(partial, 'abc', Dnsruby::TransportClock.now + 1, K::SendToken.new, true)
817
+ raise 'partial datagram accepted'
818
+ rescue IOError => error
819
+ raise 'wrong partial datagram error' unless error.message == 'Incomplete UDP datagram write'
820
+ end
821
+ raise 'UDP datagram was split across writes' unless partial.written == 'a'
822
+ RUBY
823
+ end
824
+
825
+ def test_queue_attempt_index_tracks_replacement_registration_and_cancellation
826
+ isolated(<<~'RUBY')
827
+ first, second = Queue.new, Queue.new
828
+ owner, replacement_owner = Object.new, Object.new
829
+ SELECTOR.add_observer(first, owner)
830
+ SELECTOR.add_observer(first, owner)
831
+ SELECTOR.add_observer(second, owner)
832
+ counts = K.class_variable_get(:@@observer_queue_counts)
833
+ raise 'duplicate observer registration counted twice' unless counts.length == 1 && counts[owner] == 2
834
+ SELECTOR.add_observer(first, replacement_owner)
835
+ raise 'observer replacement lost its counts' unless counts.length == 2 && counts[owner] == 1 && counts[replacement_owner] == 1
836
+
837
+ pending = settings(FrameSocket.new, :moved)
838
+ pending.socket, pending.client_queue = nil, first
839
+ SELECTOR.reserve_send(pending)
840
+ replacement = settings(FrameSocket.new, :moved)
841
+ replacement.socket, replacement.client_queue = nil, second
842
+ SELECTOR.reserve_send(replacement, pending)
843
+ index = K.class_variable_get(:@@queries_by_queue)
844
+ raise 'replacement retained old queue membership' if index.key?(first)
845
+ raise 'replacement missing new queue membership' unless index[second] == Set[:moved]
846
+ raise 'stale claim consumed replacement' if SELECTOR.claim_query(pending)
847
+
848
+ live = settings(FrameSocket.new, :live)
849
+ live.client_queue = first
850
+ SELECTOR.add_to_select(live)
851
+ queued = settings(FrameSocket.new, :queued)
852
+ queued.socket, queued.client_queue = nil, first
853
+ SELECTOR.reserve_send(queued)
854
+ SELECTOR.cancel_queries_for_queue(first)
855
+ raise 'queue cancellation retained attempts' if index.key?(first)
856
+ raise 'queue cancellation removed another query' unless SELECTOR.query_settings_for_id(:moved).equal?(replacement)
857
+ raise 'queue cancellation removed the observer' unless
858
+ K.class_variable_get(:@@observers)[first].equal?(replacement_owner) && counts[replacement_owner] == 1
859
+ raise 'queue cancellation delivered an error' unless first.empty?
860
+ raise 'queue cancellation left a reusable stream' unless live.socket.closed?
861
+
862
+ closed = settings(FrameSocket.new, :moved)
863
+ closed.client_queue, closed.send_token = second, replacement.send_token
864
+ closed.is_persistent_socket = false
865
+ closed.socket.close
866
+ raise 'closed socket registered' if SELECTOR.register_query_if_current(closed, replacement)
867
+ raise 'failed registration retained queue membership' unless index.empty?
868
+ SELECTOR.remove_observer(first, replacement_owner)
869
+ SELECTOR.remove_observer(second, owner)
870
+ raise 'completed observers retained counts or ticks' unless counts.empty? && K.class_variable_get(:@@tick_observers).empty?
871
+ RUBY
872
+ end
873
+
874
+ def test_bulk_cancellation_does_not_scan_global_queries_or_observers
875
+ isolated(<<~'RUBY')
876
+ owner = Object.new
877
+ queues = 2000.times.map { Queue.new }
878
+ queues.each_with_index do |queue, id|
879
+ SELECTOR.add_observer(queue, owner)
880
+ pending = K::QuerySettings.new(nil, nil, false, queue, id, nil, nil, nil,
881
+ Dnsruby::TransportClock.now + 60, 512, nil)
882
+ SELECTOR.reserve_send(pending)
883
+ end
884
+ queries = K.class_variable_get(:@@query_hash)
885
+ observers = K.class_variable_get(:@@observers)
886
+ [queries, observers].each do |table|
887
+ [:values, :each, :each_value].each do |method|
888
+ table.define_singleton_method(method) { |*| raise 'global ownership scan during cancellation' }
889
+ end
890
+ end
891
+ queues.each { |queue| SELECTOR.remove_observer(queue, owner) }
892
+ raise 'bulk cancellation retained state' unless queries.empty? && observers.empty? &&
893
+ K.class_variable_get(:@@queries_by_queue).empty? && K.class_variable_get(:@@observer_queue_counts).empty?
894
+ RUBY
895
+ end
896
+
897
+ private
898
+
899
+ def isolated(body)
900
+ options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
901
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
902
+ File.expand_path('../lib', __dir__), '-e', "#{TestPipelineSend::FIXTURE}\nTimeout.timeout(10) do\n#{body}\nend")
903
+ assert(status.success?, "#{stdout}\n#{stderr}")
904
+ assert_empty(stderr)
905
+ end
906
+ end