mongo 1.11.1 → 1.12.0.rc0

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/VERSION +1 -1
  5. data/lib/mongo/collection.rb +8 -3
  6. data/lib/mongo/collection_writer.rb +1 -1
  7. data/lib/mongo/connection/socket/unix_socket.rb +1 -1
  8. data/lib/mongo/cursor.rb +8 -1
  9. data/lib/mongo/db.rb +61 -33
  10. data/lib/mongo/exception.rb +57 -0
  11. data/lib/mongo/functional.rb +1 -0
  12. data/lib/mongo/functional/authentication.rb +138 -11
  13. data/lib/mongo/functional/read_preference.rb +31 -22
  14. data/lib/mongo/functional/scram.rb +555 -0
  15. data/lib/mongo/functional/uri_parser.rb +107 -79
  16. data/lib/mongo/mongo_client.rb +19 -24
  17. data/lib/mongo/mongo_replica_set_client.rb +2 -1
  18. data/test/functional/authentication_test.rb +3 -0
  19. data/test/functional/client_test.rb +33 -0
  20. data/test/functional/collection_test.rb +29 -19
  21. data/test/functional/db_api_test.rb +16 -1
  22. data/test/functional/pool_test.rb +7 -6
  23. data/test/functional/uri_test.rb +111 -7
  24. data/test/helpers/test_unit.rb +17 -3
  25. data/test/replica_set/client_test.rb +31 -0
  26. data/test/replica_set/insert_test.rb +49 -32
  27. data/test/replica_set/pinning_test.rb +50 -0
  28. data/test/replica_set/query_test.rb +1 -1
  29. data/test/replica_set/replication_ack_test.rb +3 -3
  30. data/test/shared/authentication/basic_auth_shared.rb +14 -1
  31. data/test/shared/authentication/gssapi_shared.rb +13 -8
  32. data/test/shared/authentication/scram_shared.rb +92 -0
  33. data/test/tools/mongo_config.rb +18 -6
  34. data/test/unit/client_test.rb +40 -6
  35. data/test/unit/connection_test.rb +15 -5
  36. data/test/unit/db_test.rb +1 -1
  37. data/test/unit/read_pref_test.rb +291 -0
  38. metadata +9 -6
  39. metadata.gz.sig +0 -0
@@ -25,7 +25,7 @@ class ReplicaSetQueryTest < Test::Unit::TestCase
25
25
  end
26
26
 
27
27
  def teardown
28
- @client.close if @conn
28
+ @client.close if @client
29
29
  end
30
30
 
31
31
  def test_query
@@ -37,13 +37,13 @@ class ReplicaSetAckTest < Test::Unit::TestCase
37
37
  end
38
38
 
39
39
  def test_safe_mode_with_w_failure
40
- assert_raise_error WriteConcernError, "time" do
40
+ assert_raise_error WriteConcernError do
41
41
  @col.insert({:foo => 1}, :w => 4, :wtimeout => 1, :fsync => true)
42
42
  end
43
- assert_raise_error WriteConcernError, "time" do
43
+ assert_raise_error WriteConcernError do
44
44
  @col.update({:foo => 1}, {:foo => 2}, :w => 4, :wtimeout => 1, :fsync => true)
45
45
  end
46
- assert_raise_error WriteConcernError, "time" do
46
+ assert_raise_error WriteConcernError do
47
47
  @col.remove({:foo => 2}, :w => 4, :wtimeout => 1, :fsync => true)
48
48
  end
49
49
  if @client.server_version >= '2.5.4'
@@ -23,7 +23,10 @@ module BasicAuthTests
23
23
  end
24
24
 
25
25
  def teardown
26
- @client[TEST_DB].authenticate(TEST_USER, TEST_USER_PWD) unless has_auth?(TEST_DB, TEST_USER)
26
+ return unless @client && @db
27
+ unless has_auth?(TEST_DB, TEST_USER)
28
+ @client[TEST_DB].authenticate(TEST_USER, TEST_USER_PWD)
29
+ end
27
30
 
28
31
  if @client.server_version < '2.5'
29
32
  @db['system.users'].remove
@@ -42,6 +45,16 @@ module BasicAuthTests
42
45
  @client.auths.any? { |a| a[:source] == db_name && a[:username] == username }
43
46
  end
44
47
 
48
+ def test_descriptive_mech_error
49
+ assert_raise_error Mongo::MongoArgumentError, Mongo::Authentication::MECHANISM_ERROR do
50
+ @db.authenticate('emily', nil, nil, nil, 'FAKE_MECHANISM')
51
+ end
52
+ assert_raise_error Mongo::MongoArgumentError, Mongo::Authentication::MECHANISM_ERROR do
53
+ uri = "mongodb://user:pwd@host:port/example?authSource=$external&authMechanism=FAKE_MECHANISM"
54
+ Mongo::MongoClient.from_uri(uri)
55
+ end
56
+ end
57
+
45
58
  def test_add_remove_user
