mongo 1.1.4 → 1.1.5

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 (40) hide show
  1. data/Rakefile +50 -69
  2. data/docs/CREDITS.md +4 -0
  3. data/docs/HISTORY.md +9 -0
  4. data/docs/REPLICA_SETS.md +8 -10
  5. data/lib/mongo.rb +3 -1
  6. data/lib/mongo/collection.rb +2 -1
  7. data/lib/mongo/connection.rb +146 -314
  8. data/lib/mongo/db.rb +6 -2
  9. data/lib/mongo/gridfs/grid.rb +1 -1
  10. data/lib/mongo/gridfs/grid_io.rb +29 -5
  11. data/lib/mongo/repl_set_connection.rb +290 -0
  12. data/lib/mongo/util/pool.rb +6 -8
  13. data/lib/mongo/util/uri_parser.rb +71 -0
  14. data/mongo.gemspec +1 -2
  15. data/test/collection_test.rb +9 -7
  16. data/test/connection_test.rb +0 -6
  17. data/test/grid_file_system_test.rb +2 -2
  18. data/test/grid_io_test.rb +33 -1
  19. data/test/grid_test.rb +36 -6
  20. data/test/replica_sets/connect_test.rb +59 -21
  21. data/test/replica_sets/count_test.rb +9 -7
  22. data/test/replica_sets/insert_test.rb +11 -9
  23. data/test/replica_sets/pooled_insert_test.rb +12 -13
  24. data/test/replica_sets/query_secondaries.rb +48 -8
  25. data/test/replica_sets/query_test.rb +10 -9
  26. data/test/replica_sets/replication_ack_test.rb +15 -22
  27. data/test/replica_sets/rs_test_helper.rb +29 -0
  28. data/test/test_helper.rb +13 -20
  29. data/test/threading/{test_threading_large_pool.rb → threading_with_large_pool_test.rb} +1 -1
  30. data/test/tools/repl_set_manager.rb +241 -0
  31. data/test/tools/test.rb +13 -0
  32. data/test/unit/connection_test.rb +3 -85
  33. data/test/unit/repl_set_connection_test.rb +82 -0
  34. metadata +19 -21
  35. data/test/replica_pairs/count_test.rb +0 -34
  36. data/test/replica_pairs/insert_test.rb +0 -50
  37. data/test/replica_pairs/pooled_insert_test.rb +0 -54
  38. data/test/replica_pairs/query_test.rb +0 -39
  39. data/test/replica_sets/node_type_test.rb +0 -42
  40. data/test/rs.rb +0 -24
data/Rakefile CHANGED
@@ -38,32 +38,45 @@ end
38
38
 
39
39
  desc "Test the MongoDB Ruby driver."
40
40
  task :test do
41
- puts "\nThis option has changed."
42
- puts "\nTo test the driver with the c-extensions:\nrake test:c\n"
43
- puts "To test the pure ruby driver: \nrake test:ruby"
41
+ puts "\nTo test the driver with the C-extensions:\nrake test:c\n\n"
42
+ puts "To test the pure ruby driver: \nrake test:ruby\n\n"
44
43
  end
45
44
 
46
45
  namespace :test do
47
46
 
48
- desc "Test the driver with the c extension enabled."
47
+ desc "Test the driver with the C extension enabled."
49
48
  task :c do
50
49
  ENV['C_EXT'] = 'TRUE'
51
- Rake::Task['test:unit'].invoke
52
- Rake::Task['test:functional'].invoke
53
- Rake::Task['test:bson'].invoke
54
- Rake::Task['test:pooled_threading'].invoke
55
- Rake::Task['test:drop_databases'].invoke
50
+ if ENV['TEST']
51
+ Rake::Task['test:functional'].invoke
52
+ else
53
+ Rake::Task['test:unit'].invoke
54
+ Rake::Task['test:functional'].invoke
55
+ Rake::Task['test:bson'].invoke
56
+ Rake::Task['test:pooled_threading'].invoke
57
+ Rake::Task['test:drop_databases'].invoke
58
+ end
56
59
  ENV['C_EXT'] = nil
57
60
  end
58
61
 
59
- desc "Test the driver using pure ruby (no c extension)"
62
+ desc "Test the driver using pure ruby (no C extension)"
60
63
  task :ruby do
61
64
  ENV['C_EXT'] = nil
62
- Rake::Task['test:unit'].invoke
63
- Rake::Task['test:functional'].invoke
64
- Rake::Task['test:bson'].invoke
65
- Rake::Task['test:pooled_threading'].invoke
66
- Rake::Task['test:drop_databases'].invoke
65
+ if ENV['TEST']
66
+ Rake::Task['test:functional'].invoke
67
+ else
68
+ Rake::Task['test:unit'].invoke
69
+ Rake::Task['test:functional'].invoke
70
+ Rake::Task['test:bson'].invoke
71
+ Rake::Task['test:pooled_threading'].invoke
72
+ Rake::Task['test:drop_databases'].invoke
73
+ end
74
+ end
75
+
76
+ desc "Run the replica set test suite"
77
+ Rake::TestTask.new(:rs) do |t|
78
+ t.test_files = FileList['test/replica_sets/*_test.rb']
79
+ t.verbose = true
67
80
  end
