Skip to content

Commit 485ee7b

Browse files
authored
RUBY-1563 Poll SRV records for mongos discovery (#1441)
* RUBY-1563 Implement polling SRV records for mongos * RUBY-1563 Poll SRV records for mongos discovery * Ensure host mismatch does not affect mongos status * RUBY-1563 verify last_scan start as nil * Omit rubydns on jruby since we do not use it * jruby hack * Stop creating srv monitor in topology instances * Try locking srv monitor writes for jruby
1 parent 90f09a8 commit 485ee7b

24 files changed

+1204
-19
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ end
2626
group :testing do
2727
gem 'timecop'
2828
gem 'ice_nine'
29+
gem 'rubydns', platforms: :mri
2930
gem 'rspec-retry'
3031
gem 'rspec-expectations', '~> 3.0'
3132
gem 'rspec-mocks-diag', '~> 3.0'

lib/mongo/client.rb

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class Client
7676
:read_concern,
7777
:read_retry_interval,
7878
:replica_set,
79+
:resolv_options,
7980
:retry_reads,
8081
:retry_writes,
8182
:scan,
@@ -365,6 +366,8 @@ def hash
365366
# Can be :w => Integer|String, :fsync => Boolean, :j => Boolean.
366367
# @option options [ Integer ] :zlib_compression_level The Zlib compression level to use, if using compression.
367368
# See Ruby's Zlib module for valid levels.
369+
# @option options [ Hash ] :resolv_options For internal driver use only.
370+
# Options to pass through to Resolv::DNS constructor for SRV lookups.
368371
#
369372
# @since 2.0.0
370373
def initialize(addresses_or_uri, options = nil)
@@ -374,8 +377,14 @@ def initialize(addresses_or_uri, options = nil)
374377
options = {}
375378
end
376379

380+
srv_uri = nil
377381
if addresses_or_uri.is_a?(::String)
378382
uri = URI.get(addresses_or_uri, options)
383+
if uri.is_a?(URI::SRVProtocol)
384+
# If the URI is an SRV URI, note this so that we can start
385+
# SRV polling if the topology is a sharded cluster.
386+
srv_uri = uri
387+
end
379388
addresses = uri.servers
380389
uri_options = uri.client_options.dup
381390
# Special handing for :write and :write_concern: allow client Ruby
@@ -386,8 +395,10 @@ def initialize(addresses_or_uri, options = nil)
386395
uri_options.delete(:write_concern)
387396
end
388397
options = uri_options.merge(options)
398+
@srv_records = uri.srv_records
389399
else
390400
addresses = addresses_or_uri
401+
@srv_records = nil
391402
end
392403

393404
unless options[:retry_reads] == false
@@ -423,7 +434,7 @@ def initialize(addresses_or_uri, options = nil)
423434
sdam_proc.call(self)
424435
end
425436

426-
@cluster = Cluster.new(addresses, @monitoring, cluster_options)
437+
@cluster = Cluster.new(addresses, @monitoring, cluster_options.merge(srv_uri: srv_uri))
427438

428439
# Unset monitoring, it will be taken out of cluster from now on
429440
remove_instance_variable('@monitoring')
@@ -447,7 +458,14 @@ def cluster_options
447458
# applications should read these values from client, not from cluster
448459
max_read_retries: options[:max_read_retries],
449460
read_retry_interval: options[:read_retry_interval],
450-
)
461+
).tap do |options|
462+
# If the client has a cluster already, forward srv_uri to the new
463+
# cluster to maintain SRV monitoring. If the client is brand new,
464+
# its constructor sets srv_uri manually.
465+
if cluster
466+
options.update(srv_uri: cluster.options[:srv_uri])
467+
end
468+
end
451469
end
452470

453471
# Get the maximum number of times the client can retry a read operation

