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
|
@@ -23,10 +23,22 @@ end
|
|
|
23
23
|
require 'set'
|
|
24
24
|
require 'singleton'
|
|
25
25
|
require 'dnsruby/validator_thread.rb'
|
|
26
|
+
require 'dnsruby/transport_clock'
|
|
27
|
+
require 'dnsruby/query_control'
|
|
28
|
+
require 'dnsruby/deadline_queue'
|
|
26
29
|
module Dnsruby
|
|
27
30
|
class SelectThread #:nodoc: all
|
|
28
31
|
class SelectWakeup < RuntimeError; end
|
|
29
32
|
include Singleton
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
alias_method :instance_without_fork_check, :instance
|
|
36
|
+
private :instance_without_fork_check
|
|
37
|
+
|
|
38
|
+
def instance
|
|
39
|
+
instance_without_fork_check.tap { |selector| selector.send(:ensure_current_process) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
30
42
|
# This singleton class runs a continuous select loop which
|
|
31
43
|
# listens for responses on all of the in-use sockets.
|
|
32
44
|
# When a new query is sent, the thread is woken up, and
|
|
@@ -37,24 +49,40 @@ module Dnsruby
|
|
|
37
49
|
#
|
|
38
50
|
# But how do we find the response queue for a particular query?
|
|
39
51
|
# Hash of client_id->[query, client_queue, socket]
|
|
40
|
-
# and socket->[
|
|
41
|
-
#
|
|
42
|
-
#
|
|
52
|
+
# and socket->[client_ids], including multiple queries per TCP pipeline.
|
|
53
|
+
#
|
|
54
|
+
# @@mutex owns the attempt, queue and socket indexes. Cancellation may take
|
|
55
|
+
# a SendToken lock while holding @@mutex; a token-protected write must not
|
|
56
|
+
# acquire @@mutex. The selector never waits for a pipeline writer mutex.
|
|
57
|
+
# Blocking I/O and client callbacks run outside @@mutex.
|
|
43
58
|
|
|
44
59
|
def initialize
|
|
60
|
+
@process_mutex ||= Mutex.new
|
|
61
|
+
@send_queue = @send_workers = @send_groups = nil
|
|
62
|
+
@packet_deadlines = DeadlineQueue.new
|
|
63
|
+
@idle_deadlines = DeadlineQueue.new
|
|
64
|
+
@created_sockets = ObjectSpace::WeakMap.new
|
|
45
65
|
@@mutex = Mutex.new
|
|
46
66
|
@@mutex.synchronize {
|
|
47
67
|
@@in_select=false
|
|
48
|
-
# @@notifier,@@notified=IO.pipe
|
|
49
68
|
@@sockets = Set.new
|
|
50
69
|
@@timeouts = Hash.new
|
|
51
|
-
# @@mutex.synchronize do
|
|
52
70
|
@@query_hash = Hash.new
|
|
71
|
+
@@queries_by_queue = {}.compare_by_identity
|
|
72
|
+
@@queries_by_sender = {}.compare_by_identity
|
|
53
73
|
@@socket_hash = Hash.new
|
|
54
74
|
@@socket_is_persistent = Hash.new
|
|
75
|
+
@@socket_owners = Hash.new
|
|
76
|
+
@@sockets_by_sender = {}.compare_by_identity
|
|
77
|
+
@@retiring_sockets = Set.new
|
|
55
78
|
@@observers = Hash.new
|
|
79
|
+
@@observer_queue_counts = {}.compare_by_identity
|
|
80
|
+
@@observed_queues = ObjectSpace::WeakMap.new
|
|
81
|
+
@@validation_pid = Process.pid
|
|
82
|
+
@@validations = []
|
|
56
83
|
@@tcp_buffers=Hash.new
|
|
57
84
|
@@socket_remaining_queries = Hash.new
|
|
85
|
+
@@socket_idle_deadlines = Hash.new
|
|
58
86
|
@@tick_observers = []
|
|
59
87
|
@@queued_exceptions=[]
|
|
60
88
|
@@queued_responses=[]
|
|
@@ -64,15 +92,47 @@ module Dnsruby
|
|
|
64
92
|
|
|
65
93
|
# Suppress reverse lookups
|
|
66
94
|
BasicSocket.do_not_reverse_lookup = true
|
|
67
|
-
# end
|
|
68
95
|
# Now start the select thread
|
|
96
|
+
@process_pid = Process.pid
|
|
69
97
|
@@select_thread = Thread.new { do_select }
|
|
70
|
-
|
|
71
|
-
# # Start the validator thread
|
|
72
|
-
# @@validator = ValidatorThread.instance
|
|
73
98
|
}
|
|
74
99
|
end
|
|
75
100
|
|
|
101
|
+
# Only the calling thread survives fork. Discard all parent transactions
|
|
102
|
+
# before restarting any workers, and close the child's descriptor copies
|
|
103
|
+
# without shutting down the connections still used by the parent.
|
|
104
|
+
def ensure_current_process
|
|
105
|
+
return unless @process_pid && @process_pid != Process.pid
|
|
106
|
+
@process_mutex.synchronize do
|
|
107
|
+
if @process_pid != Process.pid
|
|
108
|
+
sockets = @@sockets.to_a + @@socket_hash.keys + @@wakeup_sockets + @created_sockets.keys
|
|
109
|
+
sockets.uniq.each do |socket|
|
|
110
|
+
begin
|
|
111
|
+
socket.close unless socket.closed?
|
|
112
|
+
rescue IOError, SystemCallError
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
Thread.current[:dnsruby_response_query] = nil
|
|
116
|
+
initialize
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
private :ensure_current_process
|
|
121
|
+
|
|
122
|
+
# Also protect callers retaining a selector reference across fork.
|
|
123
|
+
def state_mutex
|
|
124
|
+
ensure_current_process
|
|
125
|
+
@@mutex
|
|
126
|
+
end
|
|
127
|
+
private :state_mutex
|
|
128
|
+
|
|
129
|
+
# Keep weak references from allocation through connect/write, before a
|
|
130
|
+
# socket joins the read set. A fork must close those descriptor copies too.
|
|
131
|
+
def track_socket(socket)
|
|
132
|
+
state_mutex.synchronize { (@created_sockets ||= ObjectSpace::WeakMap.new)[socket] = true }
|
|
133
|
+
socket
|
|
134
|
+
end
|
|
135
|
+
|
|
76
136
|
def get_socket_pair
|
|
77
137
|
# Emulate socketpair on platforms which don't support it
|
|
78
138
|
srv = nil
|
|
@@ -94,7 +154,8 @@ module Dnsruby
|
|
|
94
154
|
class QuerySettings
|
|
95
155
|
attr_accessor :query_bytes, :query, :ignore_truncation, :client_queue,
|
|
96
156
|
:client_query_id, :socket, :dest_server, :dest_port, :endtime, :udp_packet_size,
|
|
97
|
-
:single_resolver, :is_persistent_socket, :tcp_pipelining_max_queries
|
|
157
|
+
:single_resolver, :is_persistent_socket, :tcp_pipelining_max_queries, :send_token,
|
|
158
|
+
:retire_connection, :disable_edns, :authentication_error, :cache_scope, :transport_configuration
|
|
98
159
|
# new(query_bytes, query, ignore_truncation, client_queue, client_query_id,
|
|
99
160
|
# socket, dest_server, dest_port, endtime, , udp_packet_size, single_resolver)
|
|
100
161
|
def initialize(*args)
|
|
@@ -111,9 +172,230 @@ module Dnsruby
|
|
|
111
172
|
@single_resolver = args[10]
|
|
112
173
|
@is_persistent_socket = false
|
|
113
174
|
@tcp_pipelining_max_queries = nil
|
|
175
|
+
@send_token = SendToken.new
|
|
176
|
+
@retire_connection = false
|
|
177
|
+
@disable_edns = false
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def no_tcp
|
|
181
|
+
transport_configuration ? transport_configuration[:no_tcp] : single_resolver.no_tcp
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def tcp_idle_timeout
|
|
185
|
+
transport_configuration ? transport_configuration[:tcp_idle_timeout] : single_resolver.tcp_idle_timeout
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Look up client queries by socket and DNS message ID. Helper callers may
|
|
190
|
+
# register several queries with the same DNS ID; normal TCP sends keep IDs
|
|
191
|
+
# unique per connection. Hold @@mutex when updating the index.
|
|
192
|
+
class SocketQueries < Set
|
|
193
|
+
def initialize
|
|
194
|
+
super
|
|
195
|
+
@by_header_id = {}
|
|
196
|
+
@header_ids = {}
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def add_query(settings)
|
|
200
|
+
id, header_id = settings.client_query_id, settings.query.header.id
|
|
201
|
+
add(id)
|
|
202
|
+
@header_ids[id] = header_id
|
|
203
|
+
(@by_header_id[header_id] ||= Set.new).add(id)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def matching(header_id)
|
|
207
|
+
(@by_header_id[header_id] || []).to_a
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def delete(id)
|
|
211
|
+
header_id = @header_ids.delete(id)
|
|
212
|
+
if (ids = @by_header_id[header_id])
|
|
213
|
+
ids.delete(id)
|
|
214
|
+
@by_header_id.delete(header_id) if ids.empty?
|
|
215
|
+
end
|
|
216
|
+
super
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
private_constant :SocketQueries
|
|
220
|
+
|
|
221
|
+
# RFC 9520 section 3.1 limits one resolution to three attempts per
|
|
222
|
+
# server address and transport, including protocol-driven retries.
|
|
223
|
+
class TransmissionBudget
|
|
224
|
+
def self.current
|
|
225
|
+
Thread.current[:dnsruby_transmission_budget]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def self.with(budget)
|
|
229
|
+
previous = current
|
|
230
|
+
Thread.current[:dnsruby_transmission_budget] = budget
|
|
231
|
+
yield
|
|
232
|
+
ensure
|
|
233
|
+
Thread.current[:dnsruby_transmission_budget] = previous
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def initialize
|
|
237
|
+
@counts = {}
|
|
238
|
+
@contexts = {}
|
|
239
|
+
@total = 0
|
|
240
|
+
@mutex = Mutex.new
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Supporting queries and validator workers already share this object.
|
|
244
|
+
def context(key)
|
|
245
|
+
@mutex.synchronize { @contexts[key] ||= yield }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def limit_total!(maximum, error_class)
|
|
249
|
+
@mutex.synchronize do
|
|
250
|
+
@maximum = [@maximum, maximum].compact.min
|
|
251
|
+
@limit_error_class = error_class
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def admit(server, port, tcp, question = nil)
|
|
256
|
+
identity = question && [question.qname.canonical, question.qtype.code, question.qclass.code]
|
|
257
|
+
key = [server.to_s.dup.freeze, port, !!tcp, identity]
|
|
258
|
+
@mutex.synchronize do
|
|
259
|
+
count = @counts[key] || 0
|
|
260
|
+
return false if count >= 3
|
|
261
|
+
if @maximum && @total >= @maximum
|
|
262
|
+
raise @limit_error_class, 'Recursive query transmission limit exceeded'
|
|
263
|
+
end
|
|
264
|
+
@total += 1
|
|
265
|
+
@counts[key] = count + 1
|
|
266
|
+
true
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Cancellation and each nonblocking write are ordered without holding the
|
|
272
|
+
# selector mutex during I/O. Waiting for a connect or a writable socket never
|
|
273
|
+
# holds this lock, so closing a Resolver remains responsive.
|
|
274
|
+
class SendToken
|
|
275
|
+
attr_reader :control, :transmission_budget
|
|
276
|
+
|
|
277
|
+
def initialize(control = QueryControl.current, transmission_budget = nil)
|
|
278
|
+
@mutex = Mutex.new
|
|
279
|
+
@cancelled = false
|
|
280
|
+
@control = control
|
|
281
|
+
@transmission_budget = transmission_budget || TransmissionBudget.current || TransmissionBudget.new
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def cancel
|
|
285
|
+
@mutex.synchronize { @cancelled = true }
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def cancelled?
|
|
289
|
+
@control&.check!
|
|
290
|
+
@mutex.synchronize { @cancelled }
|
|
291
|
+
rescue QueryControl::Stopped
|
|
292
|
+
true
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def write
|
|
296
|
+
@mutex.synchronize do
|
|
297
|
+
return false if @cancelled
|
|
298
|
+
begin
|
|
299
|
+
@control&.check!
|
|
300
|
+
rescue QueryControl::Stopped
|
|
301
|
+
return false
|
|
302
|
+
end
|
|
303
|
+
yield
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Register ownership before work leaves the selector, including attempts
|
|
309
|
+
# which have not acquired a socket yet. Nil is never a live attempt token.
|
|
310
|
+
def reserve_send(settings, previous_settings = nil)
|
|
311
|
+
state_mutex.synchronize do
|
|
312
|
+
return false unless @@query_hash[settings.client_query_id].equal?(previous_settings)
|
|
313
|
+
if previous_settings
|
|
314
|
+
settings.retire_connection = previous_settings.retire_connection
|
|
315
|
+
remove_id_from_mutex_synchronized_block(settings.client_query_id, settings.send_token)
|
|
316
|
+
end
|
|
317
|
+
@@query_hash[settings.client_query_id] = settings
|
|
318
|
+
(@@queries_by_queue[settings.client_queue] ||= Set.new) << settings.client_query_id
|
|
319
|
+
((@@queries_by_sender ||= {}.compare_by_identity)[settings.single_resolver] ||= Set.new) << settings.client_query_id
|
|
320
|
+
@@timeouts[settings.client_query_id] = settings.endtime
|
|
321
|
+
(@packet_deadlines ||= DeadlineQueue.new)[settings.client_query_id] = settings.endtime
|
|
322
|
+
check_select_thread_synchronized
|
|
323
|
+
end
|
|
324
|
+
wake_up_select_thread
|
|
325
|
+
true
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
class SendTask
|
|
329
|
+
def initialize(&operation)
|
|
330
|
+
@operation = operation
|
|
331
|
+
@result = Queue.new
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def run
|
|
335
|
+
@result << [@operation.call, nil]
|
|
336
|
+
rescue Exception => error
|
|
337
|
+
@result << [nil, error]
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def value
|
|
341
|
+
result, error = @result.pop
|
|
342
|
+
raise error if error
|
|
343
|
+
result
|
|
114
344
|
end
|
|
115
345
|
end
|
|
116
346
|
|
|
347
|
+
# A finite pool keeps slow connects/writes off the selector without creating
|
|
348
|
+
# an unbounded number of threads. Already-owned attempts can queue without
|
|
349
|
+
# introducing a new admission limit; each retains its original deadline.
|
|
350
|
+
def defer_send(sender = nil, &operation)
|
|
351
|
+
task = SendTask.new(&operation)
|
|
352
|
+
state_mutex.synchronize do
|
|
353
|
+
unless @send_queue && @send_workers.any?(&:alive?)
|
|
354
|
+
queue = Queue.new
|
|
355
|
+
workers = []
|
|
356
|
+
begin
|
|
357
|
+
4.times { workers << Thread.new { while (work = queue.pop); work.run; end } }
|
|
358
|
+
rescue ThreadError
|
|
359
|
+
workers.length.times { queue << nil }
|
|
360
|
+
raise
|
|
361
|
+
end
|
|
362
|
+
@send_queue = queue
|
|
363
|
+
@send_workers = workers
|
|
364
|
+
@send_groups = {}
|
|
365
|
+
end
|
|
366
|
+
if sender
|
|
367
|
+
group = @send_groups[sender]
|
|
368
|
+
if group
|
|
369
|
+
group << task
|
|
370
|
+
else
|
|
371
|
+
@send_groups[sender] = [task]
|
|
372
|
+
# One worker owns each sender's FIFO. Other tasks for the same
|
|
373
|
+
# pipeline do not occupy workers just to wait for its writer lock.
|
|
374
|
+
@send_queue << SendTask.new { run_send_group(sender) }
|
|
375
|
+
end
|
|
376
|
+
else
|
|
377
|
+
@send_queue << task
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
task
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def run_send_group(sender)
|
|
384
|
+
task = state_mutex.synchronize { @send_groups[sender].shift }
|
|
385
|
+
task.run
|
|
386
|
+
state_mutex.synchronize do
|
|
387
|
+
if @send_groups[sender].empty?
|
|
388
|
+
@send_groups.delete(sender)
|
|
389
|
+
else
|
|
390
|
+
# Give another sender a turn without allowing two workers to send on
|
|
391
|
+
# this sender concurrently. A continuously busy pipeline cannot starve
|
|
392
|
+
# a different server's connection behind its entire backlog.
|
|
393
|
+
@send_queue << SendTask.new { run_send_group(sender) }
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
private :run_send_group
|
|
398
|
+
|
|
117
399
|
def tcp?(socket)
|
|
118
400
|
type = socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE)
|
|
119
401
|
[Socket::SOCK_STREAM].pack("i") == type.data
|
|
@@ -126,31 +408,203 @@ module Dnsruby
|
|
|
126
408
|
|
|
127
409
|
def add_to_select(query_settings)
|
|
128
410
|
# Add the query to sockets, and then wake the select thread up
|
|
129
|
-
|
|
411
|
+
state_mutex.synchronize {
|
|
130
412
|
check_select_thread_synchronized
|
|
131
|
-
|
|
132
|
-
# Would be a good idea at least to check this...
|
|
133
|
-
@@query_hash[query_settings.client_query_id]=query_settings
|
|
134
|
-
@@socket_hash[query_settings.socket] ||= []
|
|
135
|
-
@@socket_hash[query_settings.socket] << query_settings.client_query_id
|
|
136
|
-
@@socket_remaining_queries[query_settings.socket] ||= query_settings.tcp_pipelining_max_queries if query_settings.tcp_pipelining_max_queries != :infinite
|
|
137
|
-
@@timeouts[query_settings.client_query_id]=query_settings.endtime
|
|
138
|
-
@@sockets << query_settings.socket
|
|
139
|
-
@@socket_is_persistent[query_settings.socket] = query_settings.is_persistent_socket
|
|
413
|
+
add_query_from_mutex_synchronized_block(query_settings)
|
|
140
414
|
}
|
|
141
415
|
wake_up_select_thread
|
|
142
416
|
end
|
|
143
417
|
|
|
144
|
-
def
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
418
|
+
def query_settings_for_id(client_query_id)
|
|
419
|
+
state_mutex.synchronize { @@query_hash[client_query_id] }
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def selector_thread?
|
|
423
|
+
state_mutex.synchronize { @@select_thread.equal?(Thread.current) }
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Reserve a pipeline query before writing, or replace a protocol retry.
|
|
427
|
+
# A deferred retry must not replace an attempt already completed or replaced.
|
|
428
|
+
def register_query_if_current(query_settings, previous_settings)
|
|
429
|
+
registered = state_mutex.synchronize do
|
|
430
|
+
return false unless @@query_hash[query_settings.client_query_id].equal?(previous_settings)
|
|
431
|
+
# Selection happens outside this mutex. A timeout or close may retire
|
|
432
|
+
# that stream before admission, so leave the reservation untouched and
|
|
433
|
+
# let the sender choose a new connection under the original deadline.
|
|
434
|
+
if query_settings.is_persistent_socket &&
|
|
435
|
+
(query_settings.socket.closed? || (@@retiring_sockets ||= Set.new).include?(query_settings.socket))
|
|
436
|
+
return :retry_connection
|
|
437
|
+
end
|
|
438
|
+
query_settings.retire_connection = previous_settings.retire_connection if previous_settings
|
|
439
|
+
check_select_thread_synchronized
|
|
440
|
+
add_query_from_mutex_synchronized_block(query_settings)
|
|
441
|
+
end
|
|
442
|
+
wake_up_select_thread
|
|
443
|
+
registered
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Called only with @@mutex held. Retrying a client ID also retires its old
|
|
447
|
+
# socket membership, so stale EOF/timeout work cannot consume the retry.
|
|
448
|
+
def add_query_from_mutex_synchronized_block(settings)
|
|
449
|
+
id = settings.client_query_id
|
|
450
|
+
remove_id_from_mutex_synchronized_block(id, settings.send_token) if @@query_hash[id]
|
|
451
|
+
if settings.socket.closed?
|
|
452
|
+
@@queued_exceptions << [id, settings.client_queue,
|
|
453
|
+
SocketEofResolvError.new("TCP socket closed before all answers received"), nil]
|
|
454
|
+
return false
|
|
455
|
+
end
|
|
456
|
+
@@query_hash[id] = settings
|
|
457
|
+
(@@queries_by_queue[settings.client_queue] ||= Set.new) << id
|
|
458
|
+
((@@queries_by_sender ||= {}.compare_by_identity)[settings.single_resolver] ||= Set.new) << id
|
|
459
|
+
membership = @@socket_hash[settings.socket]
|
|
460
|
+
unless membership.is_a?(SocketQueries)
|
|
461
|
+
indexed = SocketQueries.new
|
|
462
|
+
(membership || []).each { |member| indexed.add_query(@@query_hash[member]) if @@query_hash[member] }
|
|
463
|
+
membership = @@socket_hash[settings.socket] = indexed
|
|
464
|
+
end
|
|
465
|
+
membership.add_query(settings)
|
|
466
|
+
(@@socket_idle_deadlines ||= {}).delete(settings.socket)
|
|
467
|
+
@idle_deadlines&.delete(settings.socket)
|
|
468
|
+
if settings.tcp_pipelining_max_queries != :infinite
|
|
469
|
+
@@socket_remaining_queries[settings.socket] ||= settings.tcp_pipelining_max_queries
|
|
470
|
+
end
|
|
471
|
+
@@timeouts[id] = settings.endtime
|
|
472
|
+
(@packet_deadlines ||= DeadlineQueue.new)[id] = settings.endtime
|
|
473
|
+
@@sockets << settings.socket
|
|
474
|
+
@@socket_is_persistent[settings.socket] = settings.is_persistent_socket
|
|
475
|
+
previous_owner = (@@socket_owners ||= {})[settings.socket]
|
|
476
|
+
if previous_owner && !previous_owner.equal?(settings.single_resolver) &&
|
|
477
|
+
(previous_sockets = (@@sockets_by_sender ||= {}.compare_by_identity)[previous_owner])
|
|
478
|
+
previous_sockets.delete(settings.socket)
|
|
479
|
+
@@sockets_by_sender.delete(previous_owner) if previous_sockets.empty?
|
|
149
480
|
end
|
|
481
|
+
(@@socket_owners ||= {})[settings.socket] = settings.single_resolver
|
|
482
|
+
((@@sockets_by_sender ||= {}.compare_by_identity)[settings.single_resolver] ||= Set.new) << settings.socket
|
|
483
|
+
(@@retiring_sockets ||= Set.new) << settings.socket if settings.retire_connection
|
|
484
|
+
true
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Explicit close retires streams owned by this sender, including idle and
|
|
488
|
+
# rolled-over pipelines. Other logical queries sharing a sender may finish,
|
|
489
|
+
# but future sends must choose a fresh connection.
|
|
490
|
+
def retire_sender(sender)
|
|
491
|
+
state_mutex.synchronize do
|
|
492
|
+
refresh_validation_process
|
|
493
|
+
# A connect can be in progress without a socket in our read set yet.
|
|
494
|
+
# Carry retirement through reservation replacements so its eventual
|
|
495
|
+
# registration drains and closes, while later fresh sends can reuse.
|
|
496
|
+
((@@queries_by_sender ||= {}.compare_by_identity)[sender] || []).each do |id|
|
|
497
|
+
settings = @@query_hash[id]
|
|
498
|
+
settings.retire_connection = true if settings
|
|
499
|
+
end
|
|
500
|
+
((@@sockets_by_sender ||= {}.compare_by_identity)[sender] || []).to_a.each do |socket|
|
|
501
|
+
mark_socket_for_retirement(socket)
|
|
502
|
+
end
|
|
503
|
+
@@validations.each do |queue, owner, control|
|
|
504
|
+
# A Resolver owns its validation through the queue observer. Removing
|
|
505
|
+
# that observer cancels its work; retiring a shared or reconfigured
|
|
506
|
+
# sender must let other accepted responses finish. Direct PacketSender
|
|
507
|
+
# callers have no observer, so their sender's close still cancels them.
|
|
508
|
+
control.cancel if owner.equal?(sender) && !@@observers[queue]
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
wake_up_select_thread
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def retire_socket(socket)
|
|
515
|
+
state_mutex.synchronize { mark_socket_for_retirement(socket) }
|
|
516
|
+
wake_up_select_thread
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def mark_socket_for_retirement(socket)
|
|
520
|
+
(@@retiring_sockets ||= Set.new) << socket
|
|
521
|
+
if (@@socket_hash[socket] || []).empty?
|
|
522
|
+
socket.close rescue nil
|
|
523
|
+
retire_socket_from_mutex_synchronized_block(socket)
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
private :mark_socket_for_retirement
|
|
527
|
+
|
|
528
|
+
def query_id_in_use?(socket, header_id)
|
|
529
|
+
state_mutex.synchronize do
|
|
530
|
+
!socket_ids_for_header(socket, header_id).empty?
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Helper callers may supply a plain array; add_to_select uses SocketQueries.
|
|
535
|
+
# Call with @@mutex held.
|
|
536
|
+
def socket_ids_for_header(socket, header_id)
|
|
537
|
+
ids = @@socket_hash[socket]
|
|
538
|
+
return [] unless ids
|
|
539
|
+
return ids.matching(header_id) if ids.is_a?(SocketQueries)
|
|
540
|
+
ids.select { |id| (settings = @@query_hash[id]) && settings.query.header.id == header_id }
|
|
541
|
+
end
|
|
542
|
+
private :socket_ids_for_header
|
|
543
|
+
|
|
544
|
+
def reusable_socket?(socket)
|
|
545
|
+
state_mutex.synchronize do
|
|
546
|
+
!socket.closed? && !(@@retiring_sockets ||= Set.new).include?(socket)
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# A partial TCP frame makes the whole connection unusable. Atomically claim
|
|
551
|
+
# its outstanding queries, then let the selector deliver their errors.
|
|
552
|
+
# Sender threads must never invoke resolver callbacks themselves.
|
|
553
|
+
def fail_tcp_send(settings, error)
|
|
554
|
+
state_mutex.synchronize do
|
|
555
|
+
settings.socket.close rescue nil
|
|
556
|
+
exceptions = retire_socket_from_mutex_synchronized_block(settings.socket)
|
|
557
|
+
exceptions.each do |eof, socket, id|
|
|
558
|
+
current = @@query_hash[id]
|
|
559
|
+
next unless current && current.socket == socket
|
|
560
|
+
remove_id_from_mutex_synchronized_block(id)
|
|
561
|
+
@@queued_exceptions << [id, current.client_queue,
|
|
562
|
+
current.equal?(settings) ? error : eof, nil]
|
|
563
|
+
end
|
|
564
|
+
check_select_thread_synchronized unless @@queued_exceptions.empty?
|
|
565
|
+
end
|
|
566
|
+
wake_up_select_thread
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# A send waiting for the pipeline writer has no new socket registration yet.
|
|
570
|
+
# Its original attempt may have expired or been replaced in the meantime.
|
|
571
|
+
def fail_query_if_current(id, queue, previous_settings, error)
|
|
572
|
+
state_mutex.synchronize do
|
|
573
|
+
return unless @@query_hash[id].equal?(previous_settings)
|
|
574
|
+
remove_id_from_mutex_synchronized_block(id) if previous_settings
|
|
575
|
+
@@queued_exceptions << [id, queue, error, nil]
|
|
576
|
+
check_select_thread_synchronized
|
|
577
|
+
end
|
|
578
|
+
wake_up_select_thread
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# Cache lookup can satisfy a protocol retry before it reaches the sender.
|
|
582
|
+
# Claim that exact attempt before publishing a result, just like a wire reply.
|
|
583
|
+
def push_response_for_query(settings, msg, query, resolver)
|
|
584
|
+
return unless claim_query(settings)
|
|
585
|
+
push_response_to_select(settings.client_query_id, settings.client_queue, msg, query, resolver,
|
|
586
|
+
settings.send_token&.transmission_budget)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def claim_query(settings, response_received = false)
|
|
590
|
+
state_mutex.synchronize do
|
|
591
|
+
return false unless @@query_hash[settings.client_query_id].equal?(settings)
|
|
592
|
+
remove_id_from_mutex_synchronized_block(settings.client_query_id, nil, response_received)
|
|
593
|
+
true
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def wake_up_select_thread
|
|
598
|
+
# One unread byte is sufficient: a full socket already guarantees that
|
|
599
|
+
# select will wake. Never let notification block a producer or the selector
|
|
600
|
+
# itself (cache hits and protocol retries also produce notifications).
|
|
601
|
+
@@wakeup_sockets[0].write_nonblock(".")
|
|
602
|
+
rescue IO::WaitWritable, IOError, SystemCallError
|
|
603
|
+
nil
|
|
150
604
|
end
|
|
151
605
|
|
|
152
606
|
def check_select_thread_synchronized
|
|
153
|
-
if
|
|
607
|
+
if !@@select_thread || !@@select_thread.alive?
|
|
154
608
|
Dnsruby.log.debug{"Restarting select thread"}
|
|
155
609
|
@@select_thread = Thread.new {
|
|
156
610
|
do_select
|
|
@@ -160,45 +614,40 @@ module Dnsruby
|
|
|
160
614
|
|
|
161
615
|
def select_thread_alive?
|
|
162
616
|
ret=true
|
|
163
|
-
|
|
164
|
-
ret = @@select_thread.alive?
|
|
617
|
+
state_mutex.synchronize{
|
|
618
|
+
ret = !!(@@select_thread && @@select_thread.alive?)
|
|
165
619
|
}
|
|
166
620
|
return ret
|
|
167
621
|
end
|
|
168
622
|
|
|
169
623
|
def do_select
|
|
170
624
|
unused_loop_count = 0
|
|
171
|
-
last_tick_time = Time.now - 10
|
|
172
625
|
while true do
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
last_tick_time = Time.now
|
|
176
|
-
end
|
|
626
|
+
send_tick_to_observers
|
|
627
|
+
expire_idle_sockets
|
|
177
628
|
send_queued_exceptions
|
|
178
629
|
send_queued_responses
|
|
179
630
|
send_queued_validation_responses
|
|
180
|
-
|
|
181
|
-
sockets,
|
|
182
|
-
|
|
183
|
-
timeouts.sort!
|
|
184
|
-
timeout = timeouts[0] - Time.now
|
|
185
|
-
if (timeout <= 0)
|
|
186
|
-
process_timeouts
|
|
187
|
-
timeout = 0
|
|
188
|
-
next
|
|
189
|
-
end
|
|
631
|
+
observer_deadline = next_observer_deadline
|
|
632
|
+
sockets, packet_deadline, idle_deadline = state_mutex.synchronize do
|
|
633
|
+
[@@sockets.to_a, @packet_deadlines&.first_deadline, @idle_deadlines&.first_deadline]
|
|
190
634
|
end
|
|
191
|
-
|
|
192
|
-
if
|
|
193
|
-
|
|
635
|
+
now = TransportClock.now
|
|
636
|
+
if packet_deadline && packet_deadline <= now
|
|
637
|
+
process_timeouts
|
|
638
|
+
next
|
|
194
639
|
end
|
|
195
|
-
|
|
640
|
+
deadline = [packet_deadline, observer_deadline, idle_deadline].compact.min
|
|
641
|
+
timeout = deadline ? [deadline - now, 0].max : 0.1
|
|
642
|
+
ready=nil
|
|
196
643
|
begin
|
|
197
644
|
ready, _write, _errors = IO.select(sockets, nil, nil, timeout)
|
|
198
645
|
rescue SelectWakeup
|
|
199
646
|
# If SelectWakeup, then just restart this loop - the select call will be made with the new data
|
|
200
647
|
next
|
|
201
|
-
rescue IOError, EncodeError
|
|
648
|
+
rescue IOError, Errno::EBADF, EncodeError
|
|
649
|
+
# A pipeline writer can close a socket after this loop snapshots its
|
|
650
|
+
# read set, or while native select is waiting on its descriptor.
|
|
202
651
|
exceptions = clean_up_closed_sockets
|
|
203
652
|
exceptions.each { |exception| send_exception_to_client(*exception) }
|
|
204
653
|
|
|
@@ -206,13 +655,11 @@ module Dnsruby
|
|
|
206
655
|
end
|
|
207
656
|
if ready && ready.include?(@@wakeup_sockets[1])
|
|
208
657
|
ready.delete(@@wakeup_sockets[1])
|
|
209
|
-
wakeup_msg = "loop"
|
|
210
658
|
begin
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
rescue
|
|
215
|
-
# do nothing
|
|
659
|
+
# Bound draining so continuously arriving notifications cannot starve
|
|
660
|
+
# responses and timeouts. Any remainder wakes the next select.
|
|
661
|
+
@@wakeup_sockets[1].recv_nonblock(4096)
|
|
662
|
+
rescue IO::WaitReadable, IOError, SystemCallError
|
|
216
663
|
end
|
|
217
664
|
end
|
|
218
665
|
if (ready == nil)
|
|
@@ -224,43 +671,65 @@ module Dnsruby
|
|
|
224
671
|
unused_loop_count=0
|
|
225
672
|
# process_error(errors)
|
|
226
673
|
end
|
|
227
|
-
|
|
228
|
-
if (unused_loop_count > 10 && @@query_hash.empty? && @@observers.empty?
|
|
674
|
+
state_mutex.synchronize do
|
|
675
|
+
if (unused_loop_count > 10 && @@query_hash.empty? && @@observers.empty? &&
|
|
676
|
+
@@queued_exceptions.empty? && @@queued_responses.empty? &&
|
|
677
|
+
@@queued_validation_responses.empty?)
|
|
229
678
|
Dnsruby.log.debug("Try stop select loop")
|
|
230
679
|
|
|
680
|
+
# We leave the @@wakeup_sockets here, but remove everything else
|
|
231
681
|
non_persistent_sockets = @@sockets.select { |s| ! @@socket_is_persistent[s] }
|
|
232
682
|
non_persistent_sockets.each do |socket|
|
|
233
|
-
socket
|
|
234
|
-
|
|
683
|
+
if socket != @@wakeup_sockets[1]
|
|
684
|
+
socket.close rescue nil
|
|
685
|
+
retire_socket_from_mutex_synchronized_block(socket)
|
|
686
|
+
end
|
|
235
687
|
end
|
|
236
688
|
|
|
237
|
-
Dnsruby.log.debug("Deleted #{non_persistent_sockets.size} non-persistent sockets," +
|
|
238
|
-
" #{@@sockets.count} persistent sockets remain.")
|
|
689
|
+
Dnsruby.log.debug("Deleted #{non_persistent_sockets.size - 1} non-persistent sockets," +
|
|
690
|
+
" #{@@sockets.count - 1} persistent sockets remain.")
|
|
239
691
|
@@socket_hash.clear
|
|
240
692
|
|
|
241
|
-
if @@sockets.
|
|
693
|
+
if @@sockets.size == 1
|
|
242
694
|
Dnsruby.log.debug("Stopping select loop")
|
|
695
|
+
# A producer acquiring the mutex after this decision must start a
|
|
696
|
+
# replacement, even while the old Ruby thread is still returning.
|
|
697
|
+
@@select_thread = nil
|
|
243
698
|
return
|
|
244
699
|
end
|
|
245
700
|
end
|
|
246
701
|
end
|
|
247
|
-
# }
|
|
248
702
|
end
|
|
249
703
|
end
|
|
250
704
|
|
|
251
|
-
#
|
|
252
|
-
#
|
|
705
|
+
# Detach closed sockets before notifying clients. Callbacks can register
|
|
706
|
+
# other queries, and must never run while holding the selector mutex.
|
|
253
707
|
def clean_up_closed_sockets
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
closed_sockets_in_hash.each_with_object([]) do |socket, exceptions|
|
|
258
|
-
@@socket_hash[socket].each do | client_id |
|
|
259
|
-
exceptions << [SocketEofResolvError.new("TCP socket closed before all answers received"), socket, client_id]
|
|
260
|
-
end
|
|
708
|
+
state_mutex.synchronize do
|
|
709
|
+
@@sockets.select(&:closed?).flat_map do |socket|
|
|
710
|
+
retire_socket_from_mutex_synchronized_block(socket)
|
|
261
711
|
end
|
|
262
712
|
end
|
|
263
|
-
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Called only with @@mutex held and after the socket has been closed.
|
|
716
|
+
def retire_socket_from_mutex_synchronized_block(socket)
|
|
717
|
+
ids = @@socket_hash.delete(socket) || []
|
|
718
|
+
@@sockets.delete(socket)
|
|
719
|
+
@@tcp_buffers.delete(socket)
|
|
720
|
+
@@socket_remaining_queries.delete(socket)
|
|
721
|
+
(@@socket_idle_deadlines ||= {}).delete(socket)
|
|
722
|
+
@idle_deadlines&.delete(socket)
|
|
723
|
+
@@socket_is_persistent.delete(socket)
|
|
724
|
+
owner = (@@socket_owners ||= {}).delete(socket)
|
|
725
|
+
if (sockets = (@@sockets_by_sender ||= {}.compare_by_identity)[owner])
|
|
726
|
+
sockets.delete(socket)
|
|
727
|
+
@@sockets_by_sender.delete(owner) if sockets.empty?
|
|
728
|
+
end
|
|
729
|
+
(@@retiring_sockets ||= Set.new).delete(socket)
|
|
730
|
+
ids.map do |client_id|
|
|
731
|
+
[SocketEofResolvError.new("TCP socket closed before all answers received"), socket, client_id]
|
|
732
|
+
end
|
|
264
733
|
end
|
|
265
734
|
|
|
266
735
|
def process_error(errors)
|
|
@@ -268,92 +737,96 @@ module Dnsruby
|
|
|
268
737
|
# @todo@ Process errors [can we do this in single socket environment?]
|
|
269
738
|
end
|
|
270
739
|
|
|
740
|
+
# Retained for callers of this helper; response delivery now matches only
|
|
741
|
+
# queries registered on the receiving socket instead of scanning every ID.
|
|
271
742
|
def get_active_ids(queries, id)
|
|
272
743
|
queries.keys.select { |client_query_id| client_query_id[1].header.id == id }
|
|
273
744
|
end
|
|
274
745
|
|
|
275
|
-
# @@query_hash[query_settings.client_query_id]=query_settings
|
|
276
746
|
def process_ready(ready)
|
|
277
|
-
persistent_sockets, nonpersistent_sockets =
|
|
747
|
+
persistent_sockets, nonpersistent_sockets = state_mutex.synchronize { ready.partition { |socket| persistent?(socket) } }
|
|
278
748
|
|
|
279
749
|
nonpersistent_sockets.each do |socket|
|
|
280
|
-
query_settings =
|
|
750
|
+
query_settings = state_mutex.synchronize { @@query_hash[(@@socket_hash[socket] || []).first] }
|
|
281
751
|
next if !query_settings
|
|
282
752
|
|
|
283
753
|
udp_packet_size = query_settings.udp_packet_size
|
|
284
754
|
msg, bytes = get_incoming_data(socket, udp_packet_size)
|
|
285
755
|
|
|
286
756
|
process_message(msg, bytes, socket) if msg
|
|
287
|
-
|
|
288
|
-
ready.delete(socket)
|
|
289
757
|
end
|
|
290
758
|
|
|
291
759
|
persistent_sockets.each do |socket|
|
|
292
760
|
msg, bytes = get_incoming_data(socket, 0)
|
|
293
761
|
process_message(msg, bytes, socket) if msg
|
|
294
|
-
ready.delete(socket)
|
|
295
762
|
end
|
|
296
763
|
end
|
|
297
764
|
|
|
298
765
|
def process_message(msg, bytes, socket)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
answerip = msg.answerip.downcase
|
|
305
|
-
answerfrom = msg.answerfrom.downcase
|
|
306
|
-
answeripaddr = IPAddr.new(answerip)
|
|
307
|
-
dest_server = IPAddr.new("0.0.0.0")
|
|
308
|
-
|
|
309
|
-
begin
|
|
310
|
-
destserveripaddr = IPAddr.new(dest_server)
|
|
311
|
-
rescue ArgumentError
|
|
312
|
-
# Host name not IP address
|
|
313
|
-
end
|
|
766
|
+
# Delivery looks up IDs only on this connected socket, then checks the
|
|
767
|
+
# question and TSIG through PacketSender#check_response.
|
|
768
|
+
send_response_to_client(msg, bytes, socket)
|
|
769
|
+
end
|
|
314
770
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
end
|
|
771
|
+
def matching_questions?(response, query)
|
|
772
|
+
response.question.length == query.question.length &&
|
|
773
|
+
response.question.zip(query.question).all? do |answer, request|
|
|
774
|
+
answer.qname.labels == request.qname.labels &&
|
|
775
|
+
answer.qtype == request.qtype && answer.qclass == request.qclass
|
|
776
|
+
end
|
|
322
777
|
end
|
|
778
|
+
private :matching_questions?
|
|
323
779
|
|
|
324
780
|
def send_response_to_client(msg, bytes, socket)
|
|
325
|
-
#
|
|
326
|
-
# @TODO@ Can get rid of this, as we only have one query per socket.
|
|
781
|
+
# Match the header ID against queries outstanding on this connection.
|
|
327
782
|
client_ids=[]
|
|
328
|
-
|
|
329
|
-
client_ids =
|
|
783
|
+
state_mutex.synchronize{
|
|
784
|
+
client_ids = socket_ids_for_header(socket, msg.header.id)
|
|
330
785
|
}
|
|
331
786
|
# get the queries associated with them
|
|
332
787
|
client_ids.each do |id|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
788
|
+
settings = state_mutex.synchronize { @@query_hash[id] }
|
|
789
|
+
next unless settings && settings.socket == socket
|
|
790
|
+
query_header_id = settings.query.header.id
|
|
337
791
|
if (query_header_id == msg.header.id)
|
|
792
|
+
# IDs can collide on a pipeline. Select the matching question before
|
|
793
|
+
# query-specific TSIG checks or retries, which may consume the attempt.
|
|
794
|
+
# Preserve raw/update responses and questionless FORMERR handling.
|
|
795
|
+
query = settings.query
|
|
796
|
+
unless query.send_raw || query.kind_of?(Update) || msg.question.empty?
|
|
797
|
+
next unless matching_questions?(msg, query)
|
|
798
|
+
end
|
|
338
799
|
# process the response
|
|
339
|
-
client_queue =
|
|
340
|
-
res =
|
|
341
|
-
query=
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
800
|
+
client_queue = settings.client_queue
|
|
801
|
+
res = settings.single_resolver
|
|
802
|
+
query = settings.query
|
|
803
|
+
begin
|
|
804
|
+
tcp = settings.is_persistent_socket || tcp?(socket)
|
|
805
|
+
rescue IOError, SystemCallError
|
|
806
|
+
return if socket.closed?
|
|
807
|
+
raise
|
|
808
|
+
end
|
|
348
809
|
# At this point, we should check if the response is OK
|
|
349
|
-
|
|
350
|
-
|
|
810
|
+
previous_context = Thread.current[:dnsruby_response_query]
|
|
811
|
+
begin
|
|
812
|
+
# check_response may synchronously retry (FORMERR or truncation).
|
|
813
|
+
# Carry this attempt's identity through that unchanged public API.
|
|
814
|
+
Thread.current[:dnsruby_response_query] = settings
|
|
815
|
+
ret = res.check_response(msg, bytes, query, client_queue, id, tcp)
|
|
816
|
+
ensure
|
|
817
|
+
Thread.current[:dnsruby_response_query] = previous_context
|
|
818
|
+
end
|
|
819
|
+
if ret
|
|
820
|
+
# A writer failure or a retry can replace/remove settings while
|
|
821
|
+
# check_response runs. Only one path may claim this attempt.
|
|
822
|
+
return unless claim_query(settings, true)
|
|
351
823
|
exception = msg.get_exception
|
|
352
824
|
if (ret.kind_of?TsigError)
|
|
353
825
|
exception = ret
|
|
354
826
|
end
|
|
355
827
|
Dnsruby.log.debug{"Pushing response to client queue"}
|
|
356
|
-
push_to_client(id, client_queue, msg, exception, query, res
|
|
828
|
+
push_to_client(id, client_queue, msg, exception, query, res,
|
|
829
|
+
settings.send_token&.transmission_budget)
|
|
357
830
|
# client_queue.push([id, msg, exception])
|
|
358
831
|
# notify_queue_observers(client_queue, id)
|
|
359
832
|
else
|
|
@@ -364,7 +837,6 @@ module Dnsruby
|
|
|
364
837
|
end
|
|
365
838
|
# If not, then we have an error
|
|
366
839
|
Dnsruby.log.error{"Stray packet - " + msg.inspect + "\n from " + socket.inspect}
|
|
367
|
-
print("Stray packet - " + msg.question()[0].qname.to_s + " from " + msg.answerip.to_s + ", #{client_ids.length} client_ids\n")
|
|
368
840
|
end
|
|
369
841
|
|
|
370
842
|
def persistent?(socket)
|
|
@@ -372,28 +844,69 @@ module Dnsruby
|
|
|
372
844
|
end
|
|
373
845
|
|
|
374
846
|
def remove_id(id)
|
|
375
|
-
|
|
847
|
+
state_mutex.synchronize do
|
|
376
848
|
remove_id_from_mutex_synchronized_block(id)
|
|
377
849
|
end
|
|
378
850
|
end
|
|
379
851
|
|
|
380
852
|
# THIS MUST BE CALLED FROM INSIDE A @@mutex SYNCHRONISED BLOCK!
|
|
381
|
-
def remove_id_from_mutex_synchronized_block(id)
|
|
382
|
-
|
|
853
|
+
def remove_id_from_mutex_synchronized_block(id, preserved_token = nil, response_received = false)
|
|
854
|
+
settings = @@query_hash.delete(id)
|
|
855
|
+
return unless settings
|
|
856
|
+
ids = @@queries_by_queue[settings.client_queue]
|
|
857
|
+
if ids
|
|
858
|
+
ids.delete(id)
|
|
859
|
+
@@queries_by_queue.delete(settings.client_queue) if ids.empty?
|
|
860
|
+
end
|
|
861
|
+
if (sender_ids = (@@queries_by_sender ||= {}.compare_by_identity)[settings.single_resolver])
|
|
862
|
+
sender_ids.delete(id)
|
|
863
|
+
@@queries_by_sender.delete(settings.single_resolver) if sender_ids.empty?
|
|
864
|
+
end
|
|
865
|
+
settings.send_token.cancel unless settings.send_token.equal?(preserved_token)
|
|
383
866
|
@@timeouts.delete(id)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
867
|
+
@packet_deadlines&.delete(id)
|
|
868
|
+
socket = settings.socket
|
|
869
|
+
return unless socket # A queued send may not have acquired a socket yet.
|
|
870
|
+
(@@socket_hash[socket] || []).delete(id)
|
|
871
|
+
|
|
872
|
+
if persistent?(socket)
|
|
873
|
+
decrement_remaining_queries(socket)
|
|
874
|
+
# An unanswered query can still produce a late reply, or leave an
|
|
875
|
+
# incomplete frame in the reader. Drain existing peers, but never admit
|
|
876
|
+
# another query on this stream after its ownership has been cancelled.
|
|
877
|
+
(@@retiring_sockets ||= Set.new) << socket unless response_received
|
|
878
|
+
end
|
|
388
879
|
|
|
389
|
-
|
|
390
|
-
@@
|
|
391
|
-
|
|
880
|
+
retiring = (@@retiring_sockets ||= Set.new).include?(socket) &&
|
|
881
|
+
(@@socket_hash[socket] || []).empty?
|
|
882
|
+
if !persistent?(socket) || retiring ||
|
|
883
|
+
(max_attained?(socket) && (@@socket_hash[socket] || []).empty?)
|
|
392
884
|
Dnsruby.log.debug("Closing socket #{socket}")
|
|
393
885
|
socket.close rescue nil
|
|
886
|
+
retire_socket_from_mutex_synchronized_block(socket)
|
|
887
|
+
elsif (@@socket_hash[socket] || []).empty?
|
|
888
|
+
# Idle time starts after the last outstanding response, never while a
|
|
889
|
+
# query or pipeline writer still owns the socket.
|
|
890
|
+
(@@socket_idle_deadlines ||= {})[socket] =
|
|
891
|
+
TransportClock.now + settings.tcp_idle_timeout
|
|
892
|
+
(@idle_deadlines ||= DeadlineQueue.new)[socket] = @@socket_idle_deadlines[socket]
|
|
394
893
|
end
|
|
395
894
|
end
|
|
396
895
|
|
|
896
|
+
def expire_idle_sockets
|
|
897
|
+
state_mutex.synchronize do
|
|
898
|
+
now = TransportClock.now
|
|
899
|
+
while (entry = @idle_deadlines&.first) && entry[1] <= now
|
|
900
|
+
socket = entry[0]
|
|
901
|
+
@idle_deadlines.delete(socket)
|
|
902
|
+
next unless (@@socket_hash[socket] || []).empty?
|
|
903
|
+
socket.close rescue nil
|
|
904
|
+
retire_socket_from_mutex_synchronized_block(socket)
|
|
905
|
+
end
|
|
906
|
+
end
|
|
907
|
+
end
|
|
908
|
+
private :expire_idle_sockets
|
|
909
|
+
|
|
397
910
|
def decrement_remaining_queries(socket)
|
|
398
911
|
if @@socket_remaining_queries[socket]
|
|
399
912
|
@@socket_remaining_queries[socket] -= 1
|
|
@@ -408,12 +921,22 @@ module Dnsruby
|
|
|
408
921
|
end
|
|
409
922
|
|
|
410
923
|
def process_timeouts
|
|
411
|
-
#
|
|
412
|
-
|
|
413
|
-
time_now =
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
924
|
+
# Pop only due owners under the same lock used for registrations. Keep
|
|
925
|
+
# settings identities so a replacement attempt cannot be expired later.
|
|
926
|
+
time_now = TransportClock.now
|
|
927
|
+
settings_due = state_mutex.synchronize do
|
|
928
|
+
due = []
|
|
929
|
+
while (first = @packet_deadlines&.first) && first[1] <= time_now
|
|
930
|
+
id, = first
|
|
931
|
+
@packet_deadlines.delete(id)
|
|
932
|
+
due << @@query_hash[id] if @@query_hash[id]
|
|
933
|
+
end
|
|
934
|
+
due
|
|
935
|
+
end
|
|
936
|
+
settings_due.each do |settings|
|
|
937
|
+
if claim_query(settings)
|
|
938
|
+
deliver_queue_event(settings.client_queue, [settings.client_query_id, Resolver::EventType::ERROR,
|
|
939
|
+
nil, settings.authentication_error || ResolvTimeout.new("Query timed out")])
|
|
417
940
|
end
|
|
418
941
|
end
|
|
419
942
|
end
|
|
@@ -424,58 +947,67 @@ module Dnsruby
|
|
|
424
947
|
# then process message.
|
|
425
948
|
buf = +""
|
|
426
949
|
expected_length = 0
|
|
427
|
-
|
|
428
|
-
|
|
950
|
+
reading_length = nil
|
|
951
|
+
state_mutex.synchronize {
|
|
952
|
+
return false if socket.closed?
|
|
953
|
+
buf, expected_length, reading_length = @@tcp_buffers[socket]
|
|
429
954
|
if (!buf)
|
|
430
955
|
buf = +""
|
|
431
956
|
expected_length = 2
|
|
432
|
-
|
|
957
|
+
reading_length = true
|
|
958
|
+
@@tcp_buffers[socket]=[buf, expected_length, reading_length]
|
|
433
959
|
end
|
|
434
960
|
}
|
|
435
961
|
if (buf.length() < expected_length)
|
|
436
962
|
begin
|
|
437
963
|
input, = socket.recv_nonblock(expected_length-buf.length)
|
|
438
|
-
|
|
964
|
+
# Depending on the Ruby version, EOF is nil or an empty string.
|
|
965
|
+
if input.nil? || input.empty?
|
|
439
966
|
Dnsruby.log.debug("EOF from server - no bytes read - closing socket")
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
@@sockets.delete(socket) #remove ourselves from select, app will have to retry
|
|
444
|
-
#maybe fire an event
|
|
445
|
-
@@socket_hash[socket].map do | client_id |
|
|
446
|
-
[SocketEofResolvError.new("TCP socket closed before all answers received"), socket, client_id]
|
|
447
|
-
end
|
|
967
|
+
exceptions = state_mutex.synchronize do
|
|
968
|
+
socket.close
|
|
969
|
+
retire_socket_from_mutex_synchronized_block(socket)
|
|
448
970
|
end
|
|
449
|
-
|
|
450
971
|
exceptions.each { |exception| send_exception_to_client(*exception) }
|
|
451
972
|
|
|
452
973
|
return false
|
|
453
974
|
end
|
|
454
975
|
buf << input if input
|
|
455
|
-
rescue
|
|
456
|
-
|
|
976
|
+
rescue IO::WaitReadable, Errno::EINTR
|
|
977
|
+
return false
|
|
978
|
+
rescue IOError, SystemCallError => error
|
|
979
|
+
# A reset or fatal read failure cannot become readable again. Retire
|
|
980
|
+
# every owner once, without invoking resolver callbacks under our lock.
|
|
981
|
+
exceptions = state_mutex.synchronize do
|
|
982
|
+
socket.close rescue nil
|
|
983
|
+
retire_socket_from_mutex_synchronized_block(socket)
|
|
984
|
+
end
|
|
985
|
+
exceptions.each { |exception| send_exception_to_client(*exception) }
|
|
986
|
+
Dnsruby.log.debug { "TCP read failed: #{error.class}: #{error.message}" }
|
|
457
987
|
return false
|
|
458
988
|
end
|
|
459
989
|
end
|
|
460
990
|
# If data is complete, then return it.
|
|
461
991
|
if (buf.length == expected_length)
|
|
462
|
-
if
|
|
992
|
+
if reading_length
|
|
463
993
|
# We just read the data_length field. Now we need to start reading that many bytes.
|
|
464
|
-
|
|
994
|
+
state_mutex.synchronize {
|
|
995
|
+
return false if socket.closed?
|
|
465
996
|
answersize = buf.unpack('n')[0]
|
|
466
|
-
@@tcp_buffers[socket] = [+"", answersize]
|
|
997
|
+
@@tcp_buffers[socket] = [+"", answersize, false]
|
|
467
998
|
}
|
|
468
999
|
return tcp_read(socket)
|
|
469
1000
|
else
|
|
470
1001
|
# We just read the data - now return it
|
|
471
|
-
|
|
1002
|
+
state_mutex.synchronize {
|
|
472
1003
|
@@tcp_buffers.delete(socket)
|
|
473
1004
|
}
|
|
474
1005
|
return buf
|
|
475
1006
|
end
|
|
476
1007
|
else
|
|
477
|
-
|
|
478
|
-
|
|
1008
|
+
state_mutex.synchronize {
|
|
1009
|
+
return false if socket.closed?
|
|
1010
|
+
@@tcp_buffers[socket]=[buf, expected_length, reading_length]
|
|
479
1011
|
}
|
|
480
1012
|
return false
|
|
481
1013
|
end
|
|
@@ -484,7 +1016,14 @@ module Dnsruby
|
|
|
484
1016
|
def get_incoming_data(socket, packet_size)
|
|
485
1017
|
answerfrom,answerip,answerport,answersize=nil
|
|
486
1018
|
ans,buf = nil
|
|
487
|
-
|
|
1019
|
+
begin
|
|
1020
|
+
is_tcp = tcp?(socket)
|
|
1021
|
+
rescue IOError, SystemCallError
|
|
1022
|
+
# A concurrent pipeline writer can retire this socket after IO.select
|
|
1023
|
+
# returned it. It has already queued errors for its outstanding queries.
|
|
1024
|
+
return if socket.closed?
|
|
1025
|
+
raise
|
|
1026
|
+
end
|
|
488
1027
|
|
|
489
1028
|
begin
|
|
490
1029
|
if is_tcp
|
|
@@ -498,12 +1037,11 @@ module Dnsruby
|
|
|
498
1037
|
else
|
|
499
1038
|
# @TODO@ Can we get recvfrom to stop issuing PTR queries when we already
|
|
500
1039
|
# know both the FQDN and the IP address?
|
|
501
|
-
if (ret = socket.
|
|
1040
|
+
if (ret = socket.recvfrom_nonblock(packet_size))
|
|
502
1041
|
buf = ret[0]
|
|
503
1042
|
answerport=ret[1][1]
|
|
504
1043
|
answerfrom=ret[1][2]
|
|
505
1044
|
answerip=ret[1][3]
|
|
506
|
-
answersize=(buf.length)
|
|
507
1045
|
else
|
|
508
1046
|
# recvfrom failed - why?
|
|
509
1047
|
Dnsruby.log.error{"Error - recvfrom failed from #{socket}"}
|
|
@@ -511,72 +1049,111 @@ module Dnsruby
|
|
|
511
1049
|
return
|
|
512
1050
|
end
|
|
513
1051
|
end
|
|
1052
|
+
rescue IO::WaitReadable, Errno::EINTR
|
|
1053
|
+
# Readiness is only a hint; keep the query registered for its next event.
|
|
1054
|
+
return
|
|
514
1055
|
rescue Exception => e
|
|
515
1056
|
Dnsruby.log.error{"Error - recvfrom failed from #{socket}, exception : #{e}"}
|
|
516
1057
|
handle_recvfrom_failure(socket, e)
|
|
517
1058
|
return
|
|
518
1059
|
end
|
|
1060
|
+
answersize = buf.bytesize
|
|
519
1061
|
Dnsruby.log.debug{";; answer from #{answerfrom} : #{answersize} bytes\n"}
|
|
520
1062
|
|
|
521
1063
|
begin
|
|
522
1064
|
ans = Message.decode(buf)
|
|
523
1065
|
|
|
524
|
-
if is_tcp
|
|
525
|
-
@@mutex.synchronize do
|
|
526
|
-
ids = get_active_ids(@@query_hash, ans.header.id)
|
|
527
|
-
if ids.empty?
|
|
528
|
-
Dnsruby.log.error("Decode error from #{answerip} but can't determine packet id")
|
|
529
|
-
#todo add error event? The problem is we don't have a valid id so we don't
|
|
530
|
-
#know which client queue to send the exception to
|
|
531
|
-
end
|
|
532
|
-
answerfrom = @@query_hash[ids[0]].dest_server
|
|
533
|
-
answerip = answerfrom
|
|
534
|
-
answerport = @@query_hash[ids[0]].dest_port
|
|
535
|
-
end
|
|
536
|
-
end
|
|
537
|
-
|
|
538
1066
|
rescue Exception => e
|
|
539
1067
|
Dnsruby.log.error("Decode error! #{e.class}, #{e}\nfor msg (length=#{buf.length}) : #{buf}")
|
|
540
|
-
|
|
1068
|
+
# A partial Message has a newly allocated, random header if decoding
|
|
1069
|
+
# failed before the complete header. Only the bytes identify its ID.
|
|
1070
|
+
header_id = buf.unpack('n').first if buf.bytesize >= 2
|
|
1071
|
+
client_id=get_client_id_from_answerfrom(socket, answerip, answerport, header_id)
|
|
541
1072
|
if (client_id == nil)
|
|
542
1073
|
Dnsruby.log.error{"Decode error from #{answerip} but can't determine packet id"}
|
|
1074
|
+
return
|
|
543
1075
|
end
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1076
|
+
unless is_tcp
|
|
1077
|
+
matches_question = state_mutex.synchronize do
|
|
1078
|
+
current = @@query_hash[client_id]
|
|
1079
|
+
partial = e.partial_message if e.is_a?(DecodeError)
|
|
1080
|
+
current && current.socket == socket && (current.query.send_raw ||
|
|
1081
|
+
(buf.bytesize >= 12 && partial &&
|
|
1082
|
+
(current.query.kind_of?(Update) ||
|
|
1083
|
+
(!partial.question.empty? && partial.header.qdcount == partial.question.length &&
|
|
1084
|
+
matching_questions?(partial, current.query)))))
|
|
1085
|
+
end
|
|
1086
|
+
# A truncated answer can justify TCP fallback only after its question
|
|
1087
|
+
# identifies this query (RFC 5452 section 9.1). Incomplete headers or
|
|
1088
|
+
# questions and unrelated questions cannot end the UDP transaction.
|
|
1089
|
+
return unless matches_question
|
|
1090
|
+
end
|
|
1091
|
+
# Decoding may fail after a complete header. Such a packet must match
|
|
1092
|
+
# the response direction and opcode before it can retry or fail its
|
|
1093
|
+
# owner, just as a successfully decoded response must. Use wire bytes:
|
|
1094
|
+
# an incomplete decoded Message can still contain a default header.
|
|
1095
|
+
if buf.bytesize >= 12
|
|
1096
|
+
flags = buf.unpack('n2').last
|
|
1097
|
+
matches_header = state_mutex.synchronize do
|
|
1098
|
+
current = @@query_hash[client_id]
|
|
1099
|
+
current && current.socket == socket && (current.query.send_raw ||
|
|
1100
|
+
((flags & 0x8000) != 0 && ((flags >> 11) & 15) == current.query.header.opcode.code))
|
|
1101
|
+
end
|
|
1102
|
+
return unless matches_header
|
|
1103
|
+
end
|
|
1104
|
+
# No undecodable packet can authenticate its TSIG. Discard it before
|
|
1105
|
+
# either failing the transaction or acting on an untrusted TC bit, just
|
|
1106
|
+
# as check_response discards unsigned or incorrectly signed replies.
|
|
1107
|
+
unauthenticated = state_mutex.synchronize do
|
|
1108
|
+
current = @@query_hash[client_id]
|
|
1109
|
+
if current && current.socket == socket && current.query.tsig && !current.query.send_raw
|
|
1110
|
+
current.authentication_error ||= TsigError.new('Cannot authenticate malformed response')
|
|
1111
|
+
true
|
|
1112
|
+
end
|
|
1113
|
+
end
|
|
1114
|
+
return if unauthenticated
|
|
1115
|
+
# Truncation permits a UDP-to-TCP fallback, never another TCP attempt.
|
|
1116
|
+
# Repeating malformed TCP replies must not renew the packet deadline.
|
|
1117
|
+
settings = nil
|
|
1118
|
+
if !is_tcp && (DecodeError === e) && e.partial_message && e.partial_message.header.tc
|
|
1119
|
+
settings = state_mutex.synchronize do
|
|
1120
|
+
current = @@query_hash[client_id]
|
|
1121
|
+
if current && current.socket == socket && !current.ignore_truncation && !current.no_tcp
|
|
1122
|
+
current
|
|
568
1123
|
end
|
|
569
|
-
}
|
|
570
|
-
if !sent
|
|
571
|
-
send_exception_to_client(e, socket, client_id)
|
|
572
1124
|
end
|
|
573
|
-
|
|
1125
|
+
end
|
|
1126
|
+
if settings
|
|
1127
|
+
Dnsruby.log.error{"Decode error (from #{answerip})! Header shows truncation, so trying again over TCP"}
|
|
1128
|
+
previous_context = Thread.current[:dnsruby_response_query]
|
|
1129
|
+
begin
|
|
1130
|
+
Thread.current[:dnsruby_response_query] = settings
|
|
1131
|
+
settings.single_resolver.send_async(settings.query, settings.client_queue, client_id, true)
|
|
1132
|
+
ensure
|
|
1133
|
+
Thread.current[:dnsruby_response_query] = previous_context
|
|
1134
|
+
end
|
|
574
1135
|
else
|
|
575
1136
|
send_exception_to_client(e, socket, client_id)
|
|
576
1137
|
end
|
|
577
1138
|
return
|
|
578
1139
|
end
|
|
579
1140
|
|
|
1141
|
+
if is_tcp
|
|
1142
|
+
# This supplies peer metadata only. Normal response checks below still
|
|
1143
|
+
# validate the question and TSIG, including sequential duplicate IDs.
|
|
1144
|
+
settings = state_mutex.synchronize do
|
|
1145
|
+
id = socket_ids_for_header(socket, ans.header.id).first
|
|
1146
|
+
@@query_hash[id]
|
|
1147
|
+
end
|
|
1148
|
+
unless settings
|
|
1149
|
+
Dnsruby.log.debug { "Ignoring TCP response with no matching query on this socket" }
|
|
1150
|
+
return
|
|
1151
|
+
end
|
|
1152
|
+
answerfrom = settings.dest_server
|
|
1153
|
+
answerip = answerfrom
|
|
1154
|
+
answerport = settings.dest_port
|
|
1155
|
+
end
|
|
1156
|
+
|
|
580
1157
|
if (ans!= nil)
|
|
581
1158
|
Dnsruby.log.debug{"#{ans}"}
|
|
582
1159
|
ans.answerfrom=(answerfrom)
|
|
@@ -590,14 +1167,15 @@ module Dnsruby
|
|
|
590
1167
|
# No way to notify the client about this error, unless there was only one connection on the socket
|
|
591
1168
|
# Not a problem, as there only will ever be one connection on the socket (Kaminsky attack mitigation)
|
|
592
1169
|
ids_for_socket = []
|
|
593
|
-
|
|
594
|
-
ids_for_socket = @@socket_hash[socket]
|
|
1170
|
+
state_mutex.synchronize{
|
|
1171
|
+
ids_for_socket = (@@socket_hash[socket] || []).to_a
|
|
595
1172
|
}
|
|
1173
|
+
return if ids_for_socket.empty?
|
|
596
1174
|
if (ids_for_socket.length == 1)
|
|
597
1175
|
answerfrom=nil
|
|
598
|
-
|
|
1176
|
+
state_mutex.synchronize{
|
|
599
1177
|
query_settings = @@query_hash[ids_for_socket[0]]
|
|
600
|
-
answerfrom=query_settings.dest_server
|
|
1178
|
+
answerfrom=query_settings.dest_server if query_settings
|
|
601
1179
|
}
|
|
602
1180
|
send_exception_to_client(OtherResolvError.new("recvfrom failed from #{answerfrom}; #{exception}"), socket, ids_for_socket[0])
|
|
603
1181
|
else
|
|
@@ -605,16 +1183,33 @@ module Dnsruby
|
|
|
605
1183
|
end
|
|
606
1184
|
end
|
|
607
1185
|
|
|
608
|
-
def get_client_id_from_answerfrom(socket, answerip, answerport)
|
|
609
|
-
#
|
|
1186
|
+
def get_client_id_from_answerfrom(socket, answerip, answerport, header_id=nil)
|
|
1187
|
+
# A pipelined socket can have several outstanding queries.
|
|
610
1188
|
client_id=nil
|
|
611
1189
|
# Figure out client id from answerfrom
|
|
612
|
-
|
|
613
|
-
ids =
|
|
1190
|
+
state_mutex.synchronize{
|
|
1191
|
+
ids = (header_id ? socket_ids_for_header(socket, header_id) :
|
|
1192
|
+
(@@socket_hash[socket] || [])).select { |id| @@query_hash[id] }
|
|
1193
|
+
# TCP decoding can fail before the peer address has been assigned. The
|
|
1194
|
+
# socket and partial header identify its query, including pipeline use.
|
|
1195
|
+
if answerip.nil?
|
|
1196
|
+
if header_id
|
|
1197
|
+
matching = ids.select { |id| @@query_hash[id].query.header.id == header_id }
|
|
1198
|
+
return matching.first if matching.length == 1
|
|
1199
|
+
return nil
|
|
1200
|
+
end
|
|
1201
|
+
return ids.first if ids.length == 1
|
|
1202
|
+
return nil
|
|
1203
|
+
end
|
|
614
1204
|
ids.each do |id|
|
|
615
1205
|
# Does this id speak to this dest_server?
|
|
616
1206
|
query_settings = @@query_hash[id]
|
|
617
|
-
if
|
|
1207
|
+
next if header_id && query_settings.query.header.id != header_id
|
|
1208
|
+
# recvfrom may spell a numeric zone as an interface name. The socket
|
|
1209
|
+
# is already connected to the scoped peer, so compare address bytes.
|
|
1210
|
+
source = IPAddr.new(answerip.split('%', 2).first)
|
|
1211
|
+
destination = IPAddr.new(query_settings.dest_server.to_s.split('%', 2).first)
|
|
1212
|
+
if (source == destination && answerport == query_settings.dest_port)
|
|
618
1213
|
# We have a match
|
|
619
1214
|
client_id = id
|
|
620
1215
|
break
|
|
@@ -627,47 +1222,36 @@ module Dnsruby
|
|
|
627
1222
|
def send_exception_to_client(err, socket, client_id, msg=nil)
|
|
628
1223
|
# find the client response queue
|
|
629
1224
|
client_queue = nil
|
|
630
|
-
|
|
631
|
-
|
|
1225
|
+
state_mutex.synchronize {
|
|
1226
|
+
settings = @@query_hash[client_id]
|
|
1227
|
+
return unless settings && (!socket || settings.socket == socket)
|
|
1228
|
+
client_queue = settings.client_queue
|
|
1229
|
+
remove_id_from_mutex_synchronized_block(client_id)
|
|
632
1230
|
}
|
|
633
|
-
remove_id(client_id)
|
|
634
1231
|
# push_to_client(client_id, client_queue, msg, err)
|
|
635
|
-
client_queue
|
|
636
|
-
notify_queue_observers(client_queue, client_id)
|
|
1232
|
+
deliver_queue_event(client_queue, [client_id, Resolver::EventType::ERROR, msg, err])
|
|
637
1233
|
end
|
|
638
1234
|
|
|
639
1235
|
def push_exception_to_select(client_id, client_queue, err, msg)
|
|
640
|
-
|
|
1236
|
+
state_mutex.synchronize{
|
|
641
1237
|
@@queued_exceptions.push([client_id, client_queue, err, msg])
|
|
1238
|
+
check_select_thread_synchronized
|
|
642
1239
|
}
|
|
643
|
-
# Make sure select loop is running!
|
|
644
|
-
if (@@select_thread && @@select_thread.alive?)
|
|
645
|
-
else
|
|
646
|
-
@@select_thread = Thread.new {
|
|
647
|
-
do_select
|
|
648
|
-
}
|
|
649
|
-
end
|
|
650
1240
|
wake_up_select_thread
|
|
651
1241
|
end
|
|
652
1242
|
|
|
653
|
-
def push_response_to_select(client_id, client_queue, msg, query, res)
|
|
1243
|
+
def push_response_to_select(client_id, client_queue, msg, query, res, transmission_budget = nil)
|
|
654
1244
|
# This needs to queue the response TO THE SELECT THREAD, which then needs
|
|
655
1245
|
# to send it out from its normal loop.
|
|
656
1246
|
Dnsruby.log.debug{"Pushing response to client queue direct from resolver or validator"}
|
|
657
|
-
|
|
1247
|
+
state_mutex.synchronize{
|
|
658
1248
|
err = nil
|
|
659
1249
|
if (msg.rcode == RCode.NXDOMAIN)
|
|
660
1250
|
err = NXDomain.new
|
|
661
1251
|
end
|
|
662
|
-
@@queued_responses.push([client_id, client_queue, msg, err, query, res])
|
|
1252
|
+
@@queued_responses.push([client_id, client_queue, msg, err, query, res, transmission_budget])
|
|
1253
|
+
check_select_thread_synchronized
|
|
663
1254
|
}
|
|
664
|
-
# Make sure select loop is running!
|
|
665
|
-
if (@@select_thread && @@select_thread.alive?)
|
|
666
|
-
else
|
|
667
|
-
@@select_thread = Thread.new {
|
|
668
|
-
do_select
|
|
669
|
-
}
|
|
670
|
-
end
|
|
671
1255
|
wake_up_select_thread
|
|
672
1256
|
end
|
|
673
1257
|
|
|
@@ -675,22 +1259,16 @@ module Dnsruby
|
|
|
675
1259
|
# This needs to queue the response TO THE SELECT THREAD, which then needs
|
|
676
1260
|
# to send it out from its normal loop.
|
|
677
1261
|
Dnsruby.log.debug{"Pushing response to client queue direct from resolver or validator"}
|
|
678
|
-
|
|
1262
|
+
state_mutex.synchronize{
|
|
679
1263
|
@@queued_validation_responses.push([client_id, client_queue, msg, err, query, res])
|
|
1264
|
+
check_select_thread_synchronized
|
|
680
1265
|
}
|
|
681
|
-
# Make sure select loop is running!
|
|
682
|
-
if (@@select_thread && @@select_thread.alive?)
|
|
683
|
-
else
|
|
684
|
-
@@select_thread = Thread.new {
|
|
685
|
-
do_select
|
|
686
|
-
}
|
|
687
|
-
end
|
|
688
1266
|
wake_up_select_thread
|
|
689
1267
|
end
|
|
690
1268
|
|
|
691
1269
|
def send_queued_exceptions
|
|
692
1270
|
exceptions = []
|
|
693
|
-
|
|
1271
|
+
state_mutex.synchronize{
|
|
694
1272
|
exceptions = @@queued_exceptions
|
|
695
1273
|
@@queued_exceptions = []
|
|
696
1274
|
}
|
|
@@ -698,23 +1276,21 @@ module Dnsruby
|
|
|
698
1276
|
exceptions.each do |item|
|
|
699
1277
|
client_id, client_queue, err, msg = item
|
|
700
1278
|
# push_to_client(client_id, client_queue, msg, err)
|
|
701
|
-
client_queue
|
|
702
|
-
notify_queue_observers(client_queue, client_id)
|
|
1279
|
+
deliver_queue_event(client_queue, [client_id, Resolver::EventType::ERROR, msg, err])
|
|
703
1280
|
end
|
|
704
1281
|
end
|
|
705
1282
|
|
|
706
1283
|
def send_queued_responses
|
|
707
1284
|
responses = []
|
|
708
|
-
|
|
1285
|
+
state_mutex.synchronize{
|
|
709
1286
|
responses = @@queued_responses
|
|
710
1287
|
@@queued_responses = []
|
|
711
1288
|
}
|
|
712
1289
|
|
|
713
1290
|
responses.each do |item|
|
|
714
|
-
client_id, client_queue, msg, err, query, res = item
|
|
1291
|
+
client_id, client_queue, msg, err, query, res, transmission_budget = item
|
|
715
1292
|
# push_to_client(client_id, client_queue, msg, err)
|
|
716
|
-
client_queue
|
|
717
|
-
notify_queue_observers(client_queue, client_id)
|
|
1293
|
+
next unless deliver_queue_event(client_queue, [client_id, Resolver::EventType::RECEIVED, msg, err])
|
|
718
1294
|
# Do we need to validate this? The response has come from the cache -
|
|
719
1295
|
# validate it only if it has not been validated already
|
|
720
1296
|
# So, if we need to validate it, send it to the validation thread
|
|
@@ -722,19 +1298,18 @@ module Dnsruby
|
|
|
722
1298
|
if (((msg.security_level == Message::SecurityLevel.UNCHECKED) ||
|
|
723
1299
|
(msg.security_level == Message::SecurityLevel.INDETERMINATE)) &&
|
|
724
1300
|
(ValidatorThread.requires_validation?(query, msg, err, res)))
|
|
725
|
-
validator = ValidatorThread.new(client_id, client_queue, msg, err, query
|
|
1301
|
+
validator = ValidatorThread.new(client_id, client_queue, msg, err, query, self, res, transmission_budget)
|
|
726
1302
|
validator.run
|
|
727
1303
|
else
|
|
728
1304
|
PacketSender.cache(query, msg) # The validator won't cache it, so we'd better do it now
|
|
729
|
-
client_queue
|
|
730
|
-
notify_queue_observers(client_queue, client_id)
|
|
1305
|
+
deliver_queue_event(client_queue, [client_id, Resolver::EventType::VALIDATED, msg, err])
|
|
731
1306
|
end
|
|
732
1307
|
end
|
|
733
1308
|
end
|
|
734
1309
|
|
|
735
1310
|
def send_queued_validation_responses
|
|
736
1311
|
responses = []
|
|
737
|
-
|
|
1312
|
+
state_mutex.synchronize{
|
|
738
1313
|
responses = @@queued_validation_responses
|
|
739
1314
|
@@queued_validation_responses = []
|
|
740
1315
|
}
|
|
@@ -742,77 +1317,197 @@ module Dnsruby
|
|
|
742
1317
|
responses.each do |item|
|
|
743
1318
|
client_id, client_queue, msg, err, _query, _res = item
|
|
744
1319
|
# push_to_client(client_id, client_queue, msg, err)
|
|
745
|
-
client_queue
|
|
746
|
-
notify_queue_observers(client_queue, client_id)
|
|
1320
|
+
deliver_queue_event(client_queue, [client_id, Resolver::EventType::VALIDATED, msg, err])
|
|
747
1321
|
end
|
|
748
1322
|
end
|
|
749
1323
|
|
|
750
|
-
def push_to_client(client_id, client_queue, msg, err, query, res)
|
|
751
|
-
#
|
|
752
|
-
#
|
|
753
|
-
|
|
754
|
-
# type of response.
|
|
755
|
-
client_queue.push([client_id, Resolver::EventType::RECEIVED, msg, err])
|
|
756
|
-
notify_queue_observers(client_queue, client_id)
|
|
1324
|
+
def push_to_client(client_id, client_queue, msg, err, query, res, transmission_budget = nil)
|
|
1325
|
+
# Notify the resolver before validation so it stops competing attempts
|
|
1326
|
+
# while retaining the logical query's total deadline.
|
|
1327
|
+
return unless deliver_queue_event(client_queue, [client_id, Resolver::EventType::RECEIVED, msg, err])
|
|
757
1328
|
|
|
758
1329
|
if (!err || (err.instance_of?(NXDomain)))
|
|
759
|
-
#
|
|
760
|
-
|
|
761
|
-
# which will then take responsibility for delivering it to the client.
|
|
762
|
-
# The validator will need access to the queue observers -
|
|
763
|
-
validator = ValidatorThread.new(client_id, client_queue, msg, err, query ,self, res)
|
|
1330
|
+
# The validator owns delivery of the validation result.
|
|
1331
|
+
validator = ValidatorThread.new(client_id, client_queue, msg, err, query, self, res, transmission_budget)
|
|
764
1332
|
validator.run
|
|
765
|
-
# @@validator.add_to_queue([client_id, client_queue, msg, err, query, self, res])
|
|
766
1333
|
end
|
|
767
1334
|
end
|
|
768
1335
|
|
|
769
1336
|
def add_observer(client_queue, observer)
|
|
770
|
-
|
|
771
|
-
@@
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1337
|
+
state_mutex.synchronize {
|
|
1338
|
+
(@@observed_queues ||= ObjectSpace::WeakMap.new)[client_queue] = true
|
|
1339
|
+
previous = @@observers[client_queue]
|
|
1340
|
+
unless previous.equal?(observer)
|
|
1341
|
+
remove_observer_queue_count(previous) if previous
|
|
1342
|
+
@@observers[client_queue] = observer
|
|
1343
|
+
count = (@@observer_queue_counts[observer] || 0) + 1
|
|
1344
|
+
@@observer_queue_counts[observer] = count
|
|
1345
|
+
@@tick_observers.push(observer) if count == 1
|
|
775
1346
|
end
|
|
1347
|
+
check_select_thread_synchronized
|
|
776
1348
|
}
|
|
1349
|
+
# An empty resolver still has a logical deadline but no socket to wake us.
|
|
1350
|
+
wake_up_select_thread
|
|
777
1351
|
end
|
|
778
1352
|
|
|
1353
|
+
# Take the observer snapshot before acquiring its Resolver lock: Resolver
|
|
1354
|
+
# completion acquires these locks in the opposite direction.
|
|
1355
|
+
def validation_deadline(client_queue, client_query_id)
|
|
1356
|
+
observer = state_mutex.synchronize { @@observers[client_queue] }
|
|
1357
|
+
observer.validation_deadline(client_queue, client_query_id) if observer.respond_to?(:validation_deadline)
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
def validation_transmission_budget(client_queue, client_query_id)
|
|
1361
|
+
observer = state_mutex.synchronize { @@observers[client_queue] }
|
|
1362
|
+
if observer.respond_to?(:validation_transmission_budget)
|
|
1363
|
+
observer.validation_transmission_budget(client_queue, client_query_id)
|
|
1364
|
+
end
|
|
1365
|
+
end
|
|
1366
|
+
|
|
1367
|
+
def register_validation(client_queue, sender, control)
|
|
1368
|
+
state_mutex.synchronize do
|
|
1369
|
+
refresh_validation_process
|
|
1370
|
+
# A response may have been accepted just before close or its total
|
|
1371
|
+
# deadline. Remember prior observers weakly so rejected registrations
|
|
1372
|
+
# cannot keep completed client queues alive.
|
|
1373
|
+
if client_queue.closed? ||
|
|
1374
|
+
((@@observed_queues ||= ObjectSpace::WeakMap.new)[client_queue] && !@@observers[client_queue])
|
|
1375
|
+
control.cancel
|
|
1376
|
+
return false
|
|
1377
|
+
end
|
|
1378
|
+
# The validation pool is small and bounded. Keep one registration list
|
|
1379
|
+
# rather than maintaining separate indexes for queues and senders.
|
|
1380
|
+
unless @@validations.any? { |queue, owner, token|
|
|
1381
|
+
queue.equal?(client_queue) && owner.equal?(sender) && token.equal?(control) }
|
|
1382
|
+
@@validations << [client_queue, sender, control]
|
|
1383
|
+
end
|
|
1384
|
+
true
|
|
1385
|
+
end
|
|
1386
|
+
end
|
|
1387
|
+
|
|
1388
|
+
def unregister_validation(client_queue, sender, control)
|
|
1389
|
+
state_mutex.synchronize do
|
|
1390
|
+
refresh_validation_process
|
|
1391
|
+
@@validations.delete_if do |queue, owner, token|
|
|
1392
|
+
queue.equal?(client_queue) && owner.equal?(sender) && token.equal?(control)
|
|
1393
|
+
end
|
|
1394
|
+
end
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
# Caller holds @@mutex. Parent workers do not survive fork, so they cannot
|
|
1398
|
+
# unregister their inherited entries in the child. Refresh before adding a
|
|
1399
|
+
# child registration, independently of the validation worker pool restart.
|
|
1400
|
+
def refresh_validation_process
|
|
1401
|
+
pid = Process.pid
|
|
1402
|
+
return if defined?(@@validation_pid) && @@validation_pid == pid
|
|
1403
|
+
@@validation_pid = pid
|
|
1404
|
+
@@validations = []
|
|
1405
|
+
end
|
|
1406
|
+
private :refresh_validation_process
|
|
1407
|
+
|
|
1408
|
+
def cancel_validations_for_queue_synchronized(client_queue)
|
|
1409
|
+
refresh_validation_process
|
|
1410
|
+
@@validations.each do |queue, _sender, control|
|
|
1411
|
+
control.cancel if queue.equal?(client_queue)
|
|
1412
|
+
end
|
|
1413
|
+
end
|
|
1414
|
+
private :cancel_validations_for_queue_synchronized
|
|
1415
|
+
|
|
1416
|
+
# Stop packet attempts without removing the logical query's observer. A
|
|
1417
|
+
# received response can still need validation and its overall deadline.
|
|
1418
|
+
def cancel_queries_for_queue(client_queue)
|
|
1419
|
+
state_mutex.synchronize { cancel_queries_for_queue_synchronized(client_queue) }
|
|
1420
|
+
wake_up_select_thread
|
|
1421
|
+
end
|
|
1422
|
+
|
|
1423
|
+
def cancel_queries_for_queue_synchronized(client_queue)
|
|
1424
|
+
ids = @@queries_by_queue[client_queue]
|
|
1425
|
+
ids.to_a.each { |id| remove_id_from_mutex_synchronized_block(id) } if ids
|
|
1426
|
+
end
|
|
1427
|
+
private :cancel_queries_for_queue_synchronized
|
|
1428
|
+
|
|
1429
|
+
def remove_observer_queue_count(observer)
|
|
1430
|
+
count = @@observer_queue_counts[observer] - 1
|
|
1431
|
+
if count == 0
|
|
1432
|
+
@@observer_queue_counts.delete(observer)
|
|
1433
|
+
@@tick_observers.delete(observer)
|
|
1434
|
+
else
|
|
1435
|
+
@@observer_queue_counts[observer] = count
|
|
1436
|
+
end
|
|
1437
|
+
end
|
|
1438
|
+
private :remove_observer_queue_count
|
|
1439
|
+
|
|
779
1440
|
def remove_observer(client_queue, observer)
|
|
780
|
-
|
|
1441
|
+
state_mutex.synchronize {
|
|
781
1442
|
if (@@observers[client_queue]==observer)
|
|
782
|
-
|
|
783
|
-
|
|
1443
|
+
registered_observer = @@observers.delete(client_queue)
|
|
1444
|
+
# Resolver completion/close owns every packet attempt for this queue,
|
|
1445
|
+
# including queued sends which have not opened a socket yet.
|
|
1446
|
+
cancel_queries_for_queue_synchronized(client_queue)
|
|
1447
|
+
cancel_validations_for_queue_synchronized(client_queue)
|
|
1448
|
+
remove_observer_queue_count(registered_observer)
|
|
784
1449
|
else
|
|
785
|
-
if (@@observers[client_queue] == nil)
|
|
786
|
-
end
|
|
787
1450
|
Dnsruby.log.error{"remove_observer called with wrong observer for queue"}
|
|
788
1451
|
raise ArgumentError.new("remove_observer called with wrong observer for queue")
|
|
789
1452
|
end
|
|
790
|
-
if (!@@observers.values.include?observer)
|
|
791
|
-
@@tick_observers.delete(observer)
|
|
792
|
-
end
|
|
793
1453
|
}
|
|
794
1454
|
end
|
|
795
1455
|
|
|
796
1456
|
def notify_queue_observers(client_queue, client_query_id)
|
|
797
1457
|
# If any observers are known for this query queue then notify them
|
|
798
1458
|
observer=nil
|
|
799
|
-
|
|
1459
|
+
state_mutex.synchronize {
|
|
800
1460
|
observer = @@observers[client_queue]
|
|
801
1461
|
}
|
|
802
1462
|
if (observer)
|
|
803
1463
|
observer.handle_queue_event(client_queue, client_query_id)
|
|
804
1464
|
end
|
|
1465
|
+
rescue ClosedQueueError
|
|
1466
|
+
Dnsruby.log.debug { 'Observer could not deliver to a closed client queue' }
|
|
805
1467
|
end
|
|
806
1468
|
|
|
807
|
-
def
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1469
|
+
def deliver_queue_event(client_queue, event)
|
|
1470
|
+
client_queue.push(event)
|
|
1471
|
+
notify_queue_observers(client_queue, event.first)
|
|
1472
|
+
true
|
|
1473
|
+
rescue ClosedQueueError
|
|
1474
|
+
# A direct PacketSender caller can close its queue while queries are
|
|
1475
|
+
# pending. Cancel those queries so other clients can keep running.
|
|
1476
|
+
state_mutex.synchronize do
|
|
1477
|
+
cancel_queries_for_queue_synchronized(client_queue)
|
|
1478
|
+
cancel_validations_for_queue_synchronized(client_queue)
|
|
1479
|
+
observer = @@observers.delete(client_queue)
|
|
1480
|
+
remove_observer_queue_count(observer) if observer
|
|
1481
|
+
end
|
|
1482
|
+
Dnsruby.log.debug { 'Discarding event for closed client queue' }
|
|
1483
|
+
false
|
|
1484
|
+
end
|
|
1485
|
+
private :deliver_queue_event
|
|
1486
|
+
|
|
1487
|
+
def send_tick_to_observers(due_only = true)
|
|
1488
|
+
# Observer callbacks acquire Resolver locks, so never call them under the
|
|
1489
|
+
# selector lock. Tick only observers with a retry or query deadline due.
|
|
1490
|
+
tick_observers = state_mutex.synchronize { @@tick_observers.dup }
|
|
813
1491
|
tick_observers.each do |observer|
|
|
814
|
-
|
|
1492
|
+
begin
|
|
1493
|
+
if observer.respond_to?(:next_tick_at)
|
|
1494
|
+
deadline = observer.next_tick_at
|
|
1495
|
+
observer.tick if !due_only || (deadline && deadline <= TransportClock.now)
|
|
1496
|
+
else
|
|
1497
|
+
observer.tick
|
|
1498
|
+
end
|
|
1499
|
+
rescue ClosedQueueError
|
|
1500
|
+
Dnsruby.log.debug { 'Observer tick could not deliver to a closed client queue' }
|
|
1501
|
+
end
|
|
815
1502
|
end
|
|
816
1503
|
end
|
|
1504
|
+
|
|
1505
|
+
def next_observer_deadline
|
|
1506
|
+
observers = state_mutex.synchronize { @@tick_observers.dup }
|
|
1507
|
+
observers.map do |observer|
|
|
1508
|
+
observer.respond_to?(:next_tick_at) ? observer.next_tick_at : TransportClock.now + 0.1
|
|
1509
|
+
end.compact.min
|
|
1510
|
+
end
|
|
1511
|
+
private :next_observer_deadline
|
|
817
1512
|
end
|
|
818
1513
|
end
|