46
59
  init_auth_basic
47
60
 
@@ -75,7 +75,7 @@ module GSSAPITests
75
75
  end
76
76
 
77
77
  def test_wrong_service_name_fails
78
- extra_opts = { :gssapi_service_name => 'example' }
78
+ extra_opts = { :service_name => 'example' }
79
79
  client = Mongo::MongoClient.new(MONGODB_GSSAPI_HOST, MONGODB_GSSAPI_PORT)
80
80
  if client['admin'].command(:isMaster => 1)['setName']
81
81
  client = Mongo::MongoReplicaSetClient.new(["#{MONGODB_GSSAPI_HOST}:#{MONGODB_GSSAPI_PORT}"])
@@ -93,7 +93,7 @@ module GSSAPITests
93
93
  require 'cgi'
94
94
  username = CGI::escape(ENV['MONGODB_GSSAPI_USER'])
95
95
  uri = "mongodb://#{username}@#{ENV['MONGODB_GSSAPI_HOST']}:#{ENV['MONGODB_GSSAPI_PORT']}/?" +
96
- "authMechanism=GSSAPI&gssapiServiceName=example"
96
+ "authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:example"
97
97
  client = @client.class.from_uri(uri)
98
98
  assert_raise_error Mongo::AuthenticationError do
99
99
  client[MONGODB_GSSAPI_DB].command(:dbstats => 1)
@@ -101,32 +101,37 @@ module GSSAPITests
101
101
  end
102
102
 
103
103
  def test_extra_opts
104
- extra_opts = { :gssapi_service_name => 'example', :canonicalize_host_name => true }
105
- client = Mongo::MongoClient.new(MONGODB_GSSAPI_HOST, MONGODB_GSSAPI_PORT)
104
+ extra_opts = { :service_name => 'example', :canonicalize_host_name => true,
105
+ :service_realm => 'dumdum' }
106
+ client = Mongo::MongoClient.new(TEST_HOST, MONGODB_GSSAPI_PORT)
106
107
  set_system_properties
107
108
 
108
109
  Mongo::Sasl::GSSAPI.expects(:authenticate).with do |username, client, socket, opts|
109
- assert_equal opts[:gssapi_service_name], extra_opts[:gssapi_service_name]
110
+ assert_equal opts[:service_name], extra_opts[:service_name]
110
111
  assert_equal opts[:canonicalize_host_name], extra_opts[:canonicalize_host_name]
112
+ assert_equal opts[:service_realm], extra_opts[:service_realm]
111
113
  [ username, client, socket, opts ]
112
114
  end.returns('ok' => true )
113
115
  client[MONGODB_GSSAPI_DB].authenticate(MONGODB_GSSAPI_USER, nil, nil, nil, 'GSSAPI', extra_opts)
114
116
  end
115
117
 
116
118
  def test_extra_opts_uri
117
- extra_opts = { :gssapi_service_name => 'example', :canonicalize_host_name => true }
119
+ extra_opts = { :service_name => 'example', :canonicalize_host_name => true,
120
+ :service_realm => 'dumdum' }
118
121
  set_system_properties
119
122
 
120
123
  Mongo::Sasl::GSSAPI.expects(:authenticate).with do |username, client, socket, opts|
121
- assert_equal opts[:gssapi_service_name], extra_opts[:gssapi_service_name]
124
+ assert_equal opts[:service_name], extra_opts[:service_name]
122
125
  assert_equal opts[:canonicalize_host_name], extra_opts[:canonicalize_host_name]
126
+ assert_equal opts[:service_realm], extra_opts[:service_realm]
123
127
  [ username, client, socket, opts ]
124
128
  end.returns('ok' => true)
125
129
 
126
130
  require 'cgi'
127
131
  username = CGI::escape(ENV['MONGODB_GSSAPI_USER'])
128
132
  uri = "mongodb://#{username}@#{ENV['MONGODB_GSSAPI_HOST']}:#{ENV['MONGODB_GSSAPI_PORT']}/?" +
129
- "authMechanism=GSSAPI&gssapiServiceName=example&canonicalizeHostName=true"
133
+ "authMechanism=GSSAPI&authmechanismproperties=SERVICE_NAME:example," +
134
+ "CANONICALIZE_HOST_NAME:true,SERVICE_REALM:dumdum"
130
135
  client = @client.class.from_uri(uri)
131
136
  client.expects(:receive_message).returns([[{ 'ok' => 1 }], 1, 1])
132
137
  client[MONGODB_GSSAPI_DB].command(:dbstats => 1)
