Skip to content

Commit 0a5221c

Browse files
RUBY-3838 Fix raise_on_invalid handling and protect SRV monitor thread
1 parent f5cd7e5 commit 0a5221c

5 files changed

Lines changed: 129 additions & 6 deletions

File tree

lib/mongo/srv/monitor.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ def initialize(cluster, **opts)
4545
raise ArgumentError, 'SRV URI is required' unless @srv_uri = opts.delete(:srv_uri)
4646

4747
@options = opts.freeze
48-
@resolver = Srv::Resolver.new(**opts)
48+
# Per the polling-srv-records spec, mismatched-domain records and empty
49+
# results MUST be logged and ignored, not raised. Force the polling
50+
# resolver into log-and-continue mode regardless of caller options.
51+
@resolver = Srv::Resolver.new(**opts, raise_on_invalid: false)
4952
@last_result = @srv_uri.srv_result
5053
@stop_semaphore = Semaphore.new
5154
end
@@ -82,6 +85,12 @@ def scan!
8285
rescue Resolv::ResolvError => e
8386
log_warn("SRV monitor: unable to resolve hostname #{@srv_uri.query_hostname}: #{e.class}: #{e}")
8487
return
88+
rescue Mongo::Error => e
89+
# Defense in depth: the polling resolver is configured to log-and-continue
90+
# for mismatched-domain and empty-result cases, but if any Mongo::Error
91+
# leaks out we MUST NOT let it terminate the SRV monitor thread.
92+
log_warn("SRV monitor: error resolving hostname #{@srv_uri.query_hostname}: #{e.class}: #{e}")
93+
return
8594
end
8695

8796
if last_result.empty?

lib/mongo/srv/resolver.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def get_txt_options_string(hostname)
142142
#
143143
# @return [ Boolean ] Whether an error should be raised.
144144
def raise_on_invalid?
145-
@raise_on_invalid ||= @options[:raise_on_invalid] || true
145+
@options.fetch(:raise_on_invalid, true)
146146
end
147147
end
148148
end

lib/mongo/uri/srv_protocol.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,14 @@ def raise_invalid_error!(details)
117117
raise Error::InvalidURI.new(@string, details, FORMAT)
118118
end
119119

120-
# Gets the SRV resolver.
121-
# If domain verification fails or no SRV records are found,
122-
# an error must not be raised per the spec; instead, a warning is logged.
120+
# Gets the SRV resolver used for initial URI parsing.
121+
# Per the Initial DNS Seedlist Discovery spec, the driver MUST raise an
122+
# error if domain verification fails or no SRV records are found, so
123+
# raise_on_invalid is left at its default of true.
123124
#
124125
# @return [ Mongo::Srv::Resolver ]
125126
def resolver
126127
@resolver ||= Srv::Resolver.new(
127-
raise_on_invalid: false,
128128
resolv_options: options[:resolv_options],
129129
timeout: options[:connect_timeout]
130130
)

spec/mongo/srv/monitor_spec.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,32 @@
229229
expect(cluster.addresses.map(&:to_s).sort).to eq(hosts.sort)
230230
end
231231
end
232+
233+
context 'when the resolver raises Mongo::Error::MismatchedDomain' do
234+
let(:resolver) do
235+
double('resolver').tap do |resolver|
236+
allow(resolver).to receive(:get_records)
237+
.and_raise(Mongo::Error::MismatchedDomain, 'simulated mismatched domain')
238+
end
239+
end
240+
241+
it 'does not propagate the error and leaves the topology unchanged' do
242+
expect(cluster.addresses.map(&:to_s).sort).to eq(hosts.sort)
243+
end
244+
end
245+
246+
context 'when the resolver raises Mongo::Error::NoSRVRecords' do
247+
let(:resolver) do
248+
double('resolver').tap do |resolver|
249+
allow(resolver).to receive(:get_records)
250+
.and_raise(Mongo::Error::NoSRVRecords, 'simulated no records')
251+
end
252+
end
253+
254+
it 'does not propagate the error and leaves the topology unchanged' do
255+
expect(cluster.addresses.map(&:to_s).sort).to eq(hosts.sort)
256+
end
257+
end
232258
end
233259

234260
context 'when srv_max_hosts is specified' do

spec/mongo/srv/resolver_spec.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
3+
require 'lite_spec_helper'
4+
5+
describe Mongo::Srv::Resolver do
6+
describe '#get_records' do
7+
let(:hostname) { 'foo.example.com' }
8+
let(:srv_query) { '_mongodb._tcp.' + hostname }
9+
10+
let(:mismatched_record) do
11+
double('record').tap do |record|
12+
allow(record).to receive(:target).and_return('evil.attacker.tld')
13+
allow(record).to receive(:port).and_return(27_017)
14+
allow(record).to receive(:ttl).and_return(1)
15+
end
16+
end
17+
18+
let(:dns_resolver) do
19+
instance_double(Resolv::DNS).tap do |dns|
20+
allow(dns).to receive(:timeouts=)
21+
end
22+
end
23+
24+
before do
25+
allow(Resolv::DNS).to receive(:new).and_return(dns_resolver)
26+
end
27+
28+
context 'when raise_on_invalid is false and a mismatched-domain record is returned' do
29+
let(:resolver) { described_class.new(raise_on_invalid: false) }
30+
31+
before do
32+
allow(dns_resolver).to receive(:getresources)
33+
.with(srv_query, Resolv::DNS::Resource::IN::SRV)
34+
.and_return([ mismatched_record ])
35+
end
36+
37+
it 'does not raise and logs a warning' do
38+
expect(resolver).to receive(:log_warn).at_least(:once)
39+
expect { resolver.get_records(hostname) }.not_to raise_error
40+
end
41+
end
42+
43+
context 'when raise_on_invalid is false and no records are returned' do
44+
let(:resolver) { described_class.new(raise_on_invalid: false) }
45+
46+
before do
47+
allow(dns_resolver).to receive(:getresources)
48+
.with(srv_query, Resolv::DNS::Resource::IN::SRV)
49+
.and_return([])
50+
end
51+
52+
it 'does not raise NoSRVRecords' do
53+
allow(resolver).to receive(:log_warn)
54+
expect { resolver.get_records(hostname) }.not_to raise_error
55+
end
56+
end
57+
58+
context 'when raise_on_invalid is true (default) and a mismatched-domain record is returned' do
59+
let(:resolver) { described_class.new }
60+
61+
before do
62+
allow(dns_resolver).to receive(:getresources)
63+
.with(srv_query, Resolv::DNS::Resource::IN::SRV)
64+
.and_return([ mismatched_record ])
65+
end
66+
67+
it 'raises MismatchedDomain' do
68+
expect { resolver.get_records(hostname) }
69+
.to raise_error(Mongo::Error::MismatchedDomain)
70+
end
71+
end
72+
73+
context 'when raise_on_invalid is true (default) and no records are returned' do
74+
let(:resolver) { described_class.new }
75+
76+
before do
77+
allow(dns_resolver).to receive(:getresources)
78+
.with(srv_query, Resolv::DNS::Resource::IN::SRV)
79+
.and_return([])
80+
end
81+
82+
it 'raises NoSRVRecords' do
83+
expect { resolver.get_records(hostname) }
84+
.to raise_error(Mongo::Error::NoSRVRecords)
85+
end
86+
end
87+
end
88+
end

0 commit comments

Comments
 (0)