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.
- 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 +314 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/dnsruby.gemspec +6 -4
- data/lib/dnsruby/cache.rb +279 -49
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +77 -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 +664 -207
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +798 -341
- data/lib/dnsruby/resolver.rb +472 -176
- 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 +21 -2
- 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/OPT.rb +2 -3
- 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 +986 -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 +53 -2
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +34 -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 +127 -14
- data/.coveralls.yml +0 -2
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'open3'
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
|
|
5
|
+
class TestPipelineSend < Minitest::Test
|
|
6
|
+
# Every scenario gets its own selector tables and method hooks. This also
|
|
7
|
+
# prevents the suite's real singleton from consuming fixture-owned events.
|
|
8
|
+
FIXTURE = <<~'RUBY'
|
|
9
|
+
require 'dnsruby'
|
|
10
|
+
require 'timeout'
|
|
11
|
+
Dnsruby.log.level = Logger::FATAL
|
|
12
|
+
K = Dnsruby::SelectThread
|
|
13
|
+
{
|
|
14
|
+
mutex: Mutex.new, query_hash: {}, queries_by_queue: {}.compare_by_identity, socket_hash: {}, timeouts: {}, sockets: Set.new,
|
|
15
|
+
tcp_buffers: {}, socket_is_persistent: {}, socket_remaining_queries: {},
|
|
16
|
+
socket_owners: {}, retiring_sockets: Set.new,
|
|
17
|
+
queued_exceptions: [], queued_responses: [], queued_validation_responses: [],
|
|
18
|
+
observers: {}, observer_queue_counts: {}.compare_by_identity, tick_observers: [], select_thread: nil
|
|
19
|
+
}.each { |name, value| K.class_variable_set("@@#{name}", value) }
|
|
20
|
+
SELECTOR = K.send(:allocate)
|
|
21
|
+
SELECTOR.define_singleton_method(:check_select_thread_synchronized) {}
|
|
22
|
+
SELECTOR.define_singleton_method(:wake_up_select_thread) {}
|
|
23
|
+
SELECTOR.define_singleton_method(:notify_queue_observers) { |*| }
|
|
24
|
+
K.define_singleton_method(:instance) { SELECTOR }
|
|
25
|
+
|
|
26
|
+
class FrameSocket
|
|
27
|
+
attr_accessor :on_write, :limit, :incoming
|
|
28
|
+
attr_reader :written
|
|
29
|
+
def initialize
|
|
30
|
+
@closed = false
|
|
31
|
+
@written = ''.b
|
|
32
|
+
@incoming = ''.b
|
|
33
|
+
end
|
|
34
|
+
def closed?; @closed; end
|
|
35
|
+
def close; @closed = true; end
|
|
36
|
+
def getsockopt(*)
|
|
37
|
+
raise IOError, 'closed stream' if closed?
|
|
38
|
+
Struct.new(:data).new([Socket::SOCK_STREAM].pack('i'))
|
|
39
|
+
end
|
|
40
|
+
def write_nonblock(bytes)
|
|
41
|
+
raise IOError, 'closed stream' if closed?
|
|
42
|
+
count = @limit ? [@limit, bytes.bytesize].min : bytes.bytesize
|
|
43
|
+
@written << bytes.byteslice(0, count)
|
|
44
|
+
@on_write.call(bytes, count) if @on_write
|
|
45
|
+
count
|
|
46
|
+
end
|
|
47
|
+
# Datagram sends share the byte buffer used by stream writes.
|
|
48
|
+
def send(bytes, _flags); write_nonblock(bytes); end
|
|
49
|
+
def recv_nonblock(length)
|
|
50
|
+
raise IOError, 'closed stream' if closed?
|
|
51
|
+
raise IO::EAGAINWaitReadable if @incoming.empty?
|
|
52
|
+
@incoming.slice!(0, length)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def query(name = 'pipeline.example.')
|
|
57
|
+
q = Dnsruby::Message.new(name)
|
|
58
|
+
q.do_validation = q.do_caching = false
|
|
59
|
+
q
|
|
60
|
+
end
|
|
61
|
+
def sender(socket, max = :infinite)
|
|
62
|
+
result = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: true,
|
|
63
|
+
tcp_pipelining_max_queries: max, packet_timeout: 1)
|
|
64
|
+
result.define_singleton_method(:tcp_pipeline_socket) { |_| [socket, false] }
|
|
65
|
+
result
|
|
66
|
+
end
|
|
67
|
+
def settings(socket, id, q = query, resolver = sender(socket), max = :infinite)
|
|
68
|
+
s = K::QuerySettings.new(q.encode, q, false, Queue.new, id, socket,
|
|
69
|
+
'127.0.0.1', 53, Dnsruby::TransportClock.now + 1, 512, resolver)
|
|
70
|
+
s.is_persistent_socket = true
|
|
71
|
+
s.tcp_pipelining_max_queries = max
|
|
72
|
+
s
|
|
73
|
+
end
|
|
74
|
+
def drain_errors
|
|
75
|
+
SELECTOR.send_queued_exceptions
|
|
76
|
+
end
|
|
77
|
+
def assert_clean(socket)
|
|
78
|
+
[:socket_hash, :tcp_buffers, :socket_is_persistent, :socket_remaining_queries, :socket_owners].each do |name|
|
|
79
|
+
raise "retained #{name}" if K.class_variable_get("@@#{name}").key?(socket)
|
|
80
|
+
end
|
|
81
|
+
raise 'retained socket' if K.class_variable_get(:@@sockets).include?(socket)
|
|
82
|
+
raise 'retained retirement' if K.class_variable_get(:@@retiring_sockets).include?(socket)
|
|
83
|
+
end
|
|
84
|
+
RUBY
|
|
85
|
+
|
|
86
|
+
def test_query_is_registered_before_a_fast_native_response
|
|
87
|
+
run_scenario(<<~'RUBY')
|
|
88
|
+
socket = FrameSocket.new
|
|
89
|
+
writer = sender(socket)
|
|
90
|
+
old = settings(socket, :old, query('older.example.'), writer)
|
|
91
|
+
SELECTOR.add_to_select(old) # The reused socket is already being selected.
|
|
92
|
+
q, queue = query('immediate.example.'), Queue.new
|
|
93
|
+
response = q.clone
|
|
94
|
+
response.header.qr = true
|
|
95
|
+
wire = response.encode
|
|
96
|
+
delivered = Queue.new
|
|
97
|
+
reader = nil
|
|
98
|
+
socket.on_write = ->(_bytes, _count) do
|
|
99
|
+
next unless socket.written.bytesize >= q.encode.bytesize + 2
|
|
100
|
+
socket.incoming = [wire.bytesize].pack('n') + wire
|
|
101
|
+
reader = Thread.new do
|
|
102
|
+
incoming = SELECTOR.get_incoming_data(socket, 0)
|
|
103
|
+
SELECTOR.process_message(*incoming, socket) if incoming
|
|
104
|
+
SELECTOR.send_queued_validation_responses
|
|
105
|
+
delivered << true
|
|
106
|
+
end
|
|
107
|
+
# Native nonblocking writes must not wait for selector callbacks. Check
|
|
108
|
+
# publication inside the write, then let the reader race its return.
|
|
109
|
+
raise 'query was not published before write' unless SELECTOR.query_settings_for_id(:immediate)
|
|
110
|
+
end
|
|
111
|
+
writer.send_dnsruby(q.encode, q, :immediate, queue, true)
|
|
112
|
+
reader.join
|
|
113
|
+
events = 2.times.map { queue.pop(true) }
|
|
114
|
+
raise 'wrong delivery order' unless events.map { |e| e[1] } ==
|
|
115
|
+
[Dnsruby::Resolver::EventType::RECEIVED, Dnsruby::Resolver::EventType::VALIDATED]
|
|
116
|
+
raise 'unexpected error' unless events.all? { |e| e.last.nil? }
|
|
117
|
+
raise 'duplicate answer' unless queue.empty?
|
|
118
|
+
raise 'answer still registered' if SELECTOR.query_settings_for_id(:immediate)
|
|
119
|
+
raise 'other query removed' unless SELECTOR.query_settings_for_id(:old).equal?(old)
|
|
120
|
+
RUBY
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_partial_writes_complete_frames_and_ordinary_callers_wait_for_the_writer
|
|
124
|
+
run_scenario(<<~'RUBY')
|
|
125
|
+
socket = FrameSocket.new
|
|
126
|
+
socket.limit = 3
|
|
127
|
+
writer = sender(socket)
|
|
128
|
+
entered, release, returned = Queue.new, Queue.new, Queue.new
|
|
129
|
+
first_write = true
|
|
130
|
+
socket.on_write = ->(*) do
|
|
131
|
+
if first_write
|
|
132
|
+
first_write = false
|
|
133
|
+
entered << true
|
|
134
|
+
release.pop
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
a, b = query('first.example.'), query('second.example.')
|
|
138
|
+
t1 = Thread.new { writer.send_dnsruby(a.encode, a, :first, Queue.new, true) }
|
|
139
|
+
entered.pop
|
|
140
|
+
t2 = Thread.new do
|
|
141
|
+
writer.send_dnsruby(b.encode, b, :second, Queue.new, true)
|
|
142
|
+
returned << true
|
|
143
|
+
end
|
|
144
|
+
Thread.pass until t2.status == 'sleep'
|
|
145
|
+
raise 'ordinary caller returned before sending' unless returned.empty?
|
|
146
|
+
release << true
|
|
147
|
+
t1.value
|
|
148
|
+
t2.value
|
|
149
|
+
expected = [a, b].map { |q| [q.encode.bytesize].pack('n') + q.encode }.join
|
|
150
|
+
raise 'interleaved or truncated frames' unless socket.written == expected
|
|
151
|
+
RUBY
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def test_selector_retry_defers_when_another_writer_owns_the_pipeline
|
|
155
|
+
run_scenario(<<~'RUBY')
|
|
156
|
+
socket = FrameSocket.new
|
|
157
|
+
writer = sender(socket)
|
|
158
|
+
entered, release = Queue.new, Queue.new
|
|
159
|
+
first_write = true
|
|
160
|
+
socket.on_write = ->(*) do
|
|
161
|
+
if first_write
|
|
162
|
+
first_write = false
|
|
163
|
+
entered << true
|
|
164
|
+
release.pop
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
first = query('first.example.')
|
|
168
|
+
sending = Thread.new { writer.send_dnsruby(first.encode, first, :first, Queue.new, true) }
|
|
169
|
+
entered.pop
|
|
170
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
171
|
+
retry_query = query('retry.example.')
|
|
172
|
+
old = settings(FrameSocket.new, :retry, retry_query, writer)
|
|
173
|
+
old.is_persistent_socket = false # UDP-to-TCP retry replaces a one-use socket.
|
|
174
|
+
SELECTOR.add_to_select(old)
|
|
175
|
+
worker = writer.send_dnsruby(retry_query.encode, retry_query, :retry, old.client_queue, true)
|
|
176
|
+
raise 'selector send did not defer' unless worker.respond_to?(:value)
|
|
177
|
+
raise 'new writer entered before old frame ended' unless socket.written.bytesize == first.encode.bytesize + 2
|
|
178
|
+
release << true
|
|
179
|
+
sending.value
|
|
180
|
+
worker.value
|
|
181
|
+
replacement = SELECTOR.query_settings_for_id(:retry)
|
|
182
|
+
raise 'retry lost' unless replacement && !replacement.equal?(old) && replacement.socket == socket
|
|
183
|
+
assert_clean(old.socket)
|
|
184
|
+
RUBY
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def test_deferred_retry_cannot_resurrect_an_expired_or_replaced_query
|
|
188
|
+
run_scenario(<<~'RUBY')
|
|
189
|
+
[:expired, :replaced].each do |kind|
|
|
190
|
+
socket = FrameSocket.new
|
|
191
|
+
writer = sender(socket)
|
|
192
|
+
lock = Mutex.new
|
|
193
|
+
writer.instance_variable_set(:@pipeline_mutex, lock)
|
|
194
|
+
lock.lock
|
|
195
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
196
|
+
old = settings(FrameSocket.new, kind, query("#{kind}.example."), writer)
|
|
197
|
+
SELECTOR.add_to_select(old)
|
|
198
|
+
worker = writer.send_dnsruby(old.query_bytes, old.query, kind, old.client_queue, true)
|
|
199
|
+
if kind == :expired
|
|
200
|
+
old.endtime = Dnsruby::TransportClock.now - 1
|
|
201
|
+
K.class_variable_get(:@@timeouts)[kind] = old.endtime
|
|
202
|
+
SELECTOR.instance_variable_get(:@packet_deadlines)[kind] = K.class_variable_get(:@@timeouts)[kind]
|
|
203
|
+
SELECTOR.process_timeouts
|
|
204
|
+
else
|
|
205
|
+
replacement = settings(FrameSocket.new, kind)
|
|
206
|
+
SELECTOR.add_to_select(replacement)
|
|
207
|
+
end
|
|
208
|
+
lock.unlock
|
|
209
|
+
worker.value
|
|
210
|
+
drain_errors
|
|
211
|
+
raise 'stale retry wrote bytes' unless socket.written.empty?
|
|
212
|
+
if kind == :expired
|
|
213
|
+
raise 'wrong timeout cardinality' unless old.client_queue.length == 1
|
|
214
|
+
raise 'expired retry resurrected' if SELECTOR.query_settings_for_id(kind)
|
|
215
|
+
else
|
|
216
|
+
raise 'replacement lost' unless SELECTOR.query_settings_for_id(kind).equal?(replacement)
|
|
217
|
+
raise 'obsolete retry reported an error' unless old.client_queue.empty?
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
RUBY
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_failure_retires_the_stream_and_reports_each_query_once
|
|
224
|
+
run_scenario(<<~'RUBY')
|
|
225
|
+
[:infinite, 3].each do |max|
|
|
226
|
+
[:raise, :zero].each do |failure|
|
|
227
|
+
socket = FrameSocket.new
|
|
228
|
+
writer = sender(socket, max)
|
|
229
|
+
old = settings(socket, :old, query('older.example.'), writer, max)
|
|
230
|
+
SELECTOR.add_to_select(old)
|
|
231
|
+
if failure == :zero
|
|
232
|
+
socket.define_singleton_method(:write_nonblock) { |_| 0 }
|
|
233
|
+
else
|
|
234
|
+
socket.limit = 2
|
|
235
|
+
calls = 0
|
|
236
|
+
socket.on_write = ->(*) { calls += 1; raise IOError, 'write failure' if calls == 2 }
|
|
237
|
+
end
|
|
238
|
+
q, queue = query, Queue.new
|
|
239
|
+
writer.send_dnsruby(q.encode, q, :new, queue, true)
|
|
240
|
+
drain_errors
|
|
241
|
+
[old.client_queue, queue].each do |events|
|
|
242
|
+
raise 'missing or duplicate error' unless events.length == 1
|
|
243
|
+
raise 'wrong error' unless events.pop.last.is_a?(Dnsruby::SocketEofResolvError)
|
|
244
|
+
end
|
|
245
|
+
raise 'failed stream left open' unless socket.closed?
|
|
246
|
+
assert_clean(socket)
|
|
247
|
+
raise 'query retained' if SELECTOR.query_settings_for_id(:old) || SELECTOR.query_settings_for_id(:new)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
RUBY
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def test_closed_nonpipeline_registration_does_not_attempt_a_write_or_report_twice
|
|
254
|
+
run_scenario(<<~'RUBY')
|
|
255
|
+
socket = FrameSocket.new
|
|
256
|
+
socket.close
|
|
257
|
+
writer = sender(socket)
|
|
258
|
+
writer.tcp_pipelining = false
|
|
259
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| socket }
|
|
260
|
+
q, queue = query, Queue.new
|
|
261
|
+
writer.send_dnsruby(q.encode, q, :closed, queue, true)
|
|
262
|
+
drain_errors
|
|
263
|
+
raise 'wrote closed socket' unless socket.written.empty?
|
|
264
|
+
raise 'missing or duplicate error' unless queue.length == 1
|
|
265
|
+
raise 'wrong error' unless queue.pop.last.is_a?(Dnsruby::SocketEofResolvError)
|
|
266
|
+
assert_clean(socket)
|
|
267
|
+
RUBY
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def test_retry_expiring_during_connect_releases_only_the_unused_socket
|
|
271
|
+
run_scenario(<<~'RUBY')
|
|
272
|
+
[true, false].each do |new_socket|
|
|
273
|
+
socket = FrameSocket.new
|
|
274
|
+
writer = sender(socket)
|
|
275
|
+
old = settings(FrameSocket.new, :retry, query, writer)
|
|
276
|
+
SELECTOR.add_to_select(old)
|
|
277
|
+
other = settings(socket, :other, query('other.example.'), writer)
|
|
278
|
+
SELECTOR.add_to_select(other) unless new_socket
|
|
279
|
+
writer.define_singleton_method(:tcp_pipeline_socket) do |_|
|
|
280
|
+
old.endtime = Dnsruby::TransportClock.now - 1
|
|
281
|
+
K.class_variable_get(:@@timeouts)[:retry] = old.endtime
|
|
282
|
+
SELECTOR.instance_variable_get(:@packet_deadlines)[:retry] = K.class_variable_get(:@@timeouts)[:retry]
|
|
283
|
+
SELECTOR.process_timeouts # The original attempt completes during connect.
|
|
284
|
+
@use_counts[socket] = 1
|
|
285
|
+
[socket, new_socket]
|
|
286
|
+
end
|
|
287
|
+
writer.send_dnsruby(old.query_bytes, old.query, :retry, old.client_queue, true)
|
|
288
|
+
drain_errors
|
|
289
|
+
raise 'expired retry wrote bytes' unless socket.written.empty?
|
|
290
|
+
raise 'missing timeout' unless old.client_queue.length == 1 && old.client_queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
291
|
+
raise 'expired retry retained' if SELECTOR.query_settings_for_id(:retry)
|
|
292
|
+
if new_socket
|
|
293
|
+
raise 'unused socket leaked' unless socket.closed?
|
|
294
|
+
assert_clean(socket)
|
|
295
|
+
else
|
|
296
|
+
raise 'other pending query damaged' if socket.closed? || !SELECTOR.query_settings_for_id(:other).equal?(other)
|
|
297
|
+
SELECTOR.fail_tcp_send(other, Dnsruby::SocketEofResolvError.new)
|
|
298
|
+
drain_errors
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
RUBY
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def test_configuration_marshal_and_first_send_from_a_stored_sender
|
|
305
|
+
run_scenario(<<~'RUBY')
|
|
306
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: true)
|
|
307
|
+
raise 'configuration gained a mutex' if writer.instance_variable_defined?(:@pipeline_mutex)
|
|
308
|
+
stored = Marshal.load(Marshal.dump(writer))
|
|
309
|
+
socket = FrameSocket.new
|
|
310
|
+
stored.define_singleton_method(:tcp_pipeline_socket) { |_| [socket, false] }
|
|
311
|
+
q = query
|
|
312
|
+
stored.send_dnsruby(q.encode, q, :stored, Queue.new, true)
|
|
313
|
+
raise 'stored sender cannot send' unless socket.written == [q.encode.bytesize].pack('n') + q.encode
|
|
314
|
+
raise 'stored sender not registered' unless SELECTOR.query_settings_for_id(:stored)
|
|
315
|
+
RUBY
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def test_blocked_write_and_selector_writer_wait_have_bounded_timeouts
|
|
319
|
+
run_scenario(<<~'RUBY')
|
|
320
|
+
socket = FrameSocket.new
|
|
321
|
+
writer = sender(socket)
|
|
322
|
+
socket.define_singleton_method(:write_nonblock) { |_| raise IO::EAGAINWaitWritable }
|
|
323
|
+
original_select = IO.method(:select)
|
|
324
|
+
IO.define_singleton_method(:select) { |*| nil }
|
|
325
|
+
q, queue = query, Queue.new
|
|
326
|
+
begin
|
|
327
|
+
writer.send_dnsruby(q.encode, q, :blocked, queue, true)
|
|
328
|
+
ensure
|
|
329
|
+
IO.define_singleton_method(:select, original_select)
|
|
330
|
+
end
|
|
331
|
+
drain_errors
|
|
332
|
+
raise 'wrong write timeout' unless queue.length == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
333
|
+
raise 'blocked stream not closed' unless socket.closed?
|
|
334
|
+
assert_clean(socket)
|
|
335
|
+
|
|
336
|
+
waiting_socket = FrameSocket.new
|
|
337
|
+
waiting = sender(waiting_socket)
|
|
338
|
+
waiting.packet_timeout = 0.02
|
|
339
|
+
lock = Mutex.new
|
|
340
|
+
waiting.instance_variable_set(:@pipeline_mutex, lock)
|
|
341
|
+
lock.lock
|
|
342
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
343
|
+
queue = Queue.new
|
|
344
|
+
worker = waiting.send_dnsruby(q.encode, q, :waiting, queue, true)
|
|
345
|
+
worker.value # Must finish while the other writer still owns the mutex.
|
|
346
|
+
drain_errors
|
|
347
|
+
raise 'wrong writer-wait timeout' unless queue.length == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
348
|
+
raise 'timed-out sender wrote' unless waiting_socket.written.empty?
|
|
349
|
+
lock.unlock
|
|
350
|
+
RUBY
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def test_tcp_write_timeout_preserves_the_resolvers_remaining_retries
|
|
354
|
+
run_scenario(<<~'RUBY')
|
|
355
|
+
original_tcp, original_select = Socket.method(:tcp), IO.method(:select)
|
|
356
|
+
begin
|
|
357
|
+
[false, true].each do |pipeline|
|
|
358
|
+
socket = FrameSocket.new
|
|
359
|
+
socket.define_singleton_method(:write_nonblock) { |_| raise IO::EAGAINWaitWritable }
|
|
360
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **options| socket }
|
|
361
|
+
IO.define_singleton_method(:select) { |*| nil }
|
|
362
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true,
|
|
363
|
+
tcp_pipelining: pipeline, dnssec: false, packet_timeout: 0.05)
|
|
364
|
+
q, queue = query, Queue.new
|
|
365
|
+
writer.send_async(q, queue, :write_timeout)
|
|
366
|
+
drain_errors
|
|
367
|
+
raise 'missing or duplicate timeout' unless queue.length == 1
|
|
368
|
+
error = queue.pop(true).last
|
|
369
|
+
raise 'write timeout became a connection failure' unless error.is_a?(Dnsruby::ResolvTimeout)
|
|
370
|
+
raise 'stalled socket left open' unless socket.closed?
|
|
371
|
+
assert_clean(socket)
|
|
372
|
+
|
|
373
|
+
# A timeout keeps the next attempt scheduled; a connection error would
|
|
374
|
+
# remove this server and finish the client query prematurely.
|
|
375
|
+
parent = Struct.new(:single_resolvers, :single_res_mutex).new([writer], Mutex.new)
|
|
376
|
+
engine = Dnsruby::ResolverRuby.new(parent)
|
|
377
|
+
client_queue, select_queue = Queue.new, Queue.new
|
|
378
|
+
attempt = [writer, q, :client, 0]
|
|
379
|
+
engine.instance_variable_get(:@query_list)[:client] = [q, client_queue, select_queue, []]
|
|
380
|
+
scheduled = {Dnsruby::TransportClock.now + 60 => [writer, 1]}
|
|
381
|
+
engine.instance_variable_get(:@timeouts)[:client] = [Dnsruby::TransportClock.now + 120, scheduled]
|
|
382
|
+
SELECTOR.add_observer(select_queue, engine)
|
|
383
|
+
engine.handle_error_response(select_queue, attempt, error, nil)
|
|
384
|
+
raise 'remaining retry removed' unless scheduled.size == 1
|
|
385
|
+
raise 'client query stopped early' unless engine.instance_variable_get(:@query_list).key?(:client)
|
|
386
|
+
raise 'premature client error' unless client_queue.empty?
|
|
387
|
+
engine.stop_querying(:client)
|
|
388
|
+
end
|
|
389
|
+
ensure
|
|
390
|
+
Socket.define_singleton_method(:tcp, original_tcp)
|
|
391
|
+
IO.define_singleton_method(:select, original_select)
|
|
392
|
+
end
|
|
393
|
+
RUBY
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def test_selector_worker_creation_failure_reports_the_original_attempt_once
|
|
397
|
+
run_scenario(<<~'RUBY')
|
|
398
|
+
socket = FrameSocket.new
|
|
399
|
+
writer = sender(socket)
|
|
400
|
+
lock = Mutex.new
|
|
401
|
+
writer.instance_variable_set(:@pipeline_mutex, lock)
|
|
402
|
+
lock.lock
|
|
403
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
404
|
+
old = settings(FrameSocket.new, :retry, query, writer)
|
|
405
|
+
SELECTOR.add_to_select(old)
|
|
406
|
+
factory = Thread.method(:new)
|
|
407
|
+
Thread.define_singleton_method(:new) { |*| raise ThreadError, 'cannot start worker' }
|
|
408
|
+
begin
|
|
409
|
+
writer.send_dnsruby(old.query_bytes, old.query, :retry, old.client_queue, true)
|
|
410
|
+
ensure
|
|
411
|
+
Thread.define_singleton_method(:new, factory)
|
|
412
|
+
lock.unlock
|
|
413
|
+
end
|
|
414
|
+
drain_errors
|
|
415
|
+
raise 'missing worker error' unless old.client_queue.length == 1 && old.client_queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
416
|
+
raise 'failed worker retained query' if SELECTOR.query_settings_for_id(:retry)
|
|
417
|
+
raise 'failed worker wrote' unless socket.written.empty?
|
|
418
|
+
RUBY
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def test_fresh_deferred_send_is_cancelled_by_resolver_close_before_writer_or_connect
|
|
422
|
+
run_scenario(<<~'RUBY')
|
|
423
|
+
[:writer, :connect].each do |boundary|
|
|
424
|
+
socket = FrameSocket.new
|
|
425
|
+
writer = sender(socket)
|
|
426
|
+
writer.use_tcp = true
|
|
427
|
+
parent = Dnsruby::Resolver.new(nameserver: '127.0.0.1', use_tcp: true,
|
|
428
|
+
tcp_pipelining: true, do_caching: false, dnssec: false)
|
|
429
|
+
parent.single_resolvers = [writer]
|
|
430
|
+
entered, release = Queue.new, Queue.new
|
|
431
|
+
lock = Mutex.new
|
|
432
|
+
writer.instance_variable_set(:@pipeline_mutex, lock)
|
|
433
|
+
lock.lock if boundary == :writer
|
|
434
|
+
if boundary == :connect
|
|
435
|
+
writer.define_singleton_method(:tcp_pipeline_socket) do |_|
|
|
436
|
+
entered << true
|
|
437
|
+
release.pop
|
|
438
|
+
[socket, true]
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
task = nil
|
|
442
|
+
defer = SELECTOR.method(:defer_send)
|
|
443
|
+
SELECTOR.define_singleton_method(:defer_send) { |*args, &operation| task = defer.call(*args, &operation) }
|
|
444
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
445
|
+
queue = Queue.new
|
|
446
|
+
parent.send_async(query, queue, :closing)
|
|
447
|
+
entered.pop if boundary == :connect
|
|
448
|
+
raise 'fresh attempt has no cancellation owner' if K.class_variable_get(:@@query_hash).empty?
|
|
449
|
+
parent.close
|
|
450
|
+
result = queue.pop(true)
|
|
451
|
+
raise 'close not delivered' unless result.last.is_a?(Dnsruby::OtherResolvError)
|
|
452
|
+
raise 'close retained pending attempt' unless K.class_variable_get(:@@query_hash).empty?
|
|
453
|
+
lock.unlock if boundary == :writer
|
|
454
|
+
release << true if boundary == :connect
|
|
455
|
+
task.value
|
|
456
|
+
drain_errors
|
|
457
|
+
raise 'cancelled send wrote bytes' unless socket.written.empty?
|
|
458
|
+
raise 'close delivered twice' unless queue.empty?
|
|
459
|
+
raise 'unused connection retained' if boundary == :connect && !socket.closed?
|
|
460
|
+
end
|
|
461
|
+
RUBY
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def test_close_between_registration_and_first_tcp_write_preserves_other_pipeline_queries
|
|
465
|
+
run_scenario(<<~'RUBY')
|
|
466
|
+
socket = FrameSocket.new
|
|
467
|
+
writer = sender(socket)
|
|
468
|
+
writer.use_tcp = true
|
|
469
|
+
other = settings(socket, :other, query('other.example.'), writer)
|
|
470
|
+
SELECTOR.add_to_select(other)
|
|
471
|
+
parent = Dnsruby::Resolver.new(nameserver: '127.0.0.1', use_tcp: true,
|
|
472
|
+
tcp_pipelining: true, do_caching: false, dnssec: false)
|
|
473
|
+
parent.single_resolvers = [writer]
|
|
474
|
+
register = SELECTOR.method(:register_query_if_current)
|
|
475
|
+
SELECTOR.define_singleton_method(:register_query_if_current) do |current, previous|
|
|
476
|
+
result = register.call(current, previous)
|
|
477
|
+
# Cancel after the atomic registration, before the first native write.
|
|
478
|
+
# Run outside ResolverRuby's caller-side lock, as a selector send does.
|
|
479
|
+
parent.close if result
|
|
480
|
+
result
|
|
481
|
+
end
|
|
482
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
483
|
+
task = nil
|
|
484
|
+
defer = SELECTOR.method(:defer_send)
|
|
485
|
+
SELECTOR.define_singleton_method(:defer_send) { |*args, &operation| task = defer.call(*args, &operation) }
|
|
486
|
+
queue = Queue.new
|
|
487
|
+
parent.send_async(query, queue, :closing)
|
|
488
|
+
task.value
|
|
489
|
+
drain_errors
|
|
490
|
+
raise 'cancelled registered send wrote bytes' unless socket.written.empty?
|
|
491
|
+
raise 'other pipeline query was retired' if socket.closed? || !SELECTOR.query_settings_for_id(:other).equal?(other)
|
|
492
|
+
raise 'wrong close cardinality' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::OtherResolvError)
|
|
493
|
+
RUBY
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def test_sender_pool_bounds_threads_without_rejecting_bursts
|
|
497
|
+
run_scenario(<<~'RUBY')
|
|
498
|
+
entered, release = Queue.new, Queue.new
|
|
499
|
+
blockers = 4.times.map do
|
|
500
|
+
SELECTOR.defer_send { entered << Thread.current; release.pop }
|
|
501
|
+
end
|
|
502
|
+
workers = 4.times.map { entered.pop }
|
|
503
|
+
raise 'worker count not bounded' unless workers.uniq.size == 4
|
|
504
|
+
completed = Queue.new
|
|
505
|
+
burst = 1000.times.map { SELECTOR.defer_send { completed << Thread.current } }
|
|
506
|
+
raise 'queued sends escaped the bounded workers' unless completed.empty?
|
|
507
|
+
4.times { release << true }
|
|
508
|
+
(blockers + burst).each(&:value)
|
|
509
|
+
raise 'burst dropped work' unless completed.size == 1000
|
|
510
|
+
raise 'pool expanded under burst' unless 1000.times.map { completed.pop }.all? { |thread| workers.include?(thread) }
|
|
511
|
+
RUBY
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def test_selector_delegates_uncontended_tcp_connect_and_write_waits_in_both_modes
|
|
515
|
+
run_scenario(<<~'RUBY')
|
|
516
|
+
[false, true].each do |pipeline|
|
|
517
|
+
[:connect, :write].each do |phase|
|
|
518
|
+
socket = FrameSocket.new
|
|
519
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: pipeline,
|
|
520
|
+
packet_timeout: 1, dnssec: false)
|
|
521
|
+
entered, release = Queue.new, Queue.new
|
|
522
|
+
connect = -> do
|
|
523
|
+
if phase == :connect
|
|
524
|
+
entered << Thread.current
|
|
525
|
+
release.pop
|
|
526
|
+
end
|
|
527
|
+
socket
|
|
528
|
+
end
|
|
529
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| connect.call }
|
|
530
|
+
writer.define_singleton_method(:tcp_pipeline_socket) { |_| [connect.call, true] }
|
|
531
|
+
if phase == :write
|
|
532
|
+
first = true
|
|
533
|
+
native_write = socket.method(:write_nonblock)
|
|
534
|
+
socket.define_singleton_method(:write_nonblock) do |bytes|
|
|
535
|
+
if first
|
|
536
|
+
first = false
|
|
537
|
+
raise IO::EAGAINWaitWritable
|
|
538
|
+
end
|
|
539
|
+
native_write.call(bytes)
|
|
540
|
+
end
|
|
541
|
+
IO.define_singleton_method(:select) do |*|
|
|
542
|
+
entered << Thread.current
|
|
543
|
+
release.pop
|
|
544
|
+
[[], [socket], []]
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
548
|
+
q, queue = query, Queue.new
|
|
549
|
+
task = writer.send_dnsruby(q.encode, q, :blocked, queue, true)
|
|
550
|
+
raise 'TCP send was not delegated' unless task.respond_to?(:value)
|
|
551
|
+
raise 'connect/write still ran on selector' if entered.pop.equal?(Thread.current)
|
|
552
|
+
unrelated = settings(FrameSocket.new, :unrelated)
|
|
553
|
+
unrelated.endtime = Dnsruby::TransportClock.now - 1
|
|
554
|
+
SELECTOR.add_to_select(unrelated)
|
|
555
|
+
SELECTOR.process_timeouts
|
|
556
|
+
raise 'TCP wait blocked another timeout' unless unrelated.client_queue.size == 1
|
|
557
|
+
release << true
|
|
558
|
+
task.value
|
|
559
|
+
raise 'delegated send lost bytes' unless socket.written == [q.encode.bytesize].pack('n') + q.encode
|
|
560
|
+
SELECTOR.remove_id(:blocked)
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
RUBY
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def test_retry_reserves_its_fresh_deadline_before_connecting
|
|
567
|
+
run_scenario(<<~'RUBY')
|
|
568
|
+
[false, true].each do |pipeline|
|
|
569
|
+
socket = FrameSocket.new
|
|
570
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: pipeline,
|
|
571
|
+
packet_timeout: 1, dnssec: false)
|
|
572
|
+
old = settings(FrameSocket.new, :retry, query, writer)
|
|
573
|
+
old.endtime = Dnsruby::TransportClock.now - 1
|
|
574
|
+
SELECTOR.add_to_select(old)
|
|
575
|
+
entered, release = Queue.new, Queue.new
|
|
576
|
+
connect = -> { entered << true; release.pop; socket }
|
|
577
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| connect.call }
|
|
578
|
+
writer.define_singleton_method(:tcp_pipeline_socket) { |_| [connect.call, true] }
|
|
579
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
580
|
+
Thread.current[:dnsruby_response_query] = old
|
|
581
|
+
task = writer.send_dnsruby(old.query_bytes, old.query, :retry, old.client_queue, true)
|
|
582
|
+
Thread.current[:dnsruby_response_query] = nil
|
|
583
|
+
entered.pop
|
|
584
|
+
pending = SELECTOR.query_settings_for_id(:retry)
|
|
585
|
+
raise 'old deadline retained during connect' unless pending && pending.endtime > Dnsruby::TransportClock.now
|
|
586
|
+
SELECTOR.process_timeouts
|
|
587
|
+
raise 'retry expired against original deadline' unless old.client_queue.empty?
|
|
588
|
+
release << true
|
|
589
|
+
task.value
|
|
590
|
+
raise 'retry was not sent' if socket.written.empty?
|
|
591
|
+
SELECTOR.remove_id(:retry)
|
|
592
|
+
end
|
|
593
|
+
RUBY
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def test_deferred_sender_fifo_does_not_occupy_other_workers_or_starve_other_senders
|
|
597
|
+
run_scenario(<<~'RUBY')
|
|
598
|
+
entered, release, backlog_release, healthy = Queue.new, Queue.new, Queue.new, Queue.new
|
|
599
|
+
senders = 4.times.map { Object.new }
|
|
600
|
+
initial = senders.map { |sender| SELECTOR.defer_send(sender) { entered << true; release.pop } }
|
|
601
|
+
4.times { entered.pop }
|
|
602
|
+
backlog = senders.map { |sender| SELECTOR.defer_send(sender) { backlog_release.pop } }
|
|
603
|
+
fifth = SELECTOR.defer_send(Object.new) { healthy << true }
|
|
604
|
+
4.times { release << true }
|
|
605
|
+
Timeout.timeout(1) { healthy.pop }
|
|
606
|
+
4.times { backlog_release << true }
|
|
607
|
+
(initial + backlog + [fifth]).each(&:value)
|
|
608
|
+
|
|
609
|
+
one_sender = Object.new
|
|
610
|
+
ordered = Queue.new
|
|
611
|
+
tasks = 1000.times.map { |index| SELECTOR.defer_send(one_sender) { ordered << index } }
|
|
612
|
+
tasks.each(&:value)
|
|
613
|
+
raise 'sender requests reordered' unless 1000.times.map { ordered.pop } == (0...1000).to_a
|
|
614
|
+
RUBY
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def test_cancellation_after_a_partial_frame_retires_the_corrupt_pipeline
|
|
618
|
+
run_scenario(<<~'RUBY')
|
|
619
|
+
socket = FrameSocket.new
|
|
620
|
+
writer = sender(socket)
|
|
621
|
+
other = settings(socket, :other, query('other.example.'), writer)
|
|
622
|
+
SELECTOR.add_to_select(other)
|
|
623
|
+
entered, release = Queue.new, Queue.new
|
|
624
|
+
native_write = socket.method(:write_nonblock)
|
|
625
|
+
calls = 0
|
|
626
|
+
socket.define_singleton_method(:write_nonblock) do |bytes|
|
|
627
|
+
calls += 1
|
|
628
|
+
raise IO::EAGAINWaitWritable if calls == 2
|
|
629
|
+
native_write.call(bytes.byteslice(0, 2))
|
|
630
|
+
end
|
|
631
|
+
IO.define_singleton_method(:select) do |*|
|
|
632
|
+
entered << true
|
|
633
|
+
release.pop
|
|
634
|
+
[[], [socket], []]
|
|
635
|
+
end
|
|
636
|
+
queue, owner = Queue.new, Object.new
|
|
637
|
+
SELECTOR.add_observer(queue, owner)
|
|
638
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
639
|
+
q = query
|
|
640
|
+
task = writer.send_dnsruby(q.encode, q, :cancelled, queue, true)
|
|
641
|
+
entered.pop
|
|
642
|
+
SELECTOR.remove_observer(queue, owner)
|
|
643
|
+
release << true
|
|
644
|
+
task.value
|
|
645
|
+
drain_errors
|
|
646
|
+
raise 'cancelled frame continued writing' unless socket.written.bytesize == 2
|
|
647
|
+
raise 'corrupt stream reused' unless socket.closed?
|
|
648
|
+
raise 'other query not failed once' unless other.client_queue.size == 1 && other.client_queue.pop.last.is_a?(Dnsruby::SocketEofResolvError)
|
|
649
|
+
raise 'cancelled owner received another result' unless queue.empty?
|
|
650
|
+
assert_clean(socket)
|
|
651
|
+
RUBY
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
private
|
|
655
|
+
|
|
656
|
+
def run_scenario(script)
|
|
657
|
+
options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
|
|
658
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
|
|
659
|
+
File.expand_path('../lib', __dir__), '-e', "#{FIXTURE}\nTimeout.timeout(10) do\n#{script}\nend")
|
|
660
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
661
|
+
assert_empty(stderr)
|
|
662
|
+
end
|
|
663
|
+
end
|