@@ -0,0 +1,92 @@
1
+ # Copyright (C) 2014 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 SCRAMTests
16
+
17
+ def setup_conversation
18
+ SecureRandom.expects(:base64).returns('NDA2NzU3MDY3MDYwMTgy')
19
+ @password = Digest::MD5.hexdigest("user:mongo:pencil")
20
+ @scram = Mongo::Authentication::SCRAM.new({ :username => 'user' }, @password)
21
+ end
22
+
23
+ def test_scram_authenticate
24
+ if @version.to_s > '2.7'
25
+ @client.clear_auths
26
+ assert @db.authenticate(TEST_USER, TEST_USER_PWD, nil, 'admin', 'SCRAM-SHA-1')
27
+ end
28
+ end
29
+
30
+ def test_scram_conversation_start
31
+ setup_conversation
32
+ command = @scram.start
33
+ assert_equal 1, command['saslStart']
34
+ assert_equal 'SCRAM-SHA-1', command['mechanism']
35
+ assert_equal 'n,,n=user,r=NDA2NzU3MDY3MDYwMTgy', command['payload'].to_s
36
+ end
37
+
38
+ def test_scram_conversation_continue
39
+ setup_conversation
40
+ payload = BSON::Binary.new(
41
+ 'r=NDA2NzU3MDY3MDYwMTgyt7/+IWaw1HaZZ5NmPJUTWapLpH2Gg+d8,s=AVvQXzAbxweH2RYDICaplw==,i=10000'
42
+ )
43
+ reply = { 'conversationId' => 1, 'done' => false, 'payload' => payload, 'ok' => 1.0 }
44
+ command = @scram.continue(reply)
45
+ assert_equal 1, command['saslContinue']
46
+ assert_equal 1, command['conversationId']
47
+ assert_equal(
48
+ 'c=biws,r=NDA2NzU3MDY3MDYwMTgyt7/+IWaw1HaZZ5NmPJUTWapLpH2Gg+d8,p=qYUYNy6SQ9Jucq9rFA9nVgXQdbM=',
49
+ command['payload'].to_s
50
+ )
51
+ end
52
+
53
+ def test_scram_conversation_continue_with_invalid_nonce
54
+ setup_conversation
55
+ payload = BSON::Binary.new(
56
+ 'r=NDA2NzU4MDY3MDYwMTgyt7/+IWaw1HaZZ5NmPJUTWapLpH2Gg+d8,s=AVvQXzAbxweH2RYDICaplw==,i=10000'
57
+ )
58
+ reply = { 'conversationId' => 1, 'done' => false, 'payload' => payload, 'ok' => 1.0 }
59
+ assert_raise_error Mongo::InvalidNonce do
60
+ @scram.continue(reply)
61
+ end
62
+ end
63
+
64
+ def test_scram_conversation_finalize
65
+ setup_conversation
66
+ continue_payload = BSON::Binary.new(
67
+ 'r=NDA2NzU3MDY3MDYwMTgyt7/+IWaw1HaZZ5NmPJUTWapLpH2Gg+d8,s=AVvQXzAbxweH2RYDICaplw==,i=10000'
68
+ )
69
+ continue_reply = { 'conversationId' => 1, 'done' => false, 'payload' => continue_payload, 'ok' => 1.0 }
70
+ @scram.continue(continue_reply)
71
+ payload = BSON::Binary.new('v=gwo9E8+uifshm7ixj441GvIfuUY=')
72
+ reply = { 'conversationId' => 1, 'done' => false, 'payload' => payload, 'ok' => 1.0 }
73
+ command = @scram.finalize(reply)
74
+ assert_equal 1, command['saslContinue']
75
+ assert_equal 1, command['conversationId']
76
+ assert_equal '', command['payload'].to_s
77
+ end
78
+
79
+ def test_scram_conversation_finalize_with_invalid_server_signature
80
+ setup_conversation
81
+ continue_payload = BSON::Binary.new(
82
+ 'r=NDA2NzU3MDY3MDYwMTgyt7/+IWaw1HaZZ5NmPJUTWapLpH2Gg+d8,s=AVvQXzAbxweH2RYDICaplw==,i=10000'
83
+ )
84
+ continue_reply = { 'conversationId' => 1, 'done' => false, 'payload' => continue_payload, 'ok' => 1.0 }
85
+ @scram.continue(continue_reply)
86
+ payload = BSON::Binary.new('v=LQ+8yhQeVL2a3Dh+TDJ7xHz4Srk=')
87
+ reply = { 'conversationId' => 1, 'done' => false, 'payload' => payload, 'ok' => 1.0 }
88
+ assert_raise_error Mongo::InvalidSignature do
89
+ @scram.finalize(reply)
90
+ end
91
+ end
92
+ end
@@ -121,6 +121,7 @@ module Mongo
121
121
  fast_sync = opts[:fastsync] || false