68
81
 
69
82
  Rake::TestTask.new(:unit) do |t|
@@ -77,52 +90,7 @@ namespace :test do
77
90
  end
78
91
 
79
92
  Rake::TestTask.new(:pooled_threading) do |t|
80
- t.test_files = FileList['test/threading/*.rb']
81
- t.verbose = true
82
- end
83
-
84
- Rake::TestTask.new(:replica_pair_count) do |t|
85
- t.test_files = FileList['test/replica_pairs/count_test.rb']
86
- t.verbose = true
87
- end
88
-
89
- Rake::TestTask.new(:replica_pair_insert) do |t|
90
- t.test_files = FileList['test/replica_pairs/insert_test.rb']
91
- t.verbose = true
92
- end
93
-
94
- Rake::TestTask.new(:pooled_replica_pair_insert) do |t|
95
- t.test_files = FileList['test/replica_pairs/pooled_insert_test.rb']
96
- t.verbose = true
97
- end
98
-
99
- Rake::TestTask.new(:replica_pair_query) do |t|
100
- t.test_files = FileList['test/replica_pairs/query_test.rb']
101
- t.verbose = true
102
- end
103
-
104
- Rake::TestTask.new(:replica_set_count) do |t|
105
- t.test_files = FileList['test/replica_sets/count_test.rb']
106
- t.verbose = true
107
- end
108
-
109
- Rake::TestTask.new(:replica_set_insert) do |t|
110
- t.test_files = FileList['test/replica_sets/insert_test.rb']
111
- t.verbose = true
112
- end
113
-
114
- Rake::TestTask.new(:pooled_replica_set_insert) do |t|
115
- t.test_files = FileList['test/replica_sets/pooled_insert_test.rb']
116
- t.verbose = true
117
- end
118
-
119
- Rake::TestTask.new(:replica_set_query) do |t|
120
- t.test_files = FileList['test/replica_sets/query_test.rb']
121
- t.verbose = true
122
- end
123
-
124
- Rake::TestTask.new(:replica_set_ack) do |t|
125
- t.test_files = FileList['test/replica_sets/replication_ack_test.rb']
93
+ t.test_files = FileList['test/threading/*_test.rb']
126
94
  t.verbose = true
127
95
  end
128
96
 
@@ -147,16 +115,17 @@ namespace :test do
147
115
  end
148
116
 
149
117
  task :drop_databases do |t|
150
- puts "Dropping test database..."
151
- require File.join(File.dirname(__FILE__), 'test', 'test_helper')
152
- include Mongo
153
- con = Connection.new(ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost',
154
- ENV['MONGO_RUBY_DRIVER_PORT'] || Connection::DEFAULT_PORT)
155
- con.drop_database(MONGO_TEST_DB)
118
+ puts "Dropping test databases..."
119
+ require './lib/mongo'
120
+ con = Mongo::Connection.new(ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost',
121
+ ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::Connection::DEFAULT_PORT)
122
+ con.database_names.each do |name|
123
+ con.drop_database(name) if name =~ /^ruby-test/
124
+ end
156
125
  end
157
126
  end
158
127
 
159
- desc "Generate documentation"
128
+ desc "Generate RDOC documentation"
160
129
  task :rdoc do
161
130
  version = eval(File.read("mongo.gemspec")).version
162
131
  out = File.join('html', version.to_s)
@@ -172,6 +141,19 @@ task :ydoc do
172
141
  system "yardoc lib/**/*.rb lib/mongo/**/*.rb lib/bson/**/*.rb -e yard/yard_ext.rb -p yard/templates -o #{out} --title MongoRuby-#{Mongo::VERSION} --files docs/TUTORIAL.md,docs/GridFS.md,docs/FAQ.md,docs/REPLICA_SETS.md,docs/WRITE_CONCERN.md,docs/HISTORY.md,docs/CREDITS.md,docs/1.0_UPGRADE.md"
173
142
  end
174
143
 
144
+ namespace :bamboo do
145
+ namespace :test do
146
+ task :ruby do
147
+ Rake::Task['test:ruby'].invoke
148
+ end
149
+
150
+ task :c do
151
+ Rake::Task['gem:install_extensions'].invoke
152
+ Rake::Task['test:c'].invoke
153
+ end
154
+ end
155
+ end
156
+
175
157
  namespace :gem do
176
158
 
177
159
  desc "Install the gem locally"
@@ -188,7 +170,6 @@ namespace :gem do
188
170
 
189
171
  desc "Install the optional c extensions"
190
172
  task :install_extensions do
191
- sh "gem uninstall -x -a -I bson_ext"
192
173
  sh "gem build bson_ext.gemspec"
193
174
  sh "gem install --no-rdoc --no-ri bson_ext-*.gem"
194
175
  sh "rm bson_ext-*.gem"
