mongo 1.1.4 → 1.1.5

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