122
122
  auth = opts[:auth] || true
123
123
  ipv6 = opts[:ipv6].nil? ? true : opts[:ipv6]
124
+ setParameter = opts[:setParameter] || 'enableTestCommands=1'
124
125
 
125
126
  params.merge(:command => mongod,
126
127
  :dbpath => path,
@@ -129,7 +130,8 @@ module Mongo
129
130
  :quiet => quiet,
130
131
  :fastsync => fast_sync,
131
132
  :auth => auth,
132
- :ipv6 => ipv6)
133
+ :ipv6 => ipv6,
134
+ :setParameter => setParameter)
133
135
  end
134
136
 
135
137
  def self.key_file(opts)
@@ -301,6 +303,11 @@ module Mongo
301
303
 
302
304
  def initialize(config)
303
305
  @config = config
306
+ cmd = init_config!
307
+ super(cmd, @config[:host], @config[:port])
308
+ end
309
+
310
+ def init_config!
304
311
  dbpath = @config[:dbpath]
305
312
  [dbpath, File.dirname(@config[:logpath])].compact.each{|dir| FileUtils.mkdir_p(dir) unless File.directory?(dir) }
306
313
  command = @config[:command] || 'mongod'
@@ -314,7 +321,6 @@ module Mongo
314
321
  end
315
322
  end
316
323
  cmd = [command, arguments].flatten.compact
317
- super(cmd, @config[:host], @config[:port])
318
324
  end
319
325
 
320
326
  def start(verifies = DEFAULT_VERIFIES)
@@ -334,8 +340,13 @@ module Mongo
334
340
  sleep 1
335
341
  end
336
342
  end
337
- system "ps -fp #{@pid}; cat #{@config[:logpath]}"
338
- raise Mongo::ConnectionFailure, "DbServer.start verify via connection probe failed - port:#{@port.inspect} @pid:#{@pid.inspect} kill:#{Process.kill(0, @pid).inspect} running?:#{running?.inspect} cmd:#{cmd.inspect}"
343
+ if @config.delete(:setParameter)
344
+ @cmd = init_config!
345
+ start(verifies)
346
+ else
347
+ system "ps -fp #{@pid}; cat #{@config[:logpath]}"
348
+ raise Mongo::ConnectionFailure, "DbServer.start verify via connection probe failed - port:#{@port.inspect} @pid:#{@pid.inspect} kill:#{Process.kill(0, @pid).inspect} running?:#{running?.inspect} cmd:#{cmd.inspect}"
349
+ end
339
350
  end
340
351
 
341
352
  end
@@ -346,7 +357,7 @@ module Mongo
346
357
  @config = config
347
358
  @servers = {}
348
359
  Mongo::Config::CLUSTER_OPT_KEYS.each do |key|
349
- @servers[key] = @config[key].collect{|conf| DbServer.new(conf)} if @config[key]
360
+ @servers[key] = @config[key].collect{|conf| p conf; DbServer.new(conf)} if @config[key]
350
361
  end
351
362
  end
352
363
 
@@ -646,7 +657,8 @@ module Mongo
646
657
  servers.each{|server| server.start}
647
658
  # TODO - sharded replica sets - pending
648
659
  if @config[:replicas]
649
- repl_set_initiate if repl_set_get_status.first['startupStatus'] == 3
660
+ repl_set_initiate if repl_set_get_status.first['code'] == 94 ||
661
+ (repl_set_get_status.first['startupStatus'] && repl_set_get_status.first['startupStatus'] == 3)
650
662
  repl_set_startup
651
663
  end
652
664
  if @config[:routers]
@@ -41,6 +41,10 @@ class ClientUnitTest < Test::Unit::TestCase
41
41
  assert_equal 1, @client.primary_pool.size
42
42
  end
43
43
 
44
+ should "set op timeout to default" do
45
+ assert_equal Mongo::MongoClient::DEFAULT_OP_TIMEOUT, @client.op_timeout
46
+ end
47
+
44
48
  should "default slave_ok to false" do
45
49
  assert !@client.slave_ok?
46
50
  end
@@ -67,6 +71,26 @@ class ClientUnitTest < Test::Unit::TestCase
67
71
  client.send(:initialize, *args)
68
72
  end
69
73
 