@@ -117,3 +117,7 @@ Hongli Lai (Phusion)
117
117
  Mislav Marohnić
118
118
 
119
119
  * Replaced returning with each_with_object
120
+
121
+ Alex Stupka
122
+
123
+ * Replica set port bug
@@ -1,5 +1,14 @@
1
1
  # MongoDB Ruby Driver History
2
2
 
3
+ ### 1.1.5
4
+ 2010-12-15
5
+
6
+ * ReplSetConnection class. This must be used for replica set connections from
7
+ now on. You can still use Connection.multi, but that method has been deprecated.
8
+ * Automated replica set tests. rake test:rs
9
+ * Check that request and response ids match.
10
+ * Several bug fixes. See the commit history for details.
11
+
3
12
  ### 1.1.4
4
13
  2010-11-30
5
14
 
@@ -6,17 +6,17 @@ Here follow a few considerations for those using the MongoDB Ruby driver with [r
6
6
 
7
7
  First, make sure that you've configured and initialized a replica set.
8
8
 
9
- Use `Connection.multi` to connect to a replica set:
9
+ Use `ReplSetConnection.new` to connect to a replica set:
10
10
 
11
- @connection = Connection.multi([['n1.mydb.net', 27017], ['n2.mydb.net', 27017], ['n3.mydb.net', 27017]])
11
+ @connection = ReplSetConnection.new(['n1.mydb.net', 27017], ['n2.mydb.net', 27017], ['n3.mydb.net', 27017])
12
12
 
13
13
  The driver will attempt to connect to a master node and, when found, will replace all seed nodes with known members of the replica set.
14
14
 
15
15
  ### Read slaves
16
16
 
17
- If you want to read from a seconday node, you can pass :read_secondary => true to Connection#multi.
17
+ If you want to read from a seconday node, you can pass :read_secondary => true to ReplSetConnection#new.
18
18
 
19
- @connection = Connection.multi([['n1.mydb.net', 27017], ['n2.mydb.net', 27017], ['n3.mydb.net', 27017]],
19
+ @connection = ReplSetConnection.new(['n1.mydb.net', 27017], ['n2.mydb.net', 27017], ['n3.mydb.net', 27017],
20
20
  :read_secondary => true)
21
21
 
22
22
  A random secondary will be chosen to be read from. In a typical multi-process Ruby application, you'll have a good distribution of reads across secondary nodes.
@@ -62,14 +62,12 @@ Of course, the proper way to handle connection failures will always depend on th
62
62
 
63
63
  ### Testing
64
64
 
65
- The Ruby driver (>= 1.0.6) includes some unit tests for verifying replica set behavior. They reside in *tests/replica_sets*. You can run them individually with the following rake tasks:
65
+ The Ruby driver (>= 1.1.5) includes unit tests for verifying replica set behavior. They reside in *tests/replica_sets*. You can run them as a group with the following rake task:
66
66
 
67
- rake test:replica_set_count
68
- rake test:replica_set_insert
69
- rake test:pooled_replica_set_insert
70
- rake test:replica_set_query
67
+ rake test:rs
71
68
 
72
- Make sure you have a replica set running on localhost before trying to run these tests.
69
+ The suite will set up a five-node replica set by itself and ensure that driver behaves correctly even in the face
70
+ of individual node failures. Node that the `mongod` executable must be in the search path for this to work.
73
71
 
74
72
  ### Further Reading
75
73
 
@@ -3,7 +3,7 @@
3
3
  $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
4
 
5
5
  module Mongo
6
- VERSION = "1.1.4"
6
+ VERSION = "1.1.5"
7
7
  end
8
8
 
9
9
  module Mongo
@@ -42,9 +42,11 @@ require 'mongo/util/support'
42
42
  require 'mongo/util/core_ext'
43
43
  require 'mongo/util/pool'
44
44
  require 'mongo/util/server_version'
45
+ require 'mongo/util/uri_parser'
45
46
 
46
47
  require 'mongo/collection'
47
48
  require 'mongo/connection'
49
+ require 'mongo/repl_set_connection'
48
50
  require 'mongo/cursor'
49
51
  require 'mongo/db'
50
52
  require 'mongo/exceptions'
@@ -321,6 +321,7 @@ module Mongo
321
321
  @connection.send_message_with_safe_check(Mongo::Constants::OP_DELETE, message, @db.name, nil, safe)
322
322
  else
323
323
  @connection.send_message(Mongo::Constants::OP_DELETE, message)
324
+ true
324
325
  end
325
326
  end
326
327
 
@@ -450,7 +451,7 @@ module Mongo
450
451
  generate_indexes(valid, name, opts) if valid.any?
451
452
 
452
453
  # Reset the cache here in case there are any errors inserting. Best to be safe.
453
- name.each {|n| @cache[n] = now + @cache_time}
454
+ @cache[name] = now + @cache_time
454
455
  name
455
456
  end
456
457
 
@@ -35,11 +35,7 @@ module Mongo
35
35
  STANDARD_HEADER_SIZE = 16
36
36
  RESPONSE_HEADER_SIZE = 20
37
37
 
38
- MONGODB_URI_MATCHER = /(([-_.\w\d]+):([-_\w\d]+)@)?([-.\w\d]+)(:([\w\d]+))?(\/([-\d\w]+))?/
39
- MONGODB_URI_SPEC = "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/database]"
40
-
41
- attr_reader :logger, :size, :nodes, :auths, :primary, :secondaries, :arbiters,
42
- :safe, :primary_pool, :read_pool, :secondary_pools
38
+ attr_reader :logger, :size, :auths, :primary, :safe, :primary_pool, :host_to_try
43
39
 
44
40
  # Counter for generating unique request ids.
45
41
  @@current_request_id = 0
@@ -49,8 +45,7 @@ module Mongo
49
45
  # You may specify whether connection to slave is permitted.
50
46
  # In all cases, the default host is "localhost" and the default port is 27017.
51
47
  #
52
- # To specify more than one host pair to be used as seeds in a replica set,
53
- # use Connection.multi.
48
+ # If you're connecting to a replica set, you'll need to use ReplSetConnection.new instead.
54
49
  #
55
50
  # Once connected to a replica set, you can find out which nodes are primary, secondary, and
56
51
  # arbiters with the corresponding accessors: Connection#primary, Connection#secondaries, and
@@ -92,62 +87,19 @@ module Mongo
92
87
  #
93
88
  # @core connections
94
89
  def initialize(host=nil, port=nil, options={})
95
- @auths = []
96
-
97
- if block_given?
98
- @nodes = yield self
99
- else
100
- @nodes = format_pair(host, port)
101
- end
90
+ @host_to_try = format_pair(host, port)
102
91
 
103
92
  # Host and port of current master.
104
93
  @host = @port = nil
105
94
 
106
- # Replica set name
107
- @replica_set_name = options[:rs_name]
108
-
109
- # Lock for request ids.
110
- @id_lock = Mutex.new
111
-
112
- # Pool size and timeout.
113
- @pool_size = options[:pool_size] || 1
114
- @timeout = options[:timeout] || 5.0
115
-
116
- # Mutex for synchronizing pool access
117
- @connection_mutex = Mutex.new
118
-
119
- # Global safe option. This is false by default.
120
- @safe = options[:safe] || false
121
-
122
- # Create a mutex when a new key, in this case a socket,
123
- # is added to the hash.
124
- @safe_mutexes = Hash.new { |h, k| h[k] = Mutex.new }
125
-
126
- # Condition variable for signal and wait
127
- @queue = ConditionVariable.new
128
-
129
95
  # slave_ok can be true only if one node is specified
130
96
  @slave_ok = options[:slave_ok]
131
97
 
132
- # Cache the various node types
133
- # when connecting to a replica set.
134
- @primary = nil
135
- @secondaries = []
136
- @arbiters = []
137
-
138
- # Connection pool for primay node
139
- @primary_pool = nil
140
-
141
- # Connection pools for each secondary node
142
- @secondary_pools = []
143
- @read_pool = nil
144
-
145
- @logger = options[:logger] || nil
146
-
147
- should_connect = options.fetch(:connect, true)
148
- connect if should_connect
98
+ setup(options)
149
99
  end
150
100
 
101
+ # DEPRECATED
102
+ #
151
103
  # Initialize a connection to a MongoDB replica set using an array of seed nodes.
152
104
  #
153
105
  # The seed nodes specified will be used on the initial connection to the replica set, but note
@@ -170,20 +122,13 @@ module Mongo
170
122
  # :read_secondary => true)
171
123
  #
172
124
  # @return [Mongo::Connection]
125
+ #
126
+ # @deprecated
173
127
  def self.multi(nodes, opts={})
174
- unless nodes.length > 0 && nodes.all? {|n| n.is_a? Array}
175
- raise MongoArgumentError, "Connection.multi requires at least one node to be specified."
176
- end
128
+ warn "Connection.multi is now deprecated. Please use ReplSetConnection.new instead."
177
129
 
178
- # Block returns an array, the first element being an array of nodes and the second an array
179
- # of authorizations for the database.
180
- new(nil, nil, opts) do |con|
181
- nodes.map do |node|
182
- con.instance_variable_set(:@replica_set, true)
183
- con.instance_variable_set(:@read_secondary, true) if opts[:read_secondary]
184
- con.pair_val_to_connection(node)
185
- end
186
- end
130
+ nodes << opts
131
+ ReplSetConnection.new(*nodes)
187
132
  end
188
133
 
189
134
  # Initialize a connection to MongoDB using the MongoDB URI spec:
@@ -195,8 +140,15 @@ module Mongo
195
140
  #
196
141
  # @return [Mongo::Connection]
197
142
  def self.from_uri(uri, opts={})
198
- new(nil, nil, opts) do |con|
199
- con.parse_uri(uri)
143
+ nodes, auths = Mongo::URIParser.parse(uri)
144
+ opts.merge!({:auths => auths})
145
+ if nodes.length == 1
146
+ Connection.new(nodes[0][0], nodes[0][1], opts)
147
+ elsif nodes.length > 1
148
+ nodes << opts
149
+ ReplSetConnection.new(*nodes)
150
+ else
151
+ raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node."
200
152
  end
201
153
  end
202
154
 
@@ -362,18 +314,7 @@ module Mongo
362
314
  self["admin"].command(oh)
363
315
  end
364
316
 
365
- # Increment and return the next available request id.
366
- #
367
- # return [Integer]
368
- def get_request_id
369
- request_id = ''
370
- @id_lock.synchronize do
371
- request_id = @@current_request_id += 1
372
- end
373
- request_id
374
- end
375
-
376
- # Get the build information for the current connection.
317
+ # Get the build information for the current connection.
377
318
  #
378
319
  # @return [Hash]
379
320
  def server_info
@@ -392,7 +333,7 @@ module Mongo
392
333
  #
393
334
  # @return [Boolean]
394
335
  def slave_ok?
395
- @read_secondary || @slave_ok
336
+ @slave_ok
396
337
  end
397
338
 
398
339
  # Send a message to MongoDB, adding the necessary headers.
@@ -403,7 +344,8 @@ module Mongo
403
344
  # @return [Integer] number of bytes sent
404
345
  def send_message(operation, message, log_message=nil)
405
346
  begin
406
- packed_message = add_message_headers(operation, message).to_s
347
+ add_message_headers(message, operation)
348
+ packed_message = message.to_s
407
349
  socket = checkout_writer
408
350
  send_message_on_socket(packed_message, socket)
409
351
  ensure
@@ -424,15 +366,19 @@ module Mongo
424
366
  #
425
367
  # @return [Hash] The document returned by the call to getlasterror.
426
368
  def send_message_with_safe_check(operation, message, db_name, log_message=nil, last_error_params=false)
427
- message_with_headers = add_message_headers(operation, message)
428
- message_with_check = last_error_message(db_name, last_error_params)
369
+ docs = num_received = cursor_id = ''
370
+ add_message_headers(message, operation)
371
+
372
+ last_error_message = BSON::ByteBuffer.new
373
+ build_last_error_message(last_error_message, db_name, last_error_params)
374
+ last_error_id = add_message_headers(last_error_message, Mongo::Constants::OP_QUERY)
375
+
376
+ packed_message = message.append!(last_error_message).to_s
429
377
  begin
430
378
  sock = checkout_writer
431
- packed_message = message_with_headers.append!(message_with_check).to_s
432
- docs = num_received = cursor_id = ''
433
379
  @safe_mutexes[sock].synchronize do
434
380
  send_message_on_socket(packed_message, sock)
435
- docs, num_received, cursor_id = receive(sock)
381
+ docs, num_received, cursor_id = receive(sock, last_error_id)
436
382
  end
437
383
  ensure
438
384
  checkin_writer(sock)
@@ -440,6 +386,7 @@ module Mongo
440
386
 
441
387
  if num_received == 1 && (error = docs[0]['err'] || docs[0]['errmsg'])
442
388
  close if error == "not master"
389
+ error = "wtimeout" if error == "timeout"
443
390
  raise Mongo::OperationFailure, docs[0]['code'].to_s + ': ' + error
444
391
  end
445
392
 
@@ -456,14 +403,15 @@ module Mongo
456
403
  # An array whose indexes include [0] documents returned, [1] number of document received,
457
404
  # and [3] a cursor_id.
458
405
  def receive_message(operation, message, log_message=nil, socket=nil, command=false)
459
- packed_message = add_message_headers(operation, message).to_s
406
+ request_id = add_message_headers(message, operation)
407
+ packed_message = message.to_s
460
408
  begin
461
409
  sock = socket || (command ? checkout_writer : checkout_reader)
462
410
 
463
411
  result = ''
464
412
  @safe_mutexes[sock].synchronize do
465
413
  send_message_on_socket(packed_message, sock)
466
- result = receive(sock)
414
+ result = receive(sock, request_id)
467
415
  end
468
416
  ensure
469
417
  command ? checkin_writer(sock) : checkin_reader(sock)
@@ -480,22 +428,15 @@ module Mongo
480
428
  # @raise [ConnectionFailure] if unable to connect to any host or port.
481
429
  def connect
482
430
  reset_connection
483
- @nodes_to_try = @nodes.clone
484
-
485
- while connecting?
486
- node = @nodes_to_try.shift
487
- config = check_is_master(node)
488
431
 
489
- if is_primary?(config)
490
- set_primary(node)
491
- else
492
- set_auxillary(node, config)
493
- end
432
+ config = check_is_master(@host_to_try)
433
+ if is_primary?(config)
434
+ set_primary(@host_to_try)
494
435
  end
495
436
 
496
- pick_secondary_for_read if @read_secondary
497
-
498
- raise ConnectionFailure, "failed to connect to any given host:port" unless connected?
437
+ if !connected?
438
+ raise ConnectionFailure, "Failed to connect to a master node at #{@host_to_try[0]}:#{@host_to_try[1]}"
439
+ end
499
440
  end
500
441
 
501
442
  def connecting?
@@ -513,139 +454,102 @@ module Mongo
513
454
  def close
514
455
  @primary_pool.close if @primary_pool
515
456
  @primary_pool = nil
516
- @read_pool = nil
517
- @secondary_pools.each do |pool|
518
- pool.close
519
- end
520
- end
521
-
522
- ## Configuration helper methods
523
-
524
- # Returns an array of host-port pairs.
525
- #
526
- # @private
527
- def format_pair(pair_or_host, port)
528
- case pair_or_host
529
- when String
530
- [[pair_or_host, port ? port.to_i : DEFAULT_PORT]]
531
- when nil
532
- [['localhost', DEFAULT_PORT]]
533
- end
534
- end
535
-
536
- # Convert an argument containing a host name string and a
537
- # port number integer into a [host, port] pair array.
538
- #
539
- # @private
540
- def pair_val_to_connection(a)
541
- case a
542
- when nil
543
- ['localhost', DEFAULT_PORT]
544
- when String
545
- [a, DEFAULT_PORT]
546
- when Integer
547
- ['localhost', a]
548
- when Array
549
- a
550
- end
551
- end
552
-
553
- # Parse a MongoDB URI. This method is used by Connection.from_uri.
554
- # Returns an array of nodes and an array of db authorizations, if applicable.
555
- #
556
- # @private
557
- def parse_uri(string)
558
- if string =~ /^mongodb:\/\//
559
- string = string[10..-1]
560
- else
561
- raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
562
- end
563
-
564
- nodes = []
565
- auths = []
566
- specs = string.split(',')
567
- specs.each do |spec|
568
- matches = MONGODB_URI_MATCHER.match(spec)
569
- if !matches
570
- raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
571
- end
572
-
573
- uname = matches[2]
574
- pwd = matches[3]
575
- host = matches[4]
576
- port = matches[6] || DEFAULT_PORT
577
- if !(port.to_s =~ /^\d+$/)
578
- raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits."
579
- end
580
- port = port.to_i
581
- db = matches[8]
582
-
583
- if uname && pwd && db
584
- add_auth(db, uname, pwd)
585
- elsif uname || pwd || db
586
- raise MongoArgumentError, "MongoDB URI must include all three of username, password, " +
587
- "and db if any one of these is specified."
588
- end
589
-
590
- nodes << [host, port]
591
- end
592
-
593
- nodes
594
457
  end
595
458
 
596
459
  # Checkout a socket for reading (i.e., a secondary node).
460
+ # Note: this is overridden in ReplSetConnection.
597
461
  def checkout_reader
598
462
  connect unless connected?
599
-
600
- if @read_pool
601
- @read_pool.checkout
602
- else
603
- checkout_writer
604
- end
463
+ @primary_pool.checkout
605
464
  end
606
465
 
607
466
  # Checkout a socket for writing (i.e., a primary node).
467
+ # Note: this is overridden in ReplSetConnection.
608
468
  def checkout_writer
609
469
  connect unless connected?
610
-
611
470
  @primary_pool.checkout
612
471
  end
613
472
 
614
473
  # Checkin a socket used for reading.
474
+ # Note: this is overridden in ReplSetConnection.
615
475
  def checkin_reader(socket)
616
- if @read_pool
617
- @read_pool.checkin(socket)
618
- else
619
- checkin_writer(socket)
476
+ if @primary_pool
477
+ @primary_pool.checkin(socket)
620
478
  end
621
479
  end
622
480
 
623
481
  # Checkin a socket used for writing.
482
+ # Note: this is overridden in ReplSetConnection.
624
483
  def checkin_writer(socket)
625
484
  if @primary_pool
626
485
  @primary_pool.checkin(socket)
627
486
  end
628
487
  end
629
488
 
630
- private
489
+ protected
631
490
 
632
- # Pick a node randomly from the set of possibly secondaries.
633
- def pick_secondary_for_read
634
- if (size = @secondary_pools.size) > 1
635
- @read_pool = @secondary_pools[rand(size)]
491
+ # Generic initialization code.
492
+ # @protected
493
+ def setup(options)
494
+ # Authentication objects
495
+ @auths = options.fetch(:auths, [])
496
+
497
+ # Lock for request ids.
498
+ @id_lock = Mutex.new
499
+
500
+ # Pool size and timeout.
501
+ @pool_size = options[:pool_size] || 1
502
+ @timeout = options[:timeout] || 5.0
503
+
504
+ # Mutex for synchronizing pool access
505
+ @connection_mutex = Mutex.new
506
+
507
+ # Global safe option. This is false by default.
508
+ @safe = options[:safe] || false
509
+
510
+ # Create a mutex when a new key, in this case a socket,
511
+ # is added to the hash.
512
+ @safe_mutexes = Hash.new { |h, k| h[k] = Mutex.new }
513
+
514
+ # Condition variable for signal and wait
515
+ @queue = ConditionVariable.new
516
+
517
+ # Connection pool for primay node
518
+ @primary = nil
519
+ @primary_pool = nil
520
+
521
+ @logger = options[:logger] || nil
522
+
523
+ should_connect = options.fetch(:connect, true)
524
+ connect if should_connect
525
+ end
526
+
527
+ ## Configuration helper methods
528
+
529
+ # Returns a host-port pair.
530
+ #
531
+ # @return [Array]
532
+ #
533
+ # @private
534
+ def format_pair(host, port)
535
+ case host
536
+ when String
537
+ [host, port ? port.to_i : DEFAULT_PORT]
538
+ when nil
539
+ ['localhost', DEFAULT_PORT]
636
540
  end
637
541
  end
638
542
 
543
+ private
544
+
545
+ ## Methods for establishing a connection:
546
+
639
547
  # If a ConnectionFailure is raised, this method will be called
640
548
  # to close the connection and reset connection values.
549
+ # TODO: evaluate whether this method is actually necessary
641
550
  def reset_connection
642
551
  close
643
552
  @primary = nil
644
- @secondaries = []
645
- @secondary_pools = []
646
- @arbiters = []
647
- @nodes_tried = []
648
- @nodes_to_try = []
649
553
  end
650
554
 
651
555
  # Primary is defined as either a master node or a slave if
@@ -653,8 +557,9 @@ module Mongo
653
557
  #
654
558
  # If a primary node is discovered, we set the the @host and @port and
655
559
  # apply any saved authentication.
560
+ # TODO: simplify
656
561
  def is_primary?(config)
657
- config && (config['ismaster'] == 1 || config['ismaster'] == true) || !@replica_set && @slave_ok
562
+ config && (config['ismaster'] == 1 || config['ismaster'] == true) || @slave_ok
658
563
  end
659
564
 
660
565
  def check_is_master(node)
@@ -664,41 +569,15 @@ module Mongo
664
569
  socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
665
570
 
666
571
  config = self['admin'].command({:ismaster => 1}, :sock => socket)
667
-
668
- check_set_name(config, socket)
669
572
  rescue OperationFailure, SocketError, SystemCallError, IOError => ex
670
- close unless connected?
573
+ close
671
574
  ensure
672
- @nodes_tried << node
673
- if config
674
- update_node_list(config['hosts']) if config['hosts']
675
-
676
- if config['msg'] && @logger
677
- @logger.warn("MONGODB #{config['msg']}")
678
- end
679
- end
680
-
681
575
  socket.close if socket
682
576
  end
683
577
 
684
578
  config
685
579
  end
686
580
 
687
- # Make sure that we're connected to the expected replica set.
688
- def check_set_name(config, socket)
689
- if @replica_set_name
690
- config = self['admin'].command({:replSetGetStatus => 1},
691
- :sock => socket, :check_response => false)
692
-
693
- if !Mongo::Support.ok?(config)
694
- raise ReplicaSetConnectionError, config['errmsg']
695
- elsif config['set'] != @replica_set_name
696
- raise ReplicaSetConnectionError,
697
- "Attempting to connect to replica set '#{config['set']}' but expected '#{@replica_set_name}'"
698
- end
699
- end
700
- end
701
-
702
581
  # Set the specified node as primary, and
703
582
  # apply any saved authentication credentials.
704
583
  def set_primary(node)
@@ -708,72 +587,30 @@ module Mongo
708
587
  apply_saved_authentication
709
588
  end
710
589
 
711
- # Determines what kind of node we have and caches its host
712
- # and port so that users can easily connect manually.
713
- def set_auxillary(node, config)
714
- if config
715
- if config['secondary']
716
- host, port = *node
717
- @secondaries << node unless @secondaries.include?(node)
718
- if @read_secondary
719
- @secondary_pools << Pool.new(self, host, port, :size => @pool_size, :timeout => @timeout)
720
- end
721
- elsif config['arbiterOnly']
722
- @arbiters << node unless @arbiters.include?(node)
723
- end
724
- end
725
- end
726
-
727
- # Update the list of known nodes. Only applies to replica sets,
728
- # where the response to the ismaster command will return a list
729
- # of known hosts.
730
- #
731
- # @param hosts [Array] a list of hosts, specified as string-encoded
732
- # host-port values. Example: ["myserver-1.org:27017", "myserver-1.org:27017"]
733
- #
734
- # @return [Array] the updated list of nodes
735
- def update_node_list(hosts)
736
- new_nodes = hosts.map do |host|
737
- if !host.respond_to?(:split)
738
- warn "Could not parse host #{host.inspect}."
739
- next
740
- end
741
-
742
- host, port = host.split(':')
743
- [host, port.to_i]
744
- end
745
-
746
- # Replace the list of seed nodes with the canonical list.
747
- @nodes = new_nodes.clone
748
590
 
749
- @nodes_to_try = new_nodes - @nodes_tried
750
- end
591
+ ## Low-level connection methods.
751
592
 
752
- def receive(sock)
753
- receive_and_discard_header(sock)
593
+ def receive(sock, expected_response)
594
+ begin
595
+ receive_header(sock, expected_response)
754
596
  number_received, cursor_id = receive_response_header(sock)
755
597
  read_documents(number_received, cursor_id, sock)
598
+ rescue Mongo::ConnectionFailure => ex
599
+ close
600
+ raise ex
601
+ end
756
602
  end
757
603
 
758
- def receive_header(sock)
759
- header = BSON::ByteBuffer.new
760
- header.put_binary(receive_message_on_socket(16, sock))
761
- unless header.size == STANDARD_HEADER_SIZE
762
- raise "Short read for DB response header: " +
763
- "expected #{STANDARD_HEADER_SIZE} bytes, saw #{header.size}"
604
+ def receive_header(sock, expected_response)
605
+ header = receive_message_on_socket(16, sock)
606
+ size, request_id, response_to = header.unpack('VVV')
607
+ if expected_response != response_to
608
+ raise Mongo::ConnectionFailure, "Expected response #{expected_response} but got #{response_to}"
764
609
  end
765
- header.rewind
766
- size = header.get_int
767
- request_id = header.get_int
768
- response_to = header.get_int
769
- op = header.get_int
770
- end
771
610
 
772
- def receive_and_discard_header(sock)
773
- bytes_read = receive_and_discard_message_on_socket(16, sock)
774
- unless bytes_read == STANDARD_HEADER_SIZE
611
+ unless header.size == STANDARD_HEADER_SIZE
775
612
  raise "Short read for DB response header: " +
776
- "expected #{STANDARD_HEADER_SIZE} bytes, saw #{bytes_read}"
613
+ "expected #{STANDARD_HEADER_SIZE} bytes, saw #{header.size}"
777
614
  end
778
615
  nil
779
616
  end
@@ -814,8 +651,9 @@ module Mongo
814
651
 
815
652
  # Constructs a getlasterror message. This method is used exclusively by
816
653
  # Connection#send_message_with_safe_check.
817
- def last_error_message(db_name, opts)
818
- message = BSON::ByteBuffer.new
654
+ #
655
+ # Because it modifies message by reference, we don't need to return it.
656
+ def build_last_error_message(message, db_name, opts)
819
657
  message.put_int(0)
820
658
  BSON::BSON_RUBY.serialize_cstr(message, "#{db_name}.$cmd")
821
659
  message.put_int(0)
@@ -827,18 +665,22 @@ module Mongo
827
665
  cmd.merge!(opts)
828
666
  end
829
667
  message.put_binary(BSON::BSON_CODER.serialize(cmd, false).to_s)
830
- add_message_headers(Mongo::Constants::OP_QUERY, message)
668
+ nil
831
669
  end
832
670
 
833
671
  # Prepares a message for transmission to MongoDB by
834
672
  # constructing a valid message header.
835
- def add_message_headers(operation, message)
673
+ #
674
+ # Note: this method modifies message by reference.
675
+ #
676
+ # @returns [Integer] the request id used in the header
677
+ def add_message_headers(message, operation)
836
678
  headers = [
837
679
  # Message size.
838
680
  16 + message.size,
839
681
 
840
682
  # Unique request id.
841
- get_request_id,
683
+ request_id = get_request_id,
842
684
 
843
685
  # Response id.
844
686
  0,
@@ -848,6 +690,19 @@ module Mongo
848
690
  ].pack('VVVV')
849
691
 
850
692
  message.prepend!(headers)
693
+
694
+ request_id
695
+ end
696
+
697
+ # Increment and return the next available request id.
698
+ #
699
+ # return [Integer]
700
+ def get_request_id
701
+ request_id = ''
702
+ @id_lock.synchronize do
703
+ request_id = @@current_request_id += 1
704
+ end
705
+ request_id
851
706
  end
852
707
 
853
708
  # Low-level method for sending a message on a socket.
@@ -893,29 +748,6 @@ module Mongo
893
748
  message
894
749
  end
895
750
 
896
- # Low-level data for receiving data from socket.
897
- # Unlike #receive_message_on_socket, this method immediately discards the data
898
- # and only returns the number of bytes read.
899
- def receive_and_discard_message_on_socket(length, socket)
900
- bytes_read = 0
901
- begin
902
- chunk = socket.read(length)
903
- bytes_read = chunk.length
904
- raise ConnectionFailure, "connection closed" unless bytes_read > 0
905
- if bytes_read < length
906
- while bytes_read < length
907
- socket.read(length - bytes_read, chunk)
908
- raise ConnectionFailure, "connection closed" unless chunk.length > 0
909
- bytes_read += chunk.length
910
- end
911
- end
912
- rescue => ex
913
- close
914
- raise ConnectionFailure, "Operation failed with the following exception: #{ex}"
915
- end
916
- bytes_read
917
- end
918
-
919
751
  if defined?(Encoding)
920
752
  BINARY_ENCODING = Encoding.find("binary")
921
753