dnsruby 1.74.0 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -4
- data/DNSSEC +3 -2
- data/EXAMPLES +1 -1
- data/README.md +370 -64
- data/RELEASE_NOTES.md +302 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/demo/rubydig.rb +0 -2
- data/dnsruby.gemspec +5 -4
- data/lib/dnsruby/cache.rb +279 -47
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +75 -4
- data/lib/dnsruby/deadline_queue.rb +75 -0
- data/lib/dnsruby/dnssec.rb +148 -45
- data/lib/dnsruby/ipv6.rb +45 -5
- data/lib/dnsruby/key_cache.rb +202 -35
- data/lib/dnsruby/message/decoder.rb +85 -19
- data/lib/dnsruby/message/encoder.rb +27 -16
- data/lib/dnsruby/message/header.rb +8 -12
- data/lib/dnsruby/message/message.rb +53 -22
- data/lib/dnsruby/message/section.rb +12 -34
- data/lib/dnsruby/name.rb +5 -2
- data/lib/dnsruby/packet_sender.rb +645 -203
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +795 -339
- data/lib/dnsruby/resolver.rb +468 -186
- data/lib/dnsruby/resource/CAA.rb +67 -16
- data/lib/dnsruby/resource/DLV.rb +6 -0
- data/lib/dnsruby/resource/DNSKEY.rb +108 -16
- data/lib/dnsruby/resource/DS.rb +27 -28
- data/lib/dnsruby/resource/HTTPS.rb +16 -0
- data/lib/dnsruby/resource/IN.rb +19 -0
- data/lib/dnsruby/resource/MX.rb +2 -2
- data/lib/dnsruby/resource/NSEC.rb +6 -10
- data/lib/dnsruby/resource/NSEC3.rb +55 -19
- data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
- data/lib/dnsruby/resource/RR.rb +108 -65
- data/lib/dnsruby/resource/RRSIG.rb +5 -5
- data/lib/dnsruby/resource/RRSet.rb +38 -22
- data/lib/dnsruby/resource/SRV.rb +2 -2
- data/lib/dnsruby/resource/SVCB.rb +775 -0
- data/lib/dnsruby/resource/TSIG.rb +266 -231
- data/lib/dnsruby/resource/TXT.rb +34 -42
- data/lib/dnsruby/resource/domain_name.rb +1 -1
- data/lib/dnsruby/resource/generic.rb +31 -0
- data/lib/dnsruby/resource/generic_cache.rb +384 -0
- data/lib/dnsruby/resource/generic_constants.rb +79 -0
- data/lib/dnsruby/select_thread.rb +979 -291
- data/lib/dnsruby/serial_time.rb +25 -0
- data/lib/dnsruby/single_resolver.rb +25 -6
- data/lib/dnsruby/single_verifier.rb +1620 -530
- data/lib/dnsruby/transport_clock.rb +9 -0
- data/lib/dnsruby/update.rb +8 -6
- data/lib/dnsruby/validation_budget.rb +143 -0
- data/lib/dnsruby/validator_thread.rb +139 -58
- data/lib/dnsruby/version.rb +1 -1
- data/lib/dnsruby/zone_reader.rb +196 -129
- data/lib/dnsruby/zone_transfer.rb +157 -25
- data/lib/dnsruby.rb +7 -2
- data/test/ci_watchdog.rb +33 -0
- data/test/fixtures/org-ds-2026-11-05.zone +5 -0
- data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
- data/test/fork_test_helper.rb +37 -0
- data/test/local_dns_server.rb +57 -0
- data/test/soak_server.rb +152 -0
- data/test/socket_helpers.rb +26 -0
- data/test/spec_helper.rb +8 -13
- data/test/tc_attempt_configuration.rb +137 -0
- data/test/tc_caa.rb +2 -2
- data/test/tc_cache_keys.rb +36 -0
- data/test/tc_cancelled_send_capacity.rb +158 -0
- data/test/tc_dnssec_alias_validation.rb +289 -0
- data/test/tc_dnssec_budget.rb +489 -0
- data/test/tc_dnssec_cache_bounds.rb +197 -0
- data/test/tc_dnssec_cache_lifetime.rb +615 -0
- data/test/tc_dnssec_denial_proofs.rb +285 -0
- data/test/tc_dnssec_recursion_limits.rb +186 -0
- data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
- data/test/tc_dnssec_trust_anchors.rb +286 -0
- data/test/tc_dnssec_trust_races.rb +147 -0
- data/test/tc_dnssec_wildcard_proofs.rb +249 -0
- data/test/tc_ds.rb +5 -5
- data/test/tc_edns_retry_regressions.rb +255 -0
- data/test/tc_escapedchars.rb +8 -0
- data/test/tc_final_review_fixes.rb +167 -0
- data/test/tc_final_transport_contracts.rb +104 -0
- data/test/tc_generic_cache.rb +852 -0
- data/test/tc_header.rb +26 -0
- data/test/tc_ipv6.rb +120 -0
- data/test/tc_key_cache_identity.rb +382 -0
- data/test/tc_malformed_tcp.rb +712 -0
- data/test/tc_name.rb +99 -5
- data/test/tc_nameserver_bootstrap.rb +322 -0
- data/test/tc_nsec.rb +4 -45
- data/test/tc_nsec3.rb +17 -1
- data/test/tc_pipeline_retirement.rb +358 -0
- data/test/tc_pipeline_retry.rb +462 -0
- data/test/tc_pipeline_send.rb +663 -0
- data/test/tc_protocol_review_fixes.rb +95 -0
- data/test/tc_query_control.rb +90 -0
- data/test/tc_record_codec_contracts.rb +107 -0
- data/test/tc_record_regressions.rb +225 -0
- data/test/tc_recur.rb +1 -0
- data/test/tc_recursor_address_fallback.rb +169 -0
- data/test/tc_recursor_limits.rb +139 -0
- data/test/tc_release_auxiliary_fixes.rb +288 -0
- data/test/tc_release_configuration_fixes.rb +612 -0
- data/test/tc_release_dnssec_completion.rb +405 -0
- data/test/tc_release_dnssec_fixes.rb +305 -0
- data/test/tc_release_negative_cache.rb +327 -0
- data/test/tc_release_recursor_cache.rb +212 -0
- data/test/tc_release_review_transport.rb +372 -0
- data/test/tc_release_review_transport_native.rb +56 -0
- data/test/tc_release_transport.rb +906 -0
- data/test/tc_release_transport_fixes.rb +196 -0
- data/test/tc_release_zone_defaults.rb +157 -0
- data/test/tc_release_zone_ttls.rb +39 -0
- data/test/tc_res_config.rb +42 -20
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +21 -19
- data/test/tc_resolver_concurrency.rb +383 -0
- data/test/tc_resolver_configuration_contracts.rb +31 -0
- data/test/tc_resolver_validation.rb +211 -0
- data/test/tc_review_cache.rb +163 -0
- data/test/tc_review_code_mapper.rb +279 -0
- data/test/tc_review_control_deadlines.rb +169 -0
- data/test/tc_review_followup_transport.rb +583 -0
- data/test/tc_review_packet_contracts.rb +369 -0
- data/test/tc_review_presentation.rb +213 -0
- data/test/tc_review_record_compatibility.rb +101 -0
- data/test/tc_review_transport_fixes.rb +195 -0
- data/test/tc_review_transport_native.rb +415 -0
- data/test/tc_review_validation_priming.rb +178 -0
- data/test/tc_rr-opt.rb +5 -2
- data/test/tc_rr-unknown.rb +19 -0
- data/test/tc_rr.rb +14 -0
- data/test/tc_rrset_grouping.rb +50 -0
- data/test/tc_rrset_review_fixes.rb +58 -0
- data/test/tc_rrsig.rb +21 -1
- data/test/tc_scoped_ipv6.rb +153 -0
- data/test/tc_scoped_ipv6_select.rb +157 -0
- data/test/tc_scoped_ipv6_transport.rb +329 -0
- data/test/tc_selector_wakeup.rb +42 -0
- data/test/tc_soak.rb +118 -196
- data/test/tc_socket_helpers.rb +90 -0
- data/test/tc_supporting_lookup_limits.rb +252 -0
- data/test/tc_svcb.rb +1043 -0
- data/test/tc_svcb_signed_target.rb +145 -0
- data/test/tc_svcb_transport.rb +120 -0
- data/test/tc_tcp.rb +69 -43
- data/test/tc_tcp_pipelining.rb +95 -204
- data/test/tc_transport_fork.rb +179 -0
- data/test/tc_tsig.rb +2 -2
- data/test/tc_tsig_api.rb +251 -0
- data/test/tc_tsig_rfc8945.rb +357 -0
- data/test/tc_v2_release_dnssec.rb +442 -0
- data/test/tc_v2_release_transport.rb +304 -0
- data/test/tc_v2_zone_transfer_native.rb +58 -0
- data/test/tc_zone_reader.rb +217 -1
- data/test/tcp_pipelining_server.rb +150 -0
- data/test/ts_offline.rb +77 -6
- data/test/ts_online.rb +2 -1
- metadata +105 -10
- data/.coveralls.yml +0 -2
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'open3'
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
|
|
5
|
+
class TestPipelineRetirement < Minitest::Test
|
|
6
|
+
# Selector tables are class variables. Every scenario owns a subprocess so
|
|
7
|
+
# neither its fake sockets nor its queued events can reach a live singleton.
|
|
8
|
+
SUBPROCESS_SETUP = <<~'RUBY'
|
|
9
|
+
require 'dnsruby'
|
|
10
|
+
require 'timeout'
|
|
11
|
+
|
|
12
|
+
class RetirementFixture
|
|
13
|
+
attr_reader :selector
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@klass = Dnsruby::SelectThread
|
|
17
|
+
{
|
|
18
|
+
mutex: Mutex.new, sockets: Set.new, query_hash: {}, queries_by_queue: {}.compare_by_identity, socket_hash: {}, timeouts: {},
|
|
19
|
+
socket_is_persistent: {}, socket_remaining_queries: {}, tcp_buffers: {},
|
|
20
|
+
queued_exceptions: [], queued_responses: [], queued_validation_responses: [],
|
|
21
|
+
observers: {}, observer_queue_counts: {}.compare_by_identity, tick_observers: [], select_thread: Thread.current
|
|
22
|
+
}.each { |name, value| @klass.class_variable_set("@@#{name}", value) }
|
|
23
|
+
@selector = @klass.send(:allocate)
|
|
24
|
+
@selector.define_singleton_method(:wake_up_select_thread) {}
|
|
25
|
+
@selector.define_singleton_method(:check_select_thread_synchronized) {}
|
|
26
|
+
@selector.define_singleton_method(:notify_queue_observers) { |*| }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def table(name)
|
|
30
|
+
@klass.class_variable_get("@@#{name}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def socket
|
|
34
|
+
closed = false
|
|
35
|
+
socket = Object.new
|
|
36
|
+
socket.define_singleton_method(:closed?) { closed }
|
|
37
|
+
socket.define_singleton_method(:close) { closed = true }
|
|
38
|
+
type = Struct.new(:data).new([Socket::SOCK_STREAM].pack('i'))
|
|
39
|
+
socket.define_singleton_method(:getsockopt) { |*| type }
|
|
40
|
+
socket
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def settings(socket, id: :query, persistent: true, endtime: Dnsruby::TransportClock.now + 60, register: true)
|
|
44
|
+
query = Dnsruby::Message.new('retirement.example.')
|
|
45
|
+
query.do_validation = query.do_caching = false
|
|
46
|
+
settings = @klass::QuerySettings.new(query.encode, query, false, Queue.new, id,
|
|
47
|
+
socket, '127.0.0.1', 53, endtime, 512, Dnsruby::PacketSender.allocate)
|
|
48
|
+
settings.is_persistent_socket = persistent
|
|
49
|
+
settings.tcp_pipelining_max_queries = :infinite
|
|
50
|
+
@selector.add_to_select(settings) if register
|
|
51
|
+
settings
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def response(settings)
|
|
55
|
+
settings.query.clone.tap { |message| message.header.qr = true }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def events(settings)
|
|
59
|
+
@selector.send_queued_exceptions
|
|
60
|
+
@selector.send_queued_validation_responses
|
|
61
|
+
events = []
|
|
62
|
+
events << settings.client_queue.pop(true) until settings.client_queue.empty?
|
|
63
|
+
events
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def assert_retired(socket)
|
|
67
|
+
raise 'socket still open' unless socket.closed?
|
|
68
|
+
raise 'retained socket' if table(:sockets).include?(socket)
|
|
69
|
+
[:socket_hash, :tcp_buffers, :socket_is_persistent, :socket_remaining_queries].each do |name|
|
|
70
|
+
raise "retained #{name}" if table(name).key?(socket)
|
|
71
|
+
end
|
|
72
|
+
raise 'retained socket registration' if table(:query_hash).values.any? { |settings| settings.socket == socket }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def assert_send_error(settings, error)
|
|
76
|
+
actual = events(settings)
|
|
77
|
+
expected = [[settings.client_query_id, Dnsruby::Resolver::EventType::ERROR, nil, error]]
|
|
78
|
+
raise "unexpected send failure deliveries: #{actual.inspect}" unless actual == expected
|
|
79
|
+
raise 'retained query' if table(:query_hash)[settings.client_query_id].equal?(settings)
|
|
80
|
+
raise 'retained timeout' if table(:timeouts).key?(settings.client_query_id)
|
|
81
|
+
assert_retired(settings.socket)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def assert_response(settings, response)
|
|
85
|
+
actual = events(settings)
|
|
86
|
+
expected = [Dnsruby::Resolver::EventType::RECEIVED, Dnsruby::Resolver::EventType::VALIDATED].map do |type|
|
|
87
|
+
[settings.client_query_id, type, response, nil]
|
|
88
|
+
end
|
|
89
|
+
raise "unexpected response deliveries: #{actual.inspect}" unless actual == expected
|
|
90
|
+
raise 'retained response registration' if table(:query_hash)[settings.client_query_id].equal?(settings)
|
|
91
|
+
raise 'retained response timeout' if table(:timeouts).key?(settings.client_query_id)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
RUBY
|
|
95
|
+
|
|
96
|
+
def test_closed_descriptor_during_select_keeps_the_same_worker_processing_queries
|
|
97
|
+
assert_isolated(<<~'RUBY')
|
|
98
|
+
fixture = RetirementFixture.new
|
|
99
|
+
selector = fixture.selector
|
|
100
|
+
klass = Dnsruby::SelectThread
|
|
101
|
+
broken = fixture.settings(fixture.socket, id: :broken)
|
|
102
|
+
healthy = fixture.settings(fixture.socket, id: :healthy, persistent: false)
|
|
103
|
+
response = fixture.response(healthy)
|
|
104
|
+
bytes = response.encode
|
|
105
|
+
incoming = [bytes.bytesize].pack('n') + bytes
|
|
106
|
+
healthy.socket.define_singleton_method(:recv_nonblock) { |length| incoming.slice!(0, length) }
|
|
107
|
+
wakeup = fixture.socket
|
|
108
|
+
fixture.table(:sockets) << wakeup
|
|
109
|
+
klass.class_variable_set(:@@wakeup_sockets, [wakeup, wakeup])
|
|
110
|
+
notifications = []
|
|
111
|
+
selector.define_singleton_method(:notify_queue_observers) do |_queue, id|
|
|
112
|
+
raise 'callback under selector mutex' if fixture.table(:mutex).owned?
|
|
113
|
+
notifications << [id, Thread.current]
|
|
114
|
+
end
|
|
115
|
+
# Use real startup logic: a missing queued failure must not be rescued by
|
|
116
|
+
# a later producer creating a replacement selector.
|
|
117
|
+
selector.singleton_class.remove_method(:check_select_thread_synchronized)
|
|
118
|
+
entered, release, start = Queue.new, Queue.new, Queue.new
|
|
119
|
+
original_select = IO.method(:select)
|
|
120
|
+
calls = 0
|
|
121
|
+
IO.define_singleton_method(:select) do |readers, *_|
|
|
122
|
+
calls += 1
|
|
123
|
+
if calls == 1
|
|
124
|
+
raise 'query sockets missing from selected snapshot' unless
|
|
125
|
+
readers.include?(broken.socket) && readers.include?(healthy.socket)
|
|
126
|
+
entered << true
|
|
127
|
+
release.pop # The native call is waiting on the snapshotted read set.
|
|
128
|
+
raise 'writer did not retire descriptor' unless broken.socket.closed?
|
|
129
|
+
raise Errno::EBADF, 'descriptor closed while select was waiting'
|
|
130
|
+
elsif calls == 2
|
|
131
|
+
[[healthy.socket], [], []]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
worker = Thread.new { start.pop; selector.do_select }
|
|
135
|
+
worker.report_on_exception = false
|
|
136
|
+
klass.class_variable_set(:@@select_thread, worker)
|
|
137
|
+
begin
|
|
138
|
+
start << true
|
|
139
|
+
entered.pop
|
|
140
|
+
failure = Dnsruby::SocketEofResolvError.new('concurrent writer failed')
|
|
141
|
+
selector.fail_tcp_send(broken, failure)
|
|
142
|
+
raise 'writer replaced active selector' unless fixture.table(:select_thread).equal?(worker)
|
|
143
|
+
release << true
|
|
144
|
+
worker.value
|
|
145
|
+
# Do not call any queue-draining helper: the original worker must have
|
|
146
|
+
# delivered both this failure and the unrelated, already registered reply.
|
|
147
|
+
raise 'failure not delivered once' unless broken.client_queue.length == 1 &&
|
|
148
|
+
broken.client_queue.pop(true) == [:broken, Dnsruby::Resolver::EventType::ERROR, nil, failure]
|
|
149
|
+
expected = [Dnsruby::Resolver::EventType::RECEIVED, Dnsruby::Resolver::EventType::VALIDATED]
|
|
150
|
+
actual = 2.times.map { healthy.client_queue.pop(true) }
|
|
151
|
+
raise 'unrelated response lost' unless actual.map { |event| event[1] } == expected &&
|
|
152
|
+
actual.all? { |event| event[0] == :healthy && event.last.nil? }
|
|
153
|
+
raise 'duplicate unrelated response' unless healthy.client_queue.empty?
|
|
154
|
+
raise 'a replacement worker delivered events' unless notifications.length == 3 &&
|
|
155
|
+
notifications.all? { |_id, thread| thread.equal?(worker) }
|
|
156
|
+
fixture.assert_retired(broken.socket)
|
|
157
|
+
raise 'queries retained' unless fixture.table(:query_hash).empty? && fixture.table(:timeouts).empty?
|
|
158
|
+
ensure
|
|
159
|
+
IO.define_singleton_method(:select, original_select)
|
|
160
|
+
worker.kill if worker.alive?
|
|
161
|
+
worker.join
|
|
162
|
+
end
|
|
163
|
+
RUBY
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def test_retirement_during_incoming_socket_classification_is_harmless
|
|
167
|
+
assert_isolated(<<~'RUBY')
|
|
168
|
+
fixture = RetirementFixture.new
|
|
169
|
+
selector = fixture.selector
|
|
170
|
+
socket = fixture.socket
|
|
171
|
+
settings = fixture.settings(socket)
|
|
172
|
+
error = Dnsruby::SocketEofResolvError.new('write failed during classification')
|
|
173
|
+
classified = false
|
|
174
|
+
socket.define_singleton_method(:getsockopt) do |*|
|
|
175
|
+
classified = true
|
|
176
|
+
selector.fail_tcp_send(settings, error)
|
|
177
|
+
raise IOError, 'closed stream'
|
|
178
|
+
end
|
|
179
|
+
raise 'unexpected incoming data' unless selector.get_incoming_data(socket, 0).nil?
|
|
180
|
+
raise 'classification hook not reached' unless classified
|
|
181
|
+
fixture.assert_send_error(settings, error)
|
|
182
|
+
RUBY
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def test_retirement_after_response_settings_lookup_does_not_duplicate_error
|
|
186
|
+
assert_isolated(<<~'RUBY')
|
|
187
|
+
fixture = RetirementFixture.new
|
|
188
|
+
selector = fixture.selector
|
|
189
|
+
socket = fixture.socket
|
|
190
|
+
# Nonpersistent settings exercise socket classification after lookup;
|
|
191
|
+
# persistent queries use their recorded TCP type instead.
|
|
192
|
+
settings = fixture.settings(socket, persistent: false)
|
|
193
|
+
response = fixture.response(settings)
|
|
194
|
+
error = Dnsruby::SocketEofResolvError.new('write failed after response lookup')
|
|
195
|
+
classified = false
|
|
196
|
+
socket.define_singleton_method(:getsockopt) do |*|
|
|
197
|
+
classified = true
|
|
198
|
+
selector.fail_tcp_send(settings, error)
|
|
199
|
+
raise IOError, 'closed stream'
|
|
200
|
+
end
|
|
201
|
+
selector.send_response_to_client(response, response.encode, socket)
|
|
202
|
+
raise 'response classification hook not reached' unless classified
|
|
203
|
+
fixture.assert_send_error(settings, error)
|
|
204
|
+
RUBY
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def test_recvfrom_failure_after_socket_retirement_is_harmless
|
|
208
|
+
assert_isolated(<<~'RUBY')
|
|
209
|
+
fixture = RetirementFixture.new
|
|
210
|
+
settings = fixture.settings(fixture.socket)
|
|
211
|
+
error = Dnsruby::SocketEofResolvError.new('write failed before recvfrom failure')
|
|
212
|
+
fixture.selector.fail_tcp_send(settings, error)
|
|
213
|
+
raise 'socket map was not retired' if fixture.table(:socket_hash).key?(settings.socket)
|
|
214
|
+
fixture.selector.handle_recvfrom_failure(settings.socket, IOError.new('closed stream'))
|
|
215
|
+
fixture.assert_send_error(settings, error)
|
|
216
|
+
RUBY
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def test_tcp_bytes_returning_after_retirement_do_not_restore_framing_buffers
|
|
220
|
+
assert_isolated(<<~'RUBY')
|
|
221
|
+
[:partial_prefix, :complete_prefix, :partial_body].each do |stage|
|
|
222
|
+
fixture = RetirementFixture.new
|
|
223
|
+
selector = fixture.selector
|
|
224
|
+
socket = fixture.socket
|
|
225
|
+
settings = fixture.settings(socket)
|
|
226
|
+
error = Dnsruby::SocketEofResolvError.new("write failed during #{stage}")
|
|
227
|
+
if stage == :partial_body
|
|
228
|
+
# The prefix was already consumed and one body byte was buffered.
|
|
229
|
+
fixture.table(:tcp_buffers)[socket] = [+'a', 4, false]
|
|
230
|
+
end
|
|
231
|
+
read = false
|
|
232
|
+
socket.define_singleton_method(:recv_nonblock) do |_length|
|
|
233
|
+
read = true
|
|
234
|
+
selector.fail_tcp_send(settings, error)
|
|
235
|
+
stage == :complete_prefix ? [4].pack('n') : "\0".b
|
|
236
|
+
end
|
|
237
|
+
raise "#{stage}: unexpected complete frame" unless selector.tcp_read(socket) == false
|
|
238
|
+
raise "#{stage}: read hook not reached" unless read
|
|
239
|
+
fixture.assert_send_error(settings, error)
|
|
240
|
+
end
|
|
241
|
+
RUBY
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def test_response_and_send_failure_have_only_one_terminal_owner
|
|
245
|
+
assert_isolated(<<~'RUBY')
|
|
246
|
+
[:failure_first, :response_first].each do |order|
|
|
247
|
+
fixture = RetirementFixture.new
|
|
248
|
+
selector = fixture.selector
|
|
249
|
+
settings = fixture.settings(fixture.socket)
|
|
250
|
+
response = fixture.response(settings)
|
|
251
|
+
error = Dnsruby::SocketEofResolvError.new("write failed #{order}")
|
|
252
|
+
interrupted = false
|
|
253
|
+
if order == :failure_first
|
|
254
|
+
sender = settings.single_resolver
|
|
255
|
+
check = sender.method(:check_response)
|
|
256
|
+
sender.define_singleton_method(:check_response) do |*args|
|
|
257
|
+
result = check.call(*args)
|
|
258
|
+
interrupted = true
|
|
259
|
+
selector.fail_tcp_send(settings, error)
|
|
260
|
+
result
|
|
261
|
+
end
|
|
262
|
+
else
|
|
263
|
+
push = selector.method(:push_to_client)
|
|
264
|
+
selector.define_singleton_method(:push_to_client) do |*args|
|
|
265
|
+
interrupted = true
|
|
266
|
+
raise 'response had not claimed its settings' if query_settings_for_id(settings.client_query_id).equal?(settings)
|
|
267
|
+
fail_tcp_send(settings, error)
|
|
268
|
+
push.call(*args)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
selector.send_response_to_client(response, response.encode, settings.socket)
|
|
272
|
+
raise "#{order}: ownership hook not reached" unless interrupted
|
|
273
|
+
if order == :failure_first
|
|
274
|
+
fixture.assert_send_error(settings, error)
|
|
275
|
+
else
|
|
276
|
+
fixture.assert_response(settings, response)
|
|
277
|
+
fixture.assert_retired(settings.socket)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
RUBY
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def test_stale_response_cannot_claim_a_replacement_with_the_same_client_id
|
|
284
|
+
assert_isolated(<<~'RUBY')
|
|
285
|
+
fixture = RetirementFixture.new
|
|
286
|
+
selector = fixture.selector
|
|
287
|
+
original = fixture.settings(fixture.socket)
|
|
288
|
+
replacement = fixture.settings(fixture.socket, register: false)
|
|
289
|
+
response = fixture.response(original)
|
|
290
|
+
sender = original.single_resolver
|
|
291
|
+
check = sender.method(:check_response)
|
|
292
|
+
replaced = false
|
|
293
|
+
sender.define_singleton_method(:check_response) do |*args|
|
|
294
|
+
result = check.call(*args)
|
|
295
|
+
selector.add_to_select(replacement)
|
|
296
|
+
selector.fail_tcp_send(original, Dnsruby::SocketEofResolvError.new('old write failed'))
|
|
297
|
+
replaced = true
|
|
298
|
+
result
|
|
299
|
+
end
|
|
300
|
+
selector.send_response_to_client(response, response.encode, original.socket)
|
|
301
|
+
raise 'replacement hook not reached' unless replaced
|
|
302
|
+
raise 'stale response was delivered' unless fixture.events(original).empty?
|
|
303
|
+
raise 'replacement was consumed' unless selector.query_settings_for_id(:query).equal?(replacement)
|
|
304
|
+
raise 'replacement received stale failure' unless fixture.events(replacement).empty?
|
|
305
|
+
fixture.assert_retired(original.socket)
|
|
306
|
+
response = fixture.response(replacement)
|
|
307
|
+
selector.send_response_to_client(response, response.encode, replacement.socket)
|
|
308
|
+
fixture.assert_response(replacement, response)
|
|
309
|
+
RUBY
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def test_timeout_snapshot_cannot_expire_a_replacement_with_the_same_client_id
|
|
313
|
+
assert_isolated(<<~'RUBY')
|
|
314
|
+
fixture = RetirementFixture.new
|
|
315
|
+
selector = fixture.selector
|
|
316
|
+
deadline = Dnsruby::TransportClock.now - 1
|
|
317
|
+
original = fixture.settings(fixture.socket, endtime: deadline)
|
|
318
|
+
replacement = fixture.settings(fixture.socket, register: false)
|
|
319
|
+
replaced = false
|
|
320
|
+
claim = selector.method(:claim_query)
|
|
321
|
+
selector.define_singleton_method(:claim_query) do |settings, *args|
|
|
322
|
+
# The due heap entry has been popped and the selector lock released.
|
|
323
|
+
# Another sender replaces the ID before the old settings are claimed.
|
|
324
|
+
unless replaced
|
|
325
|
+
add_to_select(replacement)
|
|
326
|
+
replaced = true
|
|
327
|
+
end
|
|
328
|
+
claim.call(settings, *args)
|
|
329
|
+
end
|
|
330
|
+
selector.process_timeouts
|
|
331
|
+
raise 'timeout snapshot hook not reached' unless replaced
|
|
332
|
+
raise 'old attempt received timeout' unless fixture.events(original).empty?
|
|
333
|
+
raise 'replacement received old timeout' unless fixture.events(replacement).empty?
|
|
334
|
+
raise 'replacement registration was consumed' unless selector.query_settings_for_id(:query).equal?(replacement)
|
|
335
|
+
raise 'replacement deadline was removed' unless fixture.table(:timeouts)[:query] == replacement.endtime
|
|
336
|
+
# The replacement can still expire on its own deadline.
|
|
337
|
+
fixture.table(:timeouts)[:query] = Dnsruby::TransportClock.now - 1
|
|
338
|
+
selector.instance_variable_get(:@packet_deadlines)[:query] = fixture.table(:timeouts)[:query]
|
|
339
|
+
selector.process_timeouts
|
|
340
|
+
events = fixture.events(replacement)
|
|
341
|
+
raise 'replacement did not receive one timeout' unless events.length == 1 &&
|
|
342
|
+
events.first[0, 3] == [:query, Dnsruby::Resolver::EventType::ERROR, nil] &&
|
|
343
|
+
events.first.last.is_a?(Dnsruby::ResolvTimeout)
|
|
344
|
+
raise 'timed-out replacement retained' if selector.query_settings_for_id(:query)
|
|
345
|
+
RUBY
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
private
|
|
349
|
+
|
|
350
|
+
def assert_isolated(body)
|
|
351
|
+
options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
|
|
352
|
+
script = SUBPROCESS_SETUP + "\nTimeout.timeout(10) do\n" + body + "\nend\n"
|
|
353
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
|
|
354
|
+
File.expand_path('../lib', __dir__), '-e', script)
|
|
355
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
356
|
+
assert_empty(stderr)
|
|
357
|
+
end
|
|
358
|
+
end
|