74
+ context 'specifying nil op timeout explicitly' do
75
+ setup do
76
+ @client = MongoClient.new('localhost', 27017, :connect => false, :op_timeout => nil)
77
+ end
78
+
79
+ should 'set op timeout to nil' do
80
+ assert_equal nil, @client.op_timeout
81
+ end
82
+ end
83
+
84
+ context 'specifying a different op timeout than default' do
85
+ setup do
86
+ @client = MongoClient.new('localhost', 27017, :connect => false, :op_timeout => 50)
87
+ end
88
+
89
+ should 'set op timeout to the specified value' do
90
+ assert_equal 50, @client.op_timeout
91
+ end
92
+ end
93
+
70
94
  context "given a replica set" do
71
95
 
72
96
  should "warn if invalid options are specified" do
@@ -92,11 +116,21 @@ class ClientUnitTest < Test::Unit::TestCase
92
116
 
93
117
  context "initializing with a unix socket" do
94
118
  setup do
95
- @connection = Mongo::Connection.new('/tmp/mongod.sock', :connect => false)
119
+ @client = MongoClient.new('/tmp/mongod.sock', :connect => false)
120
+ UNIXSocket.stubs(:new).returns(new_mock_unix_socket)
121
+ end
122
+ should "parse a unix socket" do
123
+ assert_equal "/tmp/mongod.sock", @client.host_port.first
124
+ end
125
+ end
126
+
127
+ context "initializing with a unix socket in uri" do
128
+ setup do
129
+ @client = MongoClient.from_uri("mongodb:///tmp/mongod.sock", :connect => false)
96
130
  UNIXSocket.stubs(:new).returns(new_mock_unix_socket)
97
131
  end
98
132
  should "parse a unix socket" do
99
- assert_equal "/tmp/mongod.sock", @connection.host_port.first
133
+ assert_equal "/tmp/mongod.sock", @client.host_port.first
100
134
  end
101
135
  end
102
136
 
@@ -153,11 +187,11 @@ class ClientUnitTest < Test::Unit::TestCase
153
187
 
154
188
  auth_hash = {
155
189
  :db_name => 'db',
190
+ :extra=>{},
156
191
  :username => 'hyphen-user_name',
157
192
  :password => 'p-s_s',
158
193
  :source => 'db',
159
- :mechanism => Authentication::DEFAULT_MECHANISM,
160
- :extra => {}
194
+ :mechanism => nil
161
195
  }
162
196
  assert_equal auth_hash, @client.auths.first
163
197
  end
@@ -272,11 +306,11 @@ class ClientUnitTest < Test::Unit::TestCase
272
306
 
273
307
  auth_hash = {
274
308
  :db_name => 'db',
309
+ :extra=>{},
275
310
  :username => 'hyphen-user_name',
276
311
  :password => 'p-s_s',
277
312
  :source => 'db',
278
- :mechanism => Authentication::DEFAULT_MECHANISM,
279
- :extra => {}
313
+ :mechanism => nil
280
314
  }
281
315
  assert_equal auth_hash, @client.auths.first
282
316
  end
@@ -70,7 +70,17 @@ class ConnectionUnitTest < Test::Unit::TestCase
70
70
 
71
71
  context "initializing with a unix socket" do
72
72
  setup do
73
- @connection = Mongo::Connection.new('/tmp/mongod.sock', :safe => true, :connect => false)
73
+ @connection = Mongo::Connection.new('/tmp/mongod.sock', :safe => true, :connect => false)
74
+ UNIXSocket.stubs(:new).returns(new_mock_unix_socket)
75
+ end
76
+ should "parse a unix socket" do
77
+ assert_equal "/tmp/mongod.sock", @connection.host_port.first
78
+ end
79
+ end
80
+
81
+ context "initializing with a unix socket in uri" do
82
+ setup do
83
+ @connection = Mongo::Connection.from_uri("mongodb:///tmp/mongod.sock", :connect => false)
74
84
  UNIXSocket.stubs(:new).returns(new_mock_unix_socket)
75
85
  end
76
86
  should "parse a unix socket" do
@@ -131,11 +141,11 @@ class ConnectionUnitTest < Test::Unit::TestCase
131
141
 
132
142
  auth_hash = {
133
143
  :db_name => 'db',
144
+ :extra=>{},
134
145
  :username => 'hyphen-user_name',
135
146
  :password => 'p-s_s',
136
147
  :source => 'db',
137
- :mechanism => Authentication::DEFAULT_MECHANISM,
138
- :extra => {}
148
+ :mechanism => nil
139
149
  }
140
150
  assert_equal auth_hash, @connection.auths.first
141
151
  end
@@ -250,11 +260,11 @@ class ConnectionUnitTest < Test::Unit::TestCase
250
260
 
251
261
  auth_hash = {
252
262
  :db_name => 'db',
263
+ :extra=>{},
253
264
  :username => 'hyphen-user_name',
254
265
  :password => 'p-s_s',
255
266
  :source => 'db',
256
- :mechanism => Authentication::DEFAULT_MECHANISM,
257
- :extra => {}
267
+ :mechanism => nil
258
268
  }