lib/mongo/cluster.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ class Cluster
9595
# :cleanup automatically defaults to false as well.
9696
# @option options [ Float ] :heartbeat_frequency The interval, in seconds,
9797
# for the server monitor to refresh its description via ismaster.
98+
# @option options [ Hash ] :resolv_options For internal driver use only.
99+
# Options to pass through to Resolv::DNS constructor for SRV lookups.
98100
#
99101
# @since 2.0.0
100102
def initialize(seeds, monitoring, options = Options::Redacted.new)
@@ -116,6 +118,7 @@ def initialize(seeds, monitoring, options = Options::Redacted.new)
116118
@sdam_flow_lock = Mutex.new
117119
@cluster_time = nil
118120
@cluster_time_lock = Mutex.new
121+
@srv_monitor_lock = Mutex.new
119122
@server_selection_semaphore = Semaphore.new
120123
@topology = Topology.initial(self, monitoring, options)
121124
Session::SessionPool.create(self)
@@ -280,6 +283,9 @@ def self.create(client)
280283
end
281284
end
282285

286+
# @api private
287+
attr_reader :srv_monitor
288+
283289
# Get the maximum number of times the client can retry a read operation
284290
# when using legacy read retries.
285291
#
@@ -439,6 +445,11 @@ def disconnect!(wait=false)
439445
session_pool.end_sessions
440446
@periodic_executor.stop!
441447
end
448+
@srv_monitor_lock.synchronize do
449+
if @srv_monitor
450+
@srv_monitor.stop!
451+
end
452+
end
442453
@servers.each do |server|
443454
if server.connected?
444455
server.disconnect!(wait)
@@ -569,6 +580,38 @@ def run_sdam_flow(previous_desc, updated_desc, options = {})
569580
unless updated_desc.unknown?
570581
server_selection_semaphore.broadcast
571582
end
583+
584+
check_and_start_srv_monitor
585+
end
586+
587+
# Sets the list of servers to the addresses in the provided list of address
588+
# strings.
589+
#
590+
# This method is called by the SRV monitor after receiving new DNS records
591+
# for the monitored hostname.
592+
#
593+
# Removes servers in the cluster whose addresses are not in the passed
594+
# list of server addresses, and adds servers for any addresses in the
595+
# argument which are not already in the cluster.
596+
#
597+
# @param [ Array<String> ] server_address_strs List of server addresses
598+
# to sync the cluster servers to.
599+
#
600+
# @api private
601+
def set_server_list(server_address_strs)
602+
@sdam_flow_lock.synchronize do
603+
server_address_strs.each do |address_str|
604+
unless servers_list.any? { |server| server.address.seed == address_str }
605+
add(address_str)
606+
end
607+
end
608+
609+
servers_list.each do |server|
610+
unless server_address_strs.any? { |address_str| server.address.seed == address_str }
611+
remove(server.address.seed)
612+
end
613+
end
614+
end
572615
end
573616

574617
# Determine if this cluster of servers is equal to another object. Checks the
@@ -781,7 +824,25 @@ def sessions_supported?
781824
false
782825
end
783826
end
827+
828+
# @api private
829+
def check_and_start_srv_monitor
830+
return unless topology.is_a?(Topology::Sharded) && options[:srv_uri]
831+
@srv_monitor_lock.synchronize do
832+
unless @srv_monitor
833+
monitor_options = options.merge(
834+
timeout: options[:connect_timeout] || Server::CONNECT_TIMEOUT)
835+
@srv_monitor = _srv_monitor = SrvMonitor.new(self, monitor_options)
836+
finalizer = lambda do
837+
_srv_monitor.stop!
838+
end
839+
ObjectSpace.define_finalizer(self, finalizer)
840+
end
841+
@srv_monitor.run!
842+
end
843+
end
784844
end
785845
end
786846

787847
require 'mongo/cluster/sdam_flow'
848+
require 'mongo/cluster/srv_monitor'

