Skip to content

Commit 26275b4

Browse files
p-mongop
authored andcommitted
RUBY-2219 Force read preference primaryPreferred in Single topology (#1868)
Co-authored-by: Oleg Pudeyev <oleg@bsdpower.com>
1 parent 9fbcfb2 commit 26275b4

File tree

8 files changed

+710
-275
lines changed

8 files changed

+710
-275
lines changed

docs/tutorials/ruby-driver-crud-operations.txt

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,9 @@ Read Preference
292292
~~~~~~~~~~~~~~~
293293

294294
Read preference determines the candidate :manual:`replica set</replication/>`
295-
members to which a query or command can be sent. They consist of a **mode** specified as
296-
a symbol, an array of hashes known as **tag_sets**, and two timing options:
297-
**local_threshold** and **server_selection_timeout**.
295+
members to which a query or command can be sent. They consist of a **mode**
296+
specified as a symbol, an array of hashes known as **tag_sets**, and two
297+
timing options: **local_threshold** and **server_selection_timeout**.
298298

299299
``local_threshold``
300300
Defines the upper limit in seconds of the latency window
@@ -305,6 +305,12 @@ a symbol, an array of hashes known as **tag_sets**, and two timing options:
305305
Defines how long to block for server selection
306306
before throwing an exception. The default is 30,000 milliseconds, or 30 seconds.
307307

308+
.. note::
309+
310+
Read preference does not apply to Standalone deployments. When a client
311+
is connected to a Standalone deployment, any application-specified read
312+
preference is ignored.
313+
308314
For more information on the algorithm used to select a server, please
309315
refer to the `Server Selection documentation, available on GitHub
310316
<https://github.com/mongodb/specifications/blob/master/source/server-sel
@@ -337,17 +343,29 @@ using the ``with`` method:
337343
Mode
338344
~~~~
339345

340-
There are five possible read preference modes. They are ``:primary``,
341-
``:secondary``, ``:primary_preferred``, ``:secondary_preferred``,
342-
``:nearest``. Please see the :manual:`read preference documentation in the MongoDB Manual
343-
</core/read-preference/>` for an explanation of the modes and tag sets.
346+
There are five possible read preference modes: ``:primary``, ``:secondary``,
347+
``:primary_preferred``, ``:secondary_preferred`` and``:nearest``.
348+
Please see the :manual:`read preference documentation in the MongoDB Manual
349+
</core/read-preference/>` for an explanation of the modes.
350+
351+
.. note::
352+
353+
When a client is directly connected to a server using the ``:direct_connection``
354+
Ruby option or the ``directConnection`` URI option, read preference mode
355+
is automatically set to ``:primary_preferred`` to permit read operations
356+
against secondaries. If the application specified a ``:primary`` read
357+
preference mode, the mode is automatically converted to ``:primary_preferred``.
358+
If another read preference mode is specified, it is passed to the server
359+
unchanged.
344360

345361
Tag sets
346362
~~~~~~~~
347363

348364
The ``tag_sets`` parameter is an ordered list of tag sets used to
349365
restrict the eligibility of servers for selection, such as for data
350-
center awareness.
366+
center awareness. Please see the :manual:`read preference documentation in
367+
the MongoDB Manual </core/read-preference/>` for an explanation of tag sets.
368+
351369

352370
A read preference tag set (T) matches a server tag set (S) – or
353371
equivalently a server tag set (S) matches a read preference tag set

lib/mongo/operation/shared/read_preference_supported.rb

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,83 @@
1515
module Mongo
1616
module Operation
1717

18-
# Shared behavior of operations that support read preference.
18+
# Read preference handling for pre-OP_MSG operation implementations.
19+
#
20+
# This module is not used by OP_MSG operation classes (those deriving
21+
# from OpMsgBase). Instead, read preference for those classes is handled
22+
# in SessionsSupported module.
1923
#
2024
# @since 2.5.2
25+
# @api private
2126
module ReadPreferenceSupported
2227

2328
private
2429

25-
SLAVE_OK = :slave_ok
26-
30+
# Get the options for executing the operation on a particular server.
31+
#
32+
# @param [ Server ] server The server that the operation will be
33+
# executed on.
34+
#
35+
# @return [ Hash ] The options.
36+
#
37+
# @since 2.0.0
2738
def options(server)
28-
update_options_for_slave_ok(super, server)
39+
add_slave_ok_flag_maybe(super, server)
40+
end
41+
42+
# Adds :slave_ok flag to options based on the read preference specified
43+
# in the operation or implied by the topology that the server is a
44+
# part of.
45+
#
46+
# @param [ Hash ] options The options calculated so far.
47+
#
48+
# @return [ Hash ] The new options.
49+
def add_slave_ok_flag_maybe(options, server)
50+
add_flag =
51+
# https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.rst#topology-type-single
52+
if server.standalone?
53+
# Read preference is never sent to standalones.
54+
false
55+
elsif server.cluster.single?
56+
# In Single topology the driver forces primaryPreferred read
57+
# preference mode (via the slave_ok flag, in case of old servers)
58+
# so that the query is satisfied.
59+
true
60+
else
61+
# In replica sets and sharded clusters, read preference is passed
62+
# to the server if one is specified by the application, and there
63+
# is no default.
64+
read && read.slave_ok?
65+
end
66+
67+
if add_flag
68+
options= options.dup
69+
(options[:flags] ||= []) << :slave_ok
70+
end
71+
72+
options
2973
end
3074

75+
def command(server)
76+
sel = super
77+
update_selector_for_read_pref(sel, server)
78+
end
79+
80+
# Adds $readPreference field to the command document.
81+
#
82+
# $readPreference is only sent when the server is a mongos,
83+
# following the rules described in
84+
# https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.rst#passing-read-preference-to-mongos.
85+
# The topology does not matter for figuring out whether to send
86+
# $readPreference since the decision is always made based on
87+
# server type.
88+
#
89+
# $readPreference is not sent to pre-OP_MSG replica set members.
90+
#
91+
# @param [ Hash ] sel Existing command document.
92+
# @param [ Server ] server The server that the command is to be sent to.
93+
#
94+
# @return [ Hash ] New command document to send to the server.
3195
def update_selector_for_read_pref(sel, server)
3296
if read && server.mongos? && read_pref = read.to_mongos
3397
Mongo::Lint.validate_camel_case_read_preference(read_pref)
@@ -37,21 +101,6 @@ def update_selector_for_read_pref(sel, server)
37101
sel
38102
end
39103
end
40-
41-
def update_options_for_slave_ok(opts, server)
42-
if (server.cluster.single? && !server.mongos?) || (read && read.slave_ok?)
43-
opts.dup.tap do |o|
44-
(o[:flags] ||= []) << SLAVE_OK
45-
end
46-
else
47-
opts
48-
end
49-
end
50-
51-
def command(server)
52-
sel = super
53-
update_selector_for_read_pref(sel, server)
54-
end
55104
end
56105
end
57106
end

lib/mongo/operation/shared/sessions_supported.rb

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,8 @@ def command(server)
123123
sel = selector(server).dup
124124
add_write_concern!(sel)
125125
sel[Protocol::Msg::DATABASE_IDENTIFIER] = db_name
126-
unless server.standalone?
127-
sel['$readPreference'] = read.to_doc if read
128-
end
126+
127+
add_read_preference(sel, server)
129128

130129
if server.features.sessions_enabled?
131130
apply_cluster_time!(sel, server)
@@ -139,6 +138,48 @@ def command(server)
139138
sel
140139
end
141140

141+
# Adds $readPreference field to the command document.
142+
#
143+
# $readPreference is only sent when the server is a mongos,
144+
# following the rules described in
145+
# https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.rst#passing-read-preference-to-mongos.
146+
# The topology does not matter for figuring out whether to send
147+
# $readPreference since the decision is always made based on
148+
# server type.
149+
#
150+
# $readPreference is sent to OP_MSG-grokking replica set members.
151+
#
152+
# @param [ Hash ] sel Existing command document which will be mutated.
153+
# @param [ Server ] server The server that the command is to be sent to.
154+
def add_read_preference(sel, server)
155+
# https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.rst#topology-type-single
156+
if server.standalone?
157+
# Read preference is never sent to standalones.
158+
elsif server.cluster.single?
159+
# In Single topology:
160+
# - If no read preference is specified by the application, the driver
161+
# adds mode: primaryPreferred.
162+
# - If a read preference is specified by the application, the driver
163+
# replaces the mode with primaryPreferred.
164+
read_doc = if read
165+
BSON::Document.new(read.to_doc)
166+
else
167+
BSON::Document.new
168+
end
169+
if [nil, 'primary'].include?(read_doc['mode'])
170+
read_doc['mode'] = 'primaryPreferred'
171+
end
172+
sel['$readPreference'] = read_doc
173+
else
174+
# In replica sets and sharded clusters, read preference is passed
175+
# to the server if one is specified by the application, and there
176+
# is no default.
177+
if read
178+
sel['$readPreference'] = read.to_doc
179+
end
180+
end
181+
end
182+
142183
def apply_session_options(sel, server)
143184
apply_cluster_time!(sel, server)
144185
sel[:txnNumber] = BSON::Int64.new(txn_num) if txn_num

spec/integration/read_preference_spec.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,4 +482,30 @@
482482
end
483483
end
484484
end
485+
486+
context 'secondary read with direct connection' do
487+
require_topology :replica_set
488+
489+
let(:address_str) do
490+
Mongo::ServerSelector.get(mode: :secondary).
491+
select_server(authorized_client.cluster).address.seed
492+
end
493+
494+
let(:secondary_client) do
495+
new_local_client([address_str],
496+
SpecConfig.instance.all_test_options.merge(connect: :direct))
497+
end
498+
499+
it 'succeeds without read preference' do
500+
secondary_client['foo'].find.to_a
501+
end
502+
503+
it 'succeeds with read preference: secondary' do
504+
secondary_client['foo', {read: {mode: :secondary}}].find.to_a
505+
end
506+
507+
it 'succeeds with read preference: primary' do
508+
secondary_client['foo', {read: {mode: :primary}}].find.to_a
509+
end
510+
end
485511
end

spec/mongo/operation/find/legacy_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
let(:secondary_server_single) do
7272
double('secondary_server').tap do |server|
7373
allow(server).to receive(:mongos?) { false }
74+
allow(server).to receive(:standalone?) { false }
7475
allow(server).to receive(:cluster) { cluster_single }
7576
allow(server).to receive(:features) { authorized_primary.features }
7677
end

0 commit comments

Comments
 (0)