259
269
  assert_equal auth_hash, @connection.auths.first
260
270
  end
@@ -127,7 +127,7 @@ class DBUnitTest < Test::Unit::TestCase
127
127
  end
128
128
 
129
129
  should "allow extra authentication options" do
130
- extra_opts = { :gssapiservicename => 'example', :canonicalizehostname => true }
130
+ extra_opts = { :service_name => 'example', :canonicalize_host_name => true }
131
131
  assert @client.expects(:add_auth).with(@db.name, 'emily', nil, nil, 'GSSAPI', extra_opts)
132
132
  @db.authenticate('emily', nil, nil, nil, 'GSSAPI', extra_opts)
133
133
  end
@@ -74,6 +74,11 @@ class ReadPreferenceUnitTest < Test::Unit::TestCase
74
74
  assert_equal false, ReadPreference::secondary_ok?(command)
75
75
  end
76
76
 
77
+ def test_sok_text_returns_true
78
+ command = BSON::OrderedHash['text', BSON::OrderedHash['search', 'coffee']]
79
+ assert_equal true, ReadPreference::secondary_ok?(command)
80
+ end
81
+
77
82
  def test_cmd_reroute_with_secondary
78
83
  ReadPreference::expects(:warn).with(regexp_matches(/rerouted to primary/))