lib/mongo/cluster/srv_monitor.rb

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Copyright (C) 2019 MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
module Mongo
16+
class Cluster
17+
18+
# Periodically retrieves SRV records for the cluster's SRV URI, and
19+
# sets the cluster's server list to the SRV lookup result.
20+
#
21+
# If an error is encountered during SRV lookup or an SRV record is invalid
22+
# or disallowed for security reasons, a warning is logged and monitoring
23+
# continues.
24+
#
25+
# @api private
26+
class SrvMonitor
27+
include Loggable
28+
include BackgroundThread
29+
30+
MIN_SCAN_INTERVAL = 60
31+
32+
DEFAULT_TIMEOUT = 10
33+
34+
# Creates the SRV monitor.
35+
#
36+
# @param [ Cluster ] cluster The cluster.
37+
# @param [ Hash ] options The cluster options.
38+
#
39+
# @option options [ Float ] :timeout The timeout to use for DNS lookups.
40+
# @option options [ URI::SRVProtocol ] :srv_uri The SRV URI to monitor.
41+
# @option options [ Hash ] :resolv_options For internal driver use only.
42+
# Options to pass through to Resolv::DNS constructor for SRV lookups.
43+
def initialize(cluster, options = nil)
44+
options = if options
45+
options.dup
46+
else
47+
{}
48+
end
49+
@cluster = cluster
50+
@resolver = Srv::Resolver.new(options)
51+
unless @srv_uri = options.delete(:srv_uri)
52+
raise ArgumentError, 'SRV URI is required'
53+
end
54+
@options = options.freeze
55+
@last_result = @srv_uri.srv_result
56+
@stop_semaphore = Semaphore.new
57+
end
58+
59+
attr_reader :options
60+
61+
attr_reader :cluster
62+
63+
# @return [ Srv::Result ] Last known SRV lookup result. Used for
64+
# determining intervals between SRV lookups, which depend on SRV DNS
65+
# records' TTL values.
66+
attr_reader :last_result
67+
68+
def start!
69+
super
70+
ObjectSpace.define_finalizer(self, self.class.finalize(@thread))
71+
end
72+
73+
private
74+
75+
def do_work
76+
scan!
77+
@stop_semaphore.wait(scan_interval)
78+
end
79+
80+
def scan!
81+
old_hosts = last_result.address_strs
82+
83+
begin
84+
last_result = Timeout.timeout(timeout) do
85+
@resolver.get_records(@srv_uri.query_hostname)
86+
end
87+
rescue Resolv::ResolvTimeout => e
88+
log_warn("SRV monitor: timed out trying to resolve hostname #{@srv_uri.query_hostname}: #{e.class}: #{e}")
89+
return
90+
rescue Timeout::Error
91+
log_warn("SRV monitor: timed out trying to resolve hostname #{@srv_uri.query_hostname} (timeout=#{timeout})")
92+
return
93+
rescue Resolv::ResolvError => e
94+
log_warn("SRV monitor: unable to resolve hostname #{@srv_uri.query_hostname}: #{e.class}: #{e}")
95+
return
96+
end
97+
98+
if last_result.empty?
99+
log_warn("SRV monitor: hostname #{@srv_uri.query_hostname} resolved to zero records")
100+
return
101+
end
102+
103+
@cluster.set_server_list(last_result.address_strs)
104+
end
105+
106+
def self.finalize(thread)
107+
Proc.new do
108+
thread.kill
109+
end
110+
end
111+
112+
def scan_interval
113+
if last_result.empty?
114+
[cluster.heartbeat_interval, MIN_SCAN_INTERVAL].min
115+
elsif last_result.min_ttl.nil?
116+
MIN_SCAN_INTERVAL
117+
else
118+
[last_result.min_ttl, MIN_SCAN_INTERVAL].max
119+
end
120+
end
121+
122+
def timeout
123+
options[:timeout] || DEFAULT_TIMEOUT
124+
end
125+
end
126+
end
127+
end

lib/mongo/server/description.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,7 @@ def wire_versions
619619
# @return [ true, false ] If the description is from the server.
620620
#
621621
# @since 2.0.6
622+
# @deprecated
622623
def is_server?(server)
623624
address == server.address
624625
end
@@ -632,6 +633,7 @@ def is_server?(server)
632633
# of servers.
633634
#
634635
# @since 2.0.6
636+
# @deprecated
635637
def lists_server?(server)
636638
servers.include?(server.address.to_s)
637639
end

lib/mongo/srv.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414

1515
require 'mongo/srv/result'
1616
require 'mongo/srv/resolver'
17+
require 'mongo/srv/monitor'

0 commit comments

Comments
 (0)