mongo 1.11.1 → 1.12.0.rc0

Sign up to get free protection for your applications and to get access to all the features.
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