79
84
  command = BSON::OrderedHash['mapreduce', 'test-collection',
@@ -112,4 +117,290 @@ class ReadPreferenceUnitTest < Test::Unit::TestCase
112
117
  assert_equal true, ReadPreference::secondary_ok?(command)
113
118
  end
114
119
 
120
+ def test_primary_with_tags_raises_error
121
+ # Confirm that an error is raised if you provide tags and read pref is primary
122
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
123
+ client.stubs(:primary_pool).returns(mock_pool)
124
+ read_pref_tags = {'dc' => 'nyc'}
125
+ read_pref = client.read_preference.merge(:mode => :primary,
126
+ :tags => [read_pref_tags],
127
+ :latency => 6)
128
+ assert_raise Mongo::MongoArgumentError do
129
+ client.select_pool(read_pref)
130
+ end
131
+ end
132
+
133
+ def test_secondary_pref
134
+ # Confirm that a primary is not selected
135
+ primary_pool = mock('pool')
136
+
137
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
138
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 5)
139
+ secondary_pools = [secondary_nyc, secondary_chi]
140
+
141
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
142
+ client.stubs(:primary_pool).returns(primary_pool)
143
+ client.stubs(:secondary_pools).returns(secondary_pools)
144
+
145
+ read_pref = client.read_preference.merge(:mode => :secondary)
146
+ assert_not_equal Hash.new, client.select_pool(read_pref).tags
147
+ end
148
+
149
+ def test_secondary_tags_pref
150
+ # Confirm that a secondary with matching tags is selected
151
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
152
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 5)
153
+ secondary_pools = [secondary_nyc, secondary_chi]
154
+
155
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
156
+ client.stubs(:primary_pool).returns(mock_pool)
157
+ client.stubs(:secondary_pools).returns(secondary_pools)
158
+
159
+ read_pref_tags = {'dc' => 'nyc'}
160
+ read_pref = client.read_preference.merge(:mode => :secondary,
161
+ :tags => [read_pref_tags])
162
+ assert_equal read_pref_tags, client.select_pool(read_pref).tags
163
+ end
164
+
165
+ def test_secondary_tags_with_latency
166
+ # Confirm that between more than 1 secondary matching tags, only the one within
167
+ # max acceptable latency is selected
168
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
169
+ secondary_nyc2 = mock_pool({'dc' => 'nyc'}, 25)
170
+ secondary_pools = [secondary_nyc, secondary_nyc2]
171
+
172
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
173
+ client.stubs(:secondary_pools).returns(secondary_pools)
174
+
175
+ read_pref_tags = {'dc' => 'nyc'}
176
+ read_pref = client.read_preference.merge(:mode => :secondary,
177
+ :tags => [read_pref_tags])
178
+ assert_equal 5, client.select_pool(read_pref).ping_time
179
+ end
180
+
181
+ def test_secondary_latency_pref
182
+ # Confirm that only the latency of pools matching tags is considered
183
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 10)
184
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 5)
185
+ secondary_pools = [secondary_nyc, secondary_chi]
186
+
187
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
188
+ client.stubs(:primary_pool).returns(mock_pool)
189
+ client.stubs(:secondary_pools).returns(secondary_pools)
190
+
191
+ read_pref_tags = {'dc' => 'nyc'}
192
+ read_pref = client.read_preference.merge(:mode => :secondary,
193
+ :tags => [read_pref_tags],
194
+ :latency => 3)
195
+ assert_equal read_pref_tags, client.select_pool(read_pref).tags
196
+ end
197
+
198
+ def test_primary_preferred_primary_available
199
+ # Confirm that the primary is always selected if it's available
200
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
201
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 10)
202
+ secondary_pools = [secondary_nyc, secondary_chi]
203
+ primary_pool = mock_pool
204
+
205
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
206
+ client.stubs(:secondary_pools).returns(secondary_pools)
207
+ client.stubs(:primary_pool).returns(primary_pool)
208
+
209
+ read_pref_tags = {'dc' => 'chicago'}
210
+ read_pref = client.read_preference.merge(:mode => :primary_preferred,
211
+ :tags => [read_pref_tags],
212
+ :latency => 6)
213
+ assert_equal primary_pool, client.select_pool(read_pref)
214
+ end
215
+
216
+ def test_primary_preferred_primary_not_available
217
+ # Confirm that a secondary with matching tags is selected if primary is
218
+ # not available
219
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
220
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 10)
221
+ secondary_pools = [secondary_nyc, secondary_chi]
222
+
223
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
224
+ client.stubs(:secondary_pools).returns(secondary_pools)
225
+
226
+ read_pref_tags = {'dc' => 'chicago'}
227
+ read_pref = client.read_preference.merge(:mode => :primary_preferred,
228
+ :tags => [read_pref_tags],
229
+ :latency => 6)
230
+ assert_equal read_pref_tags, client.select_pool(read_pref).tags
231
+ end
232
+
233
+ def test_primary_preferred_primary_not_available_and_no_matching_tags
234
+ # Confirm that tags are taken into account if primary is not available and
235
+ # secondaries are considered for selection.
236
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
237
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 10)
238
+ secondary_pools = [secondary_nyc, secondary_chi]
239
+
240
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
241
+ client.stubs(:secondary_pools).returns(secondary_pools)
242
+
243
+ read_pref_tags = {'dc' => 'other_city'}
244
+ read_pref = client.read_preference.merge(:mode => :primary_preferred,
245
+ :tags => [read_pref_tags],
246
+ :latency => 6)
247
+ assert_equal nil, client.select_pool(read_pref)
248
+ end
249
+
250
+ def test_secondary_preferred_with_tags
251
+ # Confirm that tags are taken into account
252
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
253
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 10)
254
+ secondary_pools = [secondary_nyc, secondary_chi]
255
+
256
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
257
+ client.stubs(:secondary_pools).returns(secondary_pools)
258
+
259
+ read_pref_tags = {'dc' => 'chicago'}
260
+ read_pref = client.read_preference.merge(:mode => :secondary_preferred,
261
+ :tags => [read_pref_tags],
262
+ :latency => 6)
263
+ assert_equal read_pref_tags, client.select_pool(read_pref).tags
264
+ end
265
+
266
+ def test_secondary_preferred_with_no_matching_tags
267
+ # Confirm that the primary is selected if no secondaries with matching tags
268
+ # are found
269
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
270
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 10)
271
+ secondary_pools = [secondary_nyc, secondary_chi]
272
+
273
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
274
+ client.stubs(:secondary_pools).returns(secondary_pools)
275
+ client.stubs(:primary_pool).returns(mock_pool)
276
+
277
+ read_pref_tags = {'dc' => 'other_city'}
278
+ read_pref = client.read_preference.merge(:mode => :secondary_preferred,
279
+ :tags => [read_pref_tags],
280
+ :latency => 6)
281
+ assert_equal Hash.new, client.select_pool(read_pref).tags
282
+ end
283
+
284
+ def test_nearest_with_tags
285
+ # Confirm that tags are taken into account when selecting nearest
286
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
287
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 10)
288
+ primary_pool = mock_pool
289
+ pools = [secondary_nyc, secondary_chi, primary_pool]
290
+
291
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
292
+ client.stubs(:pools).returns(pools)
293
+
294
+ read_pref_tags = {'dc' => 'nyc'}
295
+ read_pref = client.read_preference.merge(:mode => :nearest,
296
+ :tags => [read_pref_tags],
297
+ :latency => 3)
298
+ assert_equal read_pref_tags, client.select_pool(read_pref).tags
299
+ end
300
+
301
+ def test_nearest
302
+ # Confirm that the nearest is selected when tags aren't specified
303
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
304
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 10)
305
+ primary_pool = mock_pool({}, 1)
306
+ pools = [secondary_nyc, secondary_chi, primary_pool]
307
+
308
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
309
+ client.stubs(:pools).returns(pools)
310
+
311
+ read_pref = client.read_preference.merge(:mode => :nearest,
312
+ :latency => 3)
313
+ assert_equal Hash.new, client.select_pool(read_pref).tags
314
+ end
315
+
316
+ def test_nearest_primary_matching
317
+ # Confirm that a primary matching tags is included in nearest candidates
318
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
319
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 10)
320
+ primary_pool = mock_pool({'dc' => 'boston'}, 1)
321
+ secondary_pools = [secondary_nyc, secondary_chi]
322
+
323
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
324
+ client.stubs(:secondary_pools).returns(secondary_pools)
325
+ client.stubs(:primary_pool).returns(primary_pool)
326
+ client.stubs(:pools).returns(secondary_pools << primary_pool)
327
+
328
+ read_pref_tags = {'dc' => 'boston'}
329
+ read_pref = client.read_preference.merge(:mode => :nearest,
330
+ :tags => [read_pref_tags])
331
+ assert_equal primary_pool, client.select_pool(read_pref)
332
+ end
333
+
334
+ def test_nearest_primary_not_matching
335
+ # Confirm that a primary not matching tags is not included in nearest candidates
336
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 25)
337
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 25)
338
+ primary_pool = mock_pool({'dc' => 'boston'}, 1)
339
+ secondary_pools = [secondary_nyc, secondary_chi]
340
+
341
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
342
+ client.stubs(:secondary_pools).returns(secondary_pools)
343
+ client.stubs(:primary_pool).returns(mock_pool)
344
+ client.stubs(:pools).returns(secondary_pools << primary_pool)
345
+
346
+ read_pref_tags = {'dc' => 'SF'}
347
+ read_pref = client.read_preference.merge(:mode => :nearest,
348
+ :tags => [read_pref_tags])
349
+ assert_equal nil, client.select_pool(read_pref)
350
+ end
351
+
352
+ def test_nearest_primary_not_matching_excluded_from_latency_calculations
353
+ # Confirm that a primary not matching tags is not included in the latency calculations
354
+ secondary1 = mock_pool({'dc' => 'nyc'}, 10)
355
+ secondary2 = mock_pool({'dc' => 'nyc'}, 10)
356
+ primary_pool = mock_pool({'dc' => 'boston'}, 1)
357
+ secondary_pools = [secondary1, secondary2]
358
+
359
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
360
+ client.stubs(:secondary_pools).returns(secondary_pools)
361
+ client.stubs(:primary_pool).returns(mock_pool)
362
+ client.stubs(:pools).returns(secondary_pools << primary_pool)
363
+
364
+ read_pref_tags = {'dc' => 'nyc'}
365
+ read_pref = client.read_preference.merge(:mode => :nearest,
366
+ :tags => [read_pref_tags],
367
+ :latency => 5)
368
+ assert_equal 'nyc', client.select_pool(read_pref).tags['dc']
369
+ end
370
+
371
+ def test_nearest_matching_tags_but_not_available
372
+ # Confirm that even if a server matches a tag, it's not selected if it's down
373
+ secondary_nyc = mock_pool({'dc' => 'nyc'}, 5)
374
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 10)
375
+ primary_pool = mock_pool({'dc' => 'chicago'}, nil)
376
+ secondary_pools = [secondary_nyc, secondary_chi]
377
+
378
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
379
+ client.stubs(:secondary_pools).returns(secondary_pools)
380
+ client.stubs(:primary_pool).returns(primary_pool)
381
+ client.stubs(:pools).returns(secondary_pools << primary_pool)
382
+
383
+ tags = [{'dc' => 'nyc'}, {'dc' => 'chicago'}, {}]
384
+ read_pref = client.read_preference.merge(:mode => :nearest,
385
+ :tags => tags)
386
+ assert_equal secondary_nyc, client.select_pool(read_pref)
387
+ end
388
+
389
+ def test_nearest_multiple_tags
390
+ # Confirm that with multiple tags in the read preference, servers are still selected
391
+ secondary_nyc = mock_pool({}, 5)
392
+ secondary_chi = mock_pool({'dc' => 'chicago'}, 10)
393
+ primary_pool = mock_pool({}, 1)
394
+ secondary_pools = [secondary_nyc, secondary_chi]
395
+
396
+ client = MongoReplicaSetClient.new(["#{TEST_HOST}:#{TEST_PORT}"], :connect => false)
397
+ client.stubs(:secondary_pools).returns(secondary_pools)
398
+ client.stubs(:primary_pool).returns(mock_pool)
399
+ client.stubs(:pools).returns(secondary_pools << primary_pool)
400
+
401
+ tags = [{'dc' => 'nyc'}, {'dc' => 'chicago'}, {}]
402
+ read_pref = client.read_preference.merge(:mode => :nearest,
403
+ :tags => tags)
404
+ assert_equal secondary_chi, client.select_pool(read_pref)
405
+ end
115
406
  end