dnsruby 1.74.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -4
- data/DNSSEC +3 -2
- data/EXAMPLES +1 -1
- data/README.md +370 -64
- data/RELEASE_NOTES.md +302 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/demo/rubydig.rb +0 -2
- data/dnsruby.gemspec +5 -4
- data/lib/dnsruby/cache.rb +279 -47
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +75 -4
- data/lib/dnsruby/deadline_queue.rb +75 -0
- data/lib/dnsruby/dnssec.rb +148 -45
- data/lib/dnsruby/ipv6.rb +45 -5
- data/lib/dnsruby/key_cache.rb +202 -35
- data/lib/dnsruby/message/decoder.rb +85 -19
- data/lib/dnsruby/message/encoder.rb +27 -16
- data/lib/dnsruby/message/header.rb +8 -12
- data/lib/dnsruby/message/message.rb +53 -22
- data/lib/dnsruby/message/section.rb +12 -34
- data/lib/dnsruby/name.rb +5 -2
- data/lib/dnsruby/packet_sender.rb +645 -203
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +795 -339
- data/lib/dnsruby/resolver.rb +468 -186
- data/lib/dnsruby/resource/CAA.rb +67 -16
- data/lib/dnsruby/resource/DLV.rb +6 -0
- data/lib/dnsruby/resource/DNSKEY.rb +108 -16
- data/lib/dnsruby/resource/DS.rb +27 -28
- data/lib/dnsruby/resource/HTTPS.rb +16 -0
- data/lib/dnsruby/resource/IN.rb +19 -0
- data/lib/dnsruby/resource/MX.rb +2 -2
- data/lib/dnsruby/resource/NSEC.rb +6 -10
- data/lib/dnsruby/resource/NSEC3.rb +55 -19
- data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
- data/lib/dnsruby/resource/RR.rb +108 -65
- data/lib/dnsruby/resource/RRSIG.rb +5 -5
- data/lib/dnsruby/resource/RRSet.rb +38 -22
- data/lib/dnsruby/resource/SRV.rb +2 -2
- data/lib/dnsruby/resource/SVCB.rb +775 -0
- data/lib/dnsruby/resource/TSIG.rb +266 -231
- data/lib/dnsruby/resource/TXT.rb +34 -42
- data/lib/dnsruby/resource/domain_name.rb +1 -1
- data/lib/dnsruby/resource/generic.rb +31 -0
- data/lib/dnsruby/resource/generic_cache.rb +384 -0
- data/lib/dnsruby/resource/generic_constants.rb +79 -0
- data/lib/dnsruby/select_thread.rb +979 -291
- data/lib/dnsruby/serial_time.rb +25 -0
- data/lib/dnsruby/single_resolver.rb +25 -6
- data/lib/dnsruby/single_verifier.rb +1620 -530
- data/lib/dnsruby/transport_clock.rb +9 -0
- data/lib/dnsruby/update.rb +8 -6
- data/lib/dnsruby/validation_budget.rb +143 -0
- data/lib/dnsruby/validator_thread.rb +139 -58
- data/lib/dnsruby/version.rb +1 -1
- data/lib/dnsruby/zone_reader.rb +196 -129
- data/lib/dnsruby/zone_transfer.rb +157 -25
- data/lib/dnsruby.rb +7 -2
- data/test/ci_watchdog.rb +33 -0
- data/test/fixtures/org-ds-2026-11-05.zone +5 -0
- data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
- data/test/fork_test_helper.rb +37 -0
- data/test/local_dns_server.rb +57 -0
- data/test/soak_server.rb +152 -0
- data/test/socket_helpers.rb +26 -0
- data/test/spec_helper.rb +8 -13
- data/test/tc_attempt_configuration.rb +137 -0
- data/test/tc_caa.rb +2 -2
- data/test/tc_cache_keys.rb +36 -0
- data/test/tc_cancelled_send_capacity.rb +158 -0
- data/test/tc_dnssec_alias_validation.rb +289 -0
- data/test/tc_dnssec_budget.rb +489 -0
- data/test/tc_dnssec_cache_bounds.rb +197 -0
- data/test/tc_dnssec_cache_lifetime.rb +615 -0
- data/test/tc_dnssec_denial_proofs.rb +285 -0
- data/test/tc_dnssec_recursion_limits.rb +186 -0
- data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
- data/test/tc_dnssec_trust_anchors.rb +286 -0
- data/test/tc_dnssec_trust_races.rb +147 -0
- data/test/tc_dnssec_wildcard_proofs.rb +249 -0
- data/test/tc_ds.rb +5 -5
- data/test/tc_edns_retry_regressions.rb +255 -0
- data/test/tc_escapedchars.rb +8 -0
- data/test/tc_final_review_fixes.rb +167 -0
- data/test/tc_final_transport_contracts.rb +104 -0
- data/test/tc_generic_cache.rb +852 -0
- data/test/tc_header.rb +26 -0
- data/test/tc_ipv6.rb +120 -0
- data/test/tc_key_cache_identity.rb +382 -0
- data/test/tc_malformed_tcp.rb +712 -0
- data/test/tc_name.rb +99 -5
- data/test/tc_nameserver_bootstrap.rb +322 -0
- data/test/tc_nsec.rb +4 -45
- data/test/tc_nsec3.rb +17 -1
- data/test/tc_pipeline_retirement.rb +358 -0
- data/test/tc_pipeline_retry.rb +462 -0
- data/test/tc_pipeline_send.rb +663 -0
- data/test/tc_protocol_review_fixes.rb +95 -0
- data/test/tc_query_control.rb +90 -0
- data/test/tc_record_codec_contracts.rb +107 -0
- data/test/tc_record_regressions.rb +225 -0
- data/test/tc_recur.rb +1 -0
- data/test/tc_recursor_address_fallback.rb +169 -0
- data/test/tc_recursor_limits.rb +139 -0
- data/test/tc_release_auxiliary_fixes.rb +288 -0
- data/test/tc_release_configuration_fixes.rb +612 -0
- data/test/tc_release_dnssec_completion.rb +405 -0
- data/test/tc_release_dnssec_fixes.rb +305 -0
- data/test/tc_release_negative_cache.rb +327 -0
- data/test/tc_release_recursor_cache.rb +212 -0
- data/test/tc_release_review_transport.rb +372 -0
- data/test/tc_release_review_transport_native.rb +56 -0
- data/test/tc_release_transport.rb +906 -0
- data/test/tc_release_transport_fixes.rb +196 -0
- data/test/tc_release_zone_defaults.rb +157 -0
- data/test/tc_release_zone_ttls.rb +39 -0
- data/test/tc_res_config.rb +42 -20
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +21 -19
- data/test/tc_resolver_concurrency.rb +383 -0
- data/test/tc_resolver_configuration_contracts.rb +31 -0
- data/test/tc_resolver_validation.rb +211 -0
- data/test/tc_review_cache.rb +163 -0
- data/test/tc_review_code_mapper.rb +279 -0
- data/test/tc_review_control_deadlines.rb +169 -0
- data/test/tc_review_followup_transport.rb +583 -0
- data/test/tc_review_packet_contracts.rb +369 -0
- data/test/tc_review_presentation.rb +213 -0
- data/test/tc_review_record_compatibility.rb +101 -0
- data/test/tc_review_transport_fixes.rb +195 -0
- data/test/tc_review_transport_native.rb +415 -0
- data/test/tc_review_validation_priming.rb +178 -0
- data/test/tc_rr-opt.rb +5 -2
- data/test/tc_rr-unknown.rb +19 -0
- data/test/tc_rr.rb +14 -0
- data/test/tc_rrset_grouping.rb +50 -0
- data/test/tc_rrset_review_fixes.rb +58 -0
- data/test/tc_rrsig.rb +21 -1
- data/test/tc_scoped_ipv6.rb +153 -0
- data/test/tc_scoped_ipv6_select.rb +157 -0
- data/test/tc_scoped_ipv6_transport.rb +329 -0
- data/test/tc_selector_wakeup.rb +42 -0
- data/test/tc_soak.rb +118 -196
- data/test/tc_socket_helpers.rb +90 -0
- data/test/tc_supporting_lookup_limits.rb +252 -0
- data/test/tc_svcb.rb +1043 -0
- data/test/tc_svcb_signed_target.rb +145 -0
- data/test/tc_svcb_transport.rb +120 -0
- data/test/tc_tcp.rb +69 -43
- data/test/tc_tcp_pipelining.rb +95 -204
- data/test/tc_transport_fork.rb +179 -0
- data/test/tc_tsig.rb +2 -2
- data/test/tc_tsig_api.rb +251 -0
- data/test/tc_tsig_rfc8945.rb +357 -0
- data/test/tc_v2_release_dnssec.rb +442 -0
- data/test/tc_v2_release_transport.rb +304 -0
- data/test/tc_v2_zone_transfer_native.rb +58 -0
- data/test/tc_zone_reader.rb +217 -1
- data/test/tcp_pipelining_server.rb +150 -0
- data/test/ts_offline.rb +77 -6
- data/test/ts_online.rb +2 -1
- metadata +105 -10
- data/.coveralls.yml +0 -2
|
@@ -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,8 +671,10 @@ 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
|
|
|
231
680
|
# We leave the @@wakeup_sockets here, but remove everything else
|
|
@@ -233,7 +682,7 @@ module Dnsruby
|
|
|
233
682
|
non_persistent_sockets.each do |socket|
|
|
234
683
|
if socket != @@wakeup_sockets[1]
|
|
235
684
|
socket.close rescue nil
|
|
236
|
-
|
|
685
|
+
retire_socket_from_mutex_synchronized_block(socket)
|
|
237
686
|
end
|
|
238
687
|
end
|
|
239
688
|
|
|
@@ -243,28 +692,44 @@ module Dnsruby
|
|
|
243
692
|
|
|
244
693
|
if @@sockets.size == 1
|
|
245
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
|
|
246
698
|
return
|
|
247
699
|
end
|
|
248
700
|
end
|
|
249
701
|
end
|
|
250
|
-
# }
|
|
251
702
|
end
|
|
252
703
|
end
|
|
253
704
|
|
|
254
|
-
#
|
|
255
|
-
#
|
|
705
|
+
# Detach closed sockets before notifying clients. Callbacks can register
|
|
706
|
+
# other queries, and must never run while holding the selector mutex.
|
|
256
707
|
def clean_up_closed_sockets
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
@@sockets.delete_if { | socket | socket.closed? }
|
|
261
|
-
closed_sockets_in_hash.each_with_object([]) do |socket, exception|
|
|
262
|
-
@@socket_hash[socket].each do | client_id |
|
|
263
|
-
exceptions << [SocketEofResolvError.new("TCP socket closed before all answers received"), socket, client_id]
|
|
264
|
-
end
|
|
708
|
+
state_mutex.synchronize do
|
|
709
|
+
@@sockets.select(&:closed?).flat_map do |socket|
|
|
710
|
+
retire_socket_from_mutex_synchronized_block(socket)
|
|
265
711
|
end
|
|
266
712
|
end
|
|
267
|
-
|
|
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
|
|
268
733
|
end
|
|
269
734
|
|
|
270
735
|
def process_error(errors)
|
|
@@ -272,92 +737,96 @@ module Dnsruby
|
|
|
272
737
|
# @todo@ Process errors [can we do this in single socket environment?]
|
|
273
738
|
end
|
|
274
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.
|
|
275
742
|
def get_active_ids(queries, id)
|
|
276
743
|
queries.keys.select { |client_query_id| client_query_id[1].header.id == id }
|
|
277
744
|
end
|
|
278
745
|
|
|
279
|
-
# @@query_hash[query_settings.client_query_id]=query_settings
|
|
280
746
|
def process_ready(ready)
|
|
281
|
-
persistent_sockets, nonpersistent_sockets =
|
|
747
|
+
persistent_sockets, nonpersistent_sockets = state_mutex.synchronize { ready.partition { |socket| persistent?(socket) } }
|
|
282
748
|
|
|
283
749
|
nonpersistent_sockets.each do |socket|
|
|
284
|
-
query_settings =
|
|
750
|
+
query_settings = state_mutex.synchronize { @@query_hash[(@@socket_hash[socket] || []).first] }
|
|
285
751
|
next if !query_settings
|
|
286
752
|
|
|
287
753
|
udp_packet_size = query_settings.udp_packet_size
|
|
288
754
|
msg, bytes = get_incoming_data(socket, udp_packet_size)
|
|
289
755
|
|
|
290
756
|
process_message(msg, bytes, socket) if msg
|
|
291
|
-
|
|
292
|
-
ready.delete(socket)
|
|
293
757
|
end
|
|
294
758
|
|
|
295
759
|
persistent_sockets.each do |socket|
|
|
296
760
|
msg, bytes = get_incoming_data(socket, 0)
|
|
297
761
|
process_message(msg, bytes, socket) if msg
|
|
298
|
-
ready.delete(socket)
|
|
299
762
|
end
|
|
300
763
|
end
|
|
301
764
|
|
|
302
765
|
def process_message(msg, bytes, socket)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
answerip = msg.answerip.downcase
|
|
309
|
-
answerfrom = msg.answerfrom.downcase
|
|
310
|
-
answeripaddr = IPAddr.new(answerip)
|
|
311
|
-
dest_server = IPAddr.new("0.0.0.0")
|
|
312
|
-
|
|
313
|
-
begin
|
|
314
|
-
destserveripaddr = IPAddr.new(dest_server)
|
|
315
|
-
rescue ArgumentError
|
|
316
|
-
# Host name not IP address
|
|
317
|
-
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
|
|
318
770
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
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
|
|
326
777
|
end
|
|
778
|
+
private :matching_questions?
|
|
327
779
|
|
|
328
780
|
def send_response_to_client(msg, bytes, socket)
|
|
329
|
-
#
|
|
330
|
-
# @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.
|
|
331
782
|
client_ids=[]
|
|
332
|
-
|
|
333
|
-
client_ids =
|
|
783
|
+
state_mutex.synchronize{
|
|
784
|
+
client_ids = socket_ids_for_header(socket, msg.header.id)
|
|
334
785
|
}
|
|
335
786
|
# get the queries associated with them
|
|
336
787
|
client_ids.each do |id|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
788
|
+
settings = state_mutex.synchronize { @@query_hash[id] }
|
|
789
|
+
next unless settings && settings.socket == socket
|
|
790
|
+
query_header_id = settings.query.header.id
|
|
341
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
|
|
342
799
|
# process the response
|
|
343
|
-
client_queue =
|
|
344
|
-
res =
|
|
345
|
-
query=
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
352
809
|
# At this point, we should check if the response is OK
|
|
353
|
-
|
|
354
|
-
|
|
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)
|
|
355
823
|
exception = msg.get_exception
|
|
356
824
|
if (ret.kind_of?TsigError)
|
|
357
825
|
exception = ret
|
|
358
826
|
end
|
|
359
827
|
Dnsruby.log.debug{"Pushing response to client queue"}
|
|
360
|
-
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)
|
|
361
830
|
# client_queue.push([id, msg, exception])
|
|
362
831
|
# notify_queue_observers(client_queue, id)
|
|
363
832
|
else
|
|
@@ -368,7 +837,6 @@ module Dnsruby
|
|
|
368
837
|
end
|
|
369
838
|
# If not, then we have an error
|
|
370
839
|
Dnsruby.log.error{"Stray packet - " + msg.inspect + "\n from " + socket.inspect}
|
|
371
|
-
print("Stray packet - " + msg.question()[0].qname.to_s + " from " + msg.answerip.to_s + ", #{client_ids.length} client_ids\n")
|
|
372
840
|
end
|
|
373
841
|
|
|
374
842
|
def persistent?(socket)
|
|
@@ -376,31 +844,69 @@ module Dnsruby
|
|
|
376
844
|
end
|
|
377
845
|
|
|
378
846
|
def remove_id(id)
|
|
379
|
-
|
|
847
|
+
state_mutex.synchronize do
|
|
380
848
|
remove_id_from_mutex_synchronized_block(id)
|
|
381
849
|
end
|
|
382
850
|
end
|
|
383
851
|
|
|
384
852
|
# THIS MUST BE CALLED FROM INSIDE A @@mutex SYNCHRONISED BLOCK!
|
|
385
|
-
def remove_id_from_mutex_synchronized_block(id)
|
|
386
|
-
|
|
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)
|
|
387
866
|
@@timeouts.delete(id)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
392
879
|
|
|
393
|
-
|
|
394
|
-
@@
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
@@socket_is_persistent.delete(socket)
|
|
398
|
-
@@tcp_buffers.delete(socket)
|
|
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?)
|
|
399
884
|
Dnsruby.log.debug("Closing socket #{socket}")
|
|
400
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]
|
|
401
893
|
end
|
|
402
894
|
end
|
|
403
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
|
+
|
|
404
910
|
def decrement_remaining_queries(socket)
|
|
405
911
|
if @@socket_remaining_queries[socket]
|
|
406
912
|
@@socket_remaining_queries[socket] -= 1
|
|
@@ -415,12 +921,22 @@ module Dnsruby
|
|
|
415
921
|
end
|
|
416
922
|
|
|
417
923
|
def process_timeouts
|
|
418
|
-
#
|
|
419
|
-
|
|
420
|
-
time_now =
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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")])
|
|
424
940
|
end
|
|
425
941
|
end
|
|
426
942
|
end
|
|
@@ -431,58 +947,67 @@ module Dnsruby
|
|
|
431
947
|
# then process message.
|
|
432
948
|
buf = +""
|
|
433
949
|
expected_length = 0
|
|
434
|
-
|
|
435
|
-
|
|
950
|
+
reading_length = nil
|
|
951
|
+
state_mutex.synchronize {
|
|
952
|
+
return false if socket.closed?
|
|
953
|
+
buf, expected_length, reading_length = @@tcp_buffers[socket]
|
|
436
954
|
if (!buf)
|
|
437
955
|
buf = +""
|
|
438
956
|
expected_length = 2
|
|
439
|
-
|
|
957
|
+
reading_length = true
|
|
958
|
+
@@tcp_buffers[socket]=[buf, expected_length, reading_length]
|
|
440
959
|
end
|
|
441
960
|
}
|
|
442
961
|
if (buf.length() < expected_length)
|
|
443
962
|
begin
|
|
444
963
|
input, = socket.recv_nonblock(expected_length-buf.length)
|
|
445
|
-
|
|
964
|
+
# Depending on the Ruby version, EOF is nil or an empty string.
|
|
965
|
+
if input.nil? || input.empty?
|
|
446
966
|
Dnsruby.log.debug("EOF from server - no bytes read - closing socket")
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
@@sockets.delete(socket) #remove ourselves from select, app will have to retry
|
|
451
|
-
#maybe fire an event
|
|
452
|
-
@@socket_hash[socket].map do | client_id |
|
|
453
|
-
[SocketEofResolvError.new("TCP socket closed before all answers received"), socket, client_id]
|
|
454
|
-
end
|
|
967
|
+
exceptions = state_mutex.synchronize do
|
|
968
|
+
socket.close
|
|
969
|
+
retire_socket_from_mutex_synchronized_block(socket)
|
|
455
970
|
end
|
|
456
|
-
|
|
457
971
|
exceptions.each { |exception| send_exception_to_client(*exception) }
|
|
458
972
|
|
|
459
973
|
return false
|
|
460
974
|
end
|
|
461
975
|
buf << input if input
|
|
462
|
-
rescue
|
|
463
|
-
|
|
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}" }
|
|
464
987
|
return false
|
|
465
988
|
end
|
|
466
989
|
end
|
|
467
990
|
# If data is complete, then return it.
|
|
468
991
|
if (buf.length == expected_length)
|
|
469
|
-
if
|
|
992
|
+
if reading_length
|
|
470
993
|
# We just read the data_length field. Now we need to start reading that many bytes.
|
|
471
|
-
|
|
994
|
+
state_mutex.synchronize {
|
|
995
|
+
return false if socket.closed?
|
|
472
996
|
answersize = buf.unpack('n')[0]
|
|
473
|
-
@@tcp_buffers[socket] = [+"", answersize]
|
|
997
|
+
@@tcp_buffers[socket] = [+"", answersize, false]
|
|
474
998
|
}
|
|
475
999
|
return tcp_read(socket)
|
|
476
1000
|
else
|
|
477
1001
|
# We just read the data - now return it
|
|
478
|
-
|
|
1002
|
+
state_mutex.synchronize {
|
|
479
1003
|
@@tcp_buffers.delete(socket)
|
|
480
1004
|
}
|
|
481
1005
|
return buf
|
|
482
1006
|
end
|
|
483
1007
|
else
|
|
484
|
-
|
|
485
|
-
|
|
1008
|
+
state_mutex.synchronize {
|
|
1009
|
+
return false if socket.closed?
|
|
1010
|
+
@@tcp_buffers[socket]=[buf, expected_length, reading_length]
|
|
486
1011
|
}
|
|
487
1012
|
return false
|
|
488
1013
|
end
|
|
@@ -491,7 +1016,14 @@ module Dnsruby
|
|
|
491
1016
|
def get_incoming_data(socket, packet_size)
|
|
492
1017
|
answerfrom,answerip,answerport,answersize=nil
|
|
493
1018
|
ans,buf = nil
|
|
494
|
-
|
|
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
|
|
495
1027
|
|
|
496
1028
|
begin
|
|
497
1029
|
if is_tcp
|
|
@@ -505,12 +1037,11 @@ module Dnsruby
|
|
|
505
1037
|
else
|
|
506
1038
|
# @TODO@ Can we get recvfrom to stop issuing PTR queries when we already
|
|
507
1039
|
# know both the FQDN and the IP address?
|
|
508
|
-
if (ret = socket.
|
|
1040
|
+
if (ret = socket.recvfrom_nonblock(packet_size))
|
|
509
1041
|
buf = ret[0]
|
|
510
1042
|
answerport=ret[1][1]
|
|
511
1043
|
answerfrom=ret[1][2]
|
|
512
1044
|
answerip=ret[1][3]
|
|
513
|
-
answersize=(buf.length)
|
|
514
1045
|
else
|
|
515
1046
|
# recvfrom failed - why?
|
|
516
1047
|
Dnsruby.log.error{"Error - recvfrom failed from #{socket}"}
|
|
@@ -518,72 +1049,111 @@ module Dnsruby
|
|
|
518
1049
|
return
|
|
519
1050
|
end
|
|
520
1051
|
end
|
|
1052
|
+
rescue IO::WaitReadable, Errno::EINTR
|
|
1053
|
+
# Readiness is only a hint; keep the query registered for its next event.
|
|
1054
|
+
return
|
|
521
1055
|
rescue Exception => e
|
|
522
1056
|
Dnsruby.log.error{"Error - recvfrom failed from #{socket}, exception : #{e}"}
|
|
523
1057
|
handle_recvfrom_failure(socket, e)
|
|
524
1058
|
return
|
|
525
1059
|
end
|
|
1060
|
+
answersize = buf.bytesize
|
|
526
1061
|
Dnsruby.log.debug{";; answer from #{answerfrom} : #{answersize} bytes\n"}
|
|
527
1062
|
|
|
528
1063
|
begin
|
|
529
1064
|
ans = Message.decode(buf)
|
|
530
1065
|
|
|
531
|
-
if is_tcp
|
|
532
|
-
@@mutex.synchronize do
|
|
533
|
-
ids = get_active_ids(@@query_hash, ans.header.id)
|
|
534
|
-
if ids.empty?
|
|
535
|
-
Dnsruby.log.error("Decode error from #{answerip} but can't determine packet id")
|
|
536
|
-
#todo add error event? The problem is we don't have a valid id so we don't
|
|
537
|
-
#know which client queue to send the exception to
|
|
538
|
-
end
|
|
539
|
-
answerfrom = @@query_hash[ids[0]].dest_server
|
|
540
|
-
answerip = answerfrom
|
|
541
|
-
answerport = @@query_hash[ids[0]].dest_port
|
|
542
|
-
end
|
|
543
|
-
end
|
|
544
|
-
|
|
545
1066
|
rescue Exception => e
|
|
546
1067
|
Dnsruby.log.error("Decode error! #{e.class}, #{e}\nfor msg (length=#{buf.length}) : #{buf}")
|
|
547
|
-
|
|
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)
|
|
548
1072
|
if (client_id == nil)
|
|
549
1073
|
Dnsruby.log.error{"Decode error from #{answerip} but can't determine packet id"}
|
|
1074
|
+
return
|
|
550
1075
|
end
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
|
575
1123
|
end
|
|
576
|
-
}
|
|
577
|
-
if !sent
|
|
578
|
-
send_exception_to_client(e, socket, client_id)
|
|
579
1124
|
end
|
|
580
|
-
|
|
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
|
|
581
1135
|
else
|
|
582
1136
|
send_exception_to_client(e, socket, client_id)
|
|
583
1137
|
end
|
|
584
1138
|
return
|
|
585
1139
|
end
|
|
586
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
|
+
|
|
587
1157
|
if (ans!= nil)
|
|
588
1158
|
Dnsruby.log.debug{"#{ans}"}
|
|
589
1159
|
ans.answerfrom=(answerfrom)
|
|
@@ -597,14 +1167,15 @@ module Dnsruby
|
|
|
597
1167
|
# No way to notify the client about this error, unless there was only one connection on the socket
|
|
598
1168
|
# Not a problem, as there only will ever be one connection on the socket (Kaminsky attack mitigation)
|
|
599
1169
|
ids_for_socket = []
|
|
600
|
-
|
|
601
|
-
ids_for_socket = @@socket_hash[socket]
|
|
1170
|
+
state_mutex.synchronize{
|
|
1171
|
+
ids_for_socket = (@@socket_hash[socket] || []).to_a
|
|
602
1172
|
}
|
|
1173
|
+
return if ids_for_socket.empty?
|
|
603
1174
|
if (ids_for_socket.length == 1)
|
|
604
1175
|
answerfrom=nil
|
|
605
|
-
|
|
1176
|
+
state_mutex.synchronize{
|
|
606
1177
|
query_settings = @@query_hash[ids_for_socket[0]]
|
|
607
|
-
answerfrom=query_settings.dest_server
|
|
1178
|
+
answerfrom=query_settings.dest_server if query_settings
|
|
608
1179
|
}
|
|
609
1180
|
send_exception_to_client(OtherResolvError.new("recvfrom failed from #{answerfrom}; #{exception}"), socket, ids_for_socket[0])
|
|
610
1181
|
else
|
|
@@ -612,16 +1183,33 @@ module Dnsruby
|
|
|
612
1183
|
end
|
|
613
1184
|
end
|
|
614
1185
|
|
|
615
|
-
def get_client_id_from_answerfrom(socket, answerip, answerport)
|
|
616
|
-
#
|
|
1186
|
+
def get_client_id_from_answerfrom(socket, answerip, answerport, header_id=nil)
|
|
1187
|
+
# A pipelined socket can have several outstanding queries.
|
|
617
1188
|
client_id=nil
|
|
618
1189
|
# Figure out client id from answerfrom
|
|
619
|
-
|
|
620
|
-
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
|
|
621
1204
|
ids.each do |id|
|
|
622
1205
|
# Does this id speak to this dest_server?
|
|
623
1206
|
query_settings = @@query_hash[id]
|
|
624
|
-
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)
|
|
625
1213
|
# We have a match
|
|
626
1214
|
client_id = id
|
|
627
1215
|
break
|
|
@@ -634,47 +1222,36 @@ module Dnsruby
|
|
|
634
1222
|
def send_exception_to_client(err, socket, client_id, msg=nil)
|
|
635
1223
|
# find the client response queue
|
|
636
1224
|
client_queue = nil
|
|
637
|
-
|
|
638
|
-
|
|
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)
|
|
639
1230
|
}
|
|
640
|
-
remove_id(client_id)
|
|
641
1231
|
# push_to_client(client_id, client_queue, msg, err)
|
|
642
|
-
client_queue
|
|
643
|
-
notify_queue_observers(client_queue, client_id)
|
|
1232
|
+
deliver_queue_event(client_queue, [client_id, Resolver::EventType::ERROR, msg, err])
|
|
644
1233
|
end
|
|
645
1234
|
|
|
646
1235
|
def push_exception_to_select(client_id, client_queue, err, msg)
|
|
647
|
-
|
|
1236
|
+
state_mutex.synchronize{
|
|
648
1237
|
@@queued_exceptions.push([client_id, client_queue, err, msg])
|
|
1238
|
+
check_select_thread_synchronized
|
|
649
1239
|
}
|
|
650
|
-
# Make sure select loop is running!
|
|
651
|
-
if (@@select_thread && @@select_thread.alive?)
|
|
652
|
-
else
|
|
653
|
-
@@select_thread = Thread.new {
|
|
654
|
-
do_select
|
|
655
|
-
}
|
|
656
|
-
end
|
|
657
1240
|
wake_up_select_thread
|
|
658
1241
|
end
|
|
659
1242
|
|
|
660
|
-
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)
|
|
661
1244
|
# This needs to queue the response TO THE SELECT THREAD, which then needs
|
|
662
1245
|
# to send it out from its normal loop.
|
|
663
1246
|
Dnsruby.log.debug{"Pushing response to client queue direct from resolver or validator"}
|
|
664
|
-
|
|
1247
|
+
state_mutex.synchronize{
|
|
665
1248
|
err = nil
|
|
666
1249
|
if (msg.rcode == RCode.NXDOMAIN)
|
|
667
1250
|
err = NXDomain.new
|
|
668
1251
|
end
|
|
669
|
-
@@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
|
|
670
1254
|
}
|
|
671
|
-
# Make sure select loop is running!
|
|
672
|
-
if (@@select_thread && @@select_thread.alive?)
|
|
673
|
-
else
|
|
674
|
-
@@select_thread = Thread.new {
|
|
675
|
-
do_select
|
|
676
|
-
}
|
|
677
|
-
end
|
|
678
1255
|
wake_up_select_thread
|
|
679
1256
|
end
|
|
680
1257
|
|
|
@@ -682,22 +1259,16 @@ module Dnsruby
|
|
|
682
1259
|
# This needs to queue the response TO THE SELECT THREAD, which then needs
|
|
683
1260
|
# to send it out from its normal loop.
|
|
684
1261
|
Dnsruby.log.debug{"Pushing response to client queue direct from resolver or validator"}
|
|
685
|
-
|
|
1262
|
+
state_mutex.synchronize{
|
|
686
1263
|
@@queued_validation_responses.push([client_id, client_queue, msg, err, query, res])
|
|
1264
|
+
check_select_thread_synchronized
|
|
687
1265
|
}
|
|
688
|
-
# Make sure select loop is running!
|
|
689
|
-
if (@@select_thread && @@select_thread.alive?)
|
|
690
|
-
else
|
|
691
|
-
@@select_thread = Thread.new {
|
|
692
|
-
do_select
|
|
693
|
-
}
|
|
694
|
-
end
|
|
695
1266
|
wake_up_select_thread
|
|
696
1267
|
end
|
|
697
1268
|
|
|
698
1269
|
def send_queued_exceptions
|
|
699
1270
|
exceptions = []
|
|
700
|
-
|
|
1271
|
+
state_mutex.synchronize{
|
|
701
1272
|
exceptions = @@queued_exceptions
|
|
702
1273
|
@@queued_exceptions = []
|
|
703
1274
|
}
|
|
@@ -705,23 +1276,21 @@ module Dnsruby
|
|
|
705
1276
|
exceptions.each do |item|
|
|
706
1277
|
client_id, client_queue, err, msg = item
|
|
707
1278
|
# push_to_client(client_id, client_queue, msg, err)
|
|
708
|
-
client_queue
|
|
709
|
-
notify_queue_observers(client_queue, client_id)
|
|
1279
|
+
deliver_queue_event(client_queue, [client_id, Resolver::EventType::ERROR, msg, err])
|
|
710
1280
|
end
|
|
711
1281
|
end
|
|
712
1282
|
|
|
713
1283
|
def send_queued_responses
|
|
714
1284
|
responses = []
|
|
715
|
-
|
|
1285
|
+
state_mutex.synchronize{
|
|
716
1286
|
responses = @@queued_responses
|
|
717
1287
|
@@queued_responses = []
|
|
718
1288
|
}
|
|
719
1289
|
|
|
720
1290
|
responses.each do |item|
|
|
721
|
-
client_id, client_queue, msg, err, query, res = item
|
|
1291
|
+
client_id, client_queue, msg, err, query, res, transmission_budget = item
|
|
722
1292
|
# push_to_client(client_id, client_queue, msg, err)
|
|
723
|
-
client_queue
|
|
724
|
-
notify_queue_observers(client_queue, client_id)
|
|
1293
|
+
next unless deliver_queue_event(client_queue, [client_id, Resolver::EventType::RECEIVED, msg, err])
|
|
725
1294
|
# Do we need to validate this? The response has come from the cache -
|
|
726
1295
|
# validate it only if it has not been validated already
|
|
727
1296
|
# So, if we need to validate it, send it to the validation thread
|
|
@@ -729,19 +1298,18 @@ module Dnsruby
|
|
|
729
1298
|
if (((msg.security_level == Message::SecurityLevel.UNCHECKED) ||
|
|
730
1299
|
(msg.security_level == Message::SecurityLevel.INDETERMINATE)) &&
|
|
731
1300
|
(ValidatorThread.requires_validation?(query, msg, err, res)))
|
|
732
|
-
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)
|
|
733
1302
|
validator.run
|
|
734
1303
|
else
|
|
735
1304
|
PacketSender.cache(query, msg) # The validator won't cache it, so we'd better do it now
|
|
736
|
-
client_queue
|
|
737
|
-
notify_queue_observers(client_queue, client_id)
|
|
1305
|
+
deliver_queue_event(client_queue, [client_id, Resolver::EventType::VALIDATED, msg, err])
|
|
738
1306
|
end
|
|
739
1307
|
end
|
|
740
1308
|
end
|
|
741
1309
|
|
|
742
1310
|
def send_queued_validation_responses
|
|
743
1311
|
responses = []
|
|
744
|
-
|
|
1312
|
+
state_mutex.synchronize{
|
|
745
1313
|
responses = @@queued_validation_responses
|
|
746
1314
|
@@queued_validation_responses = []
|
|
747
1315
|
}
|
|
@@ -749,77 +1317,197 @@ module Dnsruby
|
|
|
749
1317
|
responses.each do |item|
|
|
750
1318
|
client_id, client_queue, msg, err, _query, _res = item
|
|
751
1319
|
# push_to_client(client_id, client_queue, msg, err)
|
|
752
|
-
client_queue
|
|
753
|
-
notify_queue_observers(client_queue, client_id)
|
|
1320
|
+
deliver_queue_event(client_queue, [client_id, Resolver::EventType::VALIDATED, msg, err])
|
|
754
1321
|
end
|
|
755
1322
|
end
|
|
756
1323
|
|
|
757
|
-
def push_to_client(client_id, client_queue, msg, err, query, res)
|
|
758
|
-
#
|
|
759
|
-
#
|
|
760
|
-
|
|
761
|
-
# type of response.
|
|
762
|
-
client_queue.push([client_id, Resolver::EventType::RECEIVED, msg, err])
|
|
763
|
-
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])
|
|
764
1328
|
|
|
765
1329
|
if (!err || (err.instance_of?(NXDomain)))
|
|
766
|
-
#
|
|
767
|
-
|
|
768
|
-
# which will then take responsibility for delivering it to the client.
|
|
769
|
-
# The validator will need access to the queue observers -
|
|
770
|
-
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)
|
|
771
1332
|
validator.run
|
|
772
|
-
# @@validator.add_to_queue([client_id, client_queue, msg, err, query, self, res])
|
|
773
1333
|
end
|
|
774
1334
|
end
|
|
775
1335
|
|
|
776
1336
|
def add_observer(client_queue, observer)
|
|
777
|
-
|
|
778
|
-
@@
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
|
782
1346
|
end
|
|
1347
|
+
check_select_thread_synchronized
|
|
783
1348
|
}
|
|
1349
|
+
# An empty resolver still has a logical deadline but no socket to wake us.
|
|
1350
|
+
wake_up_select_thread
|
|
784
1351
|
end
|
|
785
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
|
+
|
|
786
1440
|
def remove_observer(client_queue, observer)
|
|
787
|
-
|
|
1441
|
+
state_mutex.synchronize {
|
|
788
1442
|
if (@@observers[client_queue]==observer)
|
|
789
|
-
|
|
790
|
-
|
|
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)
|
|
791
1449
|
else
|
|
792
|
-
if (@@observers[client_queue] == nil)
|
|
793
|
-
end
|
|
794
1450
|
Dnsruby.log.error{"remove_observer called with wrong observer for queue"}
|
|
795
1451
|
raise ArgumentError.new("remove_observer called with wrong observer for queue")
|
|
796
1452
|
end
|
|
797
|
-
if (!@@observers.values.include?observer)
|
|
798
|
-
@@tick_observers.delete(observer)
|
|
799
|
-
end
|
|
800
1453
|
}
|
|
801
1454
|
end
|
|
802
1455
|
|
|
803
1456
|
def notify_queue_observers(client_queue, client_query_id)
|
|
804
1457
|
# If any observers are known for this query queue then notify them
|
|
805
1458
|
observer=nil
|
|
806
|
-
|
|
1459
|
+
state_mutex.synchronize {
|
|
807
1460
|
observer = @@observers[client_queue]
|
|
808
1461
|
}
|
|
809
1462
|
if (observer)
|
|
810
1463
|
observer.handle_queue_event(client_queue, client_query_id)
|
|
811
1464
|
end
|
|
1465
|
+
rescue ClosedQueueError
|
|
1466
|
+
Dnsruby.log.debug { 'Observer could not deliver to a closed client queue' }
|
|
812
1467
|
end
|
|
813
1468
|
|
|
814
|
-
def
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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 }
|
|
820
1491
|
tick_observers.each do |observer|
|
|
821
|
-
|
|
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
|
|
822
1502
|
end
|
|
823
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
|
|
824
1512
|
end
|
|
825
1513
|
end
|