mongo 1.0 → 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 (95) hide show
  1. data/LICENSE.txt +1 -13
  2. data/{README.rdoc → README.md} +129 -149
  3. data/Rakefile +94 -58
  4. data/bin/mongo_console +21 -0
  5. data/docs/1.0_UPGRADE.md +21 -0
  6. data/docs/CREDITS.md +123 -0
  7. data/docs/FAQ.md +112 -0
  8. data/docs/GridFS.md +158 -0
  9. data/docs/HISTORY.md +185 -0
  10. data/docs/REPLICA_SETS.md +75 -0
  11. data/docs/TUTORIAL.md +247 -0
  12. data/docs/WRITE_CONCERN.md +28 -0
  13. data/lib/mongo/collection.rb +225 -105
  14. data/lib/mongo/connection.rb +374 -315
  15. data/lib/mongo/cursor.rb +122 -77
  16. data/lib/mongo/db.rb +109 -85
  17. data/lib/mongo/exceptions.rb +6 -0
  18. data/lib/mongo/gridfs/grid.rb +19 -11
  19. data/lib/mongo/gridfs/grid_ext.rb +36 -9
  20. data/lib/mongo/gridfs/grid_file_system.rb +15 -9
  21. data/lib/mongo/gridfs/grid_io.rb +49 -16
  22. data/lib/mongo/gridfs/grid_io_fix.rb +38 -0
  23. data/lib/mongo/repl_set_connection.rb +290 -0
  24. data/lib/mongo/util/conversions.rb +3 -1
  25. data/lib/mongo/util/core_ext.rb +17 -4
  26. data/lib/mongo/util/pool.rb +125 -0
  27. data/lib/mongo/util/server_version.rb +2 -0
  28. data/lib/mongo/util/support.rb +12 -0
  29. data/lib/mongo/util/uri_parser.rb +71 -0
  30. data/lib/mongo.rb +23 -7
  31. data/{mongo-ruby-driver.gemspec → mongo.gemspec} +9 -7
  32. data/test/auxillary/1.4_features.rb +2 -2
  33. data/test/auxillary/authentication_test.rb +1 -1
  34. data/test/auxillary/autoreconnect_test.rb +1 -1
  35. data/test/{slave_connection_test.rb → auxillary/slave_connection_test.rb} +6 -6
  36. data/test/bson/binary_test.rb +15 -0
  37. data/test/bson/bson_test.rb +537 -0
  38. data/test/bson/byte_buffer_test.rb +190 -0
  39. data/test/bson/hash_with_indifferent_access_test.rb +38 -0
  40. data/test/bson/json_test.rb +17 -0
  41. data/test/bson/object_id_test.rb +141 -0
  42. data/test/bson/ordered_hash_test.rb +197 -0
  43. data/test/collection_test.rb +195 -15
  44. data/test/connection_test.rb +93 -56
  45. data/test/conversions_test.rb +1 -1
  46. data/test/cursor_fail_test.rb +75 -0
  47. data/test/cursor_message_test.rb +43 -0
  48. data/test/cursor_test.rb +93 -32
  49. data/test/db_api_test.rb +28 -55
  50. data/test/db_connection_test.rb +2 -3
  51. data/test/db_test.rb +45 -40
  52. data/test/grid_file_system_test.rb +14 -6
  53. data/test/grid_io_test.rb +36 -7
  54. data/test/grid_test.rb +54 -10
  55. data/test/replica_sets/connect_test.rb +84 -0
  56. data/test/replica_sets/count_test.rb +35 -0
  57. data/test/{replica → replica_sets}/insert_test.rb +17 -14
  58. data/test/replica_sets/pooled_insert_test.rb +55 -0
  59. data/test/replica_sets/query_secondaries.rb +80 -0
  60. data/test/replica_sets/query_test.rb +41 -0
  61. data/test/replica_sets/replication_ack_test.rb +64 -0
  62. data/test/replica_sets/rs_test_helper.rb +29 -0
  63. data/test/safe_test.rb +68 -0
  64. data/test/support/hash_with_indifferent_access.rb +199 -0
  65. data/test/support/keys.rb +45 -0
  66. data/test/support_test.rb +19 -0
  67. data/test/test_helper.rb +53 -15
  68. data/test/threading/{test_threading_large_pool.rb → threading_with_large_pool_test.rb} +2 -2
  69. data/test/threading_test.rb +2 -2
  70. data/test/tools/repl_set_manager.rb +241 -0
  71. data/test/tools/test.rb +13 -0
  72. data/test/unit/collection_test.rb +70 -7
  73. data/test/unit/connection_test.rb +18 -39
  74. data/test/unit/cursor_test.rb +7 -8
  75. data/test/unit/db_test.rb +14 -17
  76. data/test/unit/grid_test.rb +49 -0
  77. data/test/unit/pool_test.rb +9 -0
  78. data/test/unit/repl_set_connection_test.rb +82 -0
  79. data/test/unit/safe_test.rb +125 -0
  80. metadata +132 -51
  81. data/bin/bson_benchmark.rb +0 -59
  82. data/bin/fail_if_no_c.rb +0 -11
  83. data/examples/admin.rb +0 -43
  84. data/examples/capped.rb +0 -22
  85. data/examples/cursor.rb +0 -48
  86. data/examples/gridfs.rb +0 -44
  87. data/examples/index_test.rb +0 -126
  88. data/examples/info.rb +0 -31
  89. data/examples/queries.rb +0 -70
  90. data/examples/simple.rb +0 -24
  91. data/examples/strict.rb +0 -35
  92. data/examples/types.rb +0 -36
  93. data/test/replica/count_test.rb +0 -34
  94. data/test/replica/pooled_insert_test.rb +0 -54
  95. data/test/replica/query_test.rb +0 -39
@@ -1,3 +1,5 @@
1
+ # encoding: UTF-8
2
+
1
3
  # --
2
4
  # Copyright (C) 2008-2010 10gen Inc.
3
5
  #
@@ -22,6 +24,9 @@ module Mongo
22
24
 
23
25
  # Instantiates and manages connections to MongoDB.
24
26
  class Connection
27
+ TCPSocket = ::TCPSocket
28
+ Mutex = ::Mutex
29
+ ConditionVariable = ::ConditionVariable
25
30
 
26
31
  # Abort connections if a ConnectionError is raised.
27
32
  Thread.abort_on_exception = true
@@ -30,34 +35,38 @@ module Mongo
30
35
  STANDARD_HEADER_SIZE = 16
31
36
  RESPONSE_HEADER_SIZE = 20
32
37
 
33
- MONGODB_URI_MATCHER = /(([.\w\d]+):([\w\d]+)@)?([.\w\d]+)(:([\w\d]+))?(\/([-\d\w]+))?/
34
- MONGODB_URI_SPEC = "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/database]"
35
-
36
- attr_reader :logger, :size, :host, :port, :nodes, :auths, :sockets, :checked_out
38
+ attr_reader :logger, :size, :auths, :primary, :safe, :primary_pool, :host_to_try
37
39
 
38
40
  # Counter for generating unique request ids.
39
41
  @@current_request_id = 0
40
42
 
41
- # Create a connection to MongoDB. Specify either one or a pair of servers,
42
- # along with a maximum connection pool size and timeout.
43
+ # Create a connection to single MongoDB instance.
43
44
  #
44
- # If connecting to just one server, you may specify whether connection to slave is permitted.
45
+ # You may specify whether connection to slave is permitted.
45
46
  # In all cases, the default host is "localhost" and the default port is 27017.
46
47
  #
47
- # To specify a pair, use Connection.paired.
48
+ # If you're connecting to a replica set, you'll need to use ReplSetConnection.new instead.
48
49
  #
49
- # Note that there are a few issues when using connection pooling with Ruby 1.9 on Windows. These
50
- # should be resolved in the next release.
50
+ # Once connected to a replica set, you can find out which nodes are primary, secondary, and
51
+ # arbiters with the corresponding accessors: Connection#primary, Connection#secondaries, and
52
+ # Connection#arbiters. This is useful if your application needs to connect manually to nodes other
53
+ # than the primary.
51
54
  #
52
55
  # @param [String, Hash] host.
53
56
  # @param [Integer] port specify a port number here if only one host is being specified.
54
57
  #
58
+ # @option options [Boolean, Hash] :safe (false) Set the default safe-mode options
59
+ # propogated to DB objects instantiated off of this Connection. This
60
+ # default can be overridden upon instantiation of any DB by explicity setting a :safe value
61
+ # on initialization.
55
62
  # @option options [Boolean] :slave_ok (false) Must be set to +true+ when connecting
56
63
  # to a single, slave node.
57
64
  # @option options [Logger, #debug] :logger (nil) Logger instance to receive driver operation log.
58
- # @option options [Integer] :pool_size (1) The maximum number of socket connections that can be opened to the database.
59
- # @option options [Float] :timeout (5.0) When all of the connections to the pool are checked out,
65
+ # @option options [Integer] :pool_size (1) The maximum number of socket connections allowed per
66
+ # connection pool. Note: this setting is relevant only for multi-threaded applications.
67
+ # @option options [Float] :timeout (5.0) When all of the connections a pool are checked out,
60
68
  # this is the number of seconds to wait for a new connection to be released before throwing an exception.
69
+ # Note: this setting is relevant only for multi-threaded applications (which in Ruby are rare).
61
70
  #
62
71
  # @example localhost, 27017
63
72
  # Connection.new
@@ -71,71 +80,55 @@ module Mongo
71
80
  # @example localhost, 3000, where this node may be a slave
72
81
  # Connection.new("localhost", 3000, :slave_ok => true)
73
82
  #
74
- # @see http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby Replica pairs in Ruby
83
+ # @see http://api.mongodb.org/ruby/current/file.REPLICA_SETS.html Replica sets in Ruby
84
+ #
85
+ # @raise [ReplicaSetConnectionError] This is raised if a replica set name is specified and the
86
+ # driver fails to connect to a replica set with that name.
75
87
  #
76
88
  # @core connections
77
89
  def initialize(host=nil, port=nil, options={})
78
- @auths = []
79
-
80
- if block_given?
81
- @nodes = yield self
82
- else
83
- @nodes = format_pair(host, port)
84
- end
90
+ @host_to_try = format_pair(host, port)
85
91
 
86
92
  # Host and port of current master.
87
93
  @host = @port = nil
88
94
 
89
- # Lock for request ids.
90
- @id_lock = Mutex.new
91
-
92
- # Pool size and timeout.
93
- @size = options[:pool_size] || 1
94
- @timeout = options[:timeout] || 5.0
95
-
96
- # Mutex for synchronizing pool access
97
- @connection_mutex = Mutex.new
98
- @safe_mutex = Mutex.new
99
-
100
- # Condition variable for signal and wait
101
- @queue = ConditionVariable.new
102
-
103
- @sockets = []
104
- @checked_out = []
105
-
106
95
  # slave_ok can be true only if one node is specified
107
- @slave_ok = options[:slave_ok] && @nodes.length == 1
108
- @logger = options[:logger] || nil
109
- @options = options
96
+ @slave_ok = options[:slave_ok]
110
97
 
111
- should_connect = options[:connect].nil? ? true : options[:connect]
112
- connect_to_master if should_connect
98
+ setup(options)
113
99
  end
114
100
 
115
- # Initialize a paired connection to MongoDB.
101
+ # DEPRECATED
116
102
  #
117
- # @param nodes [Array] An array of arrays, each of which specified a host and port.
118
- # @param opts Takes the same options as Connection.new
103
+ # Initialize a connection to a MongoDB replica set using an array of seed nodes.
119
104
  #
120
- # @example
121
- # Connection.paired([["db1.example.com", 27017],
122
- # ["db2.example.com", 27017]])
105
+ # The seed nodes specified will be used on the initial connection to the replica set, but note
106
+ # that this list of nodes will be replced by the list of canonical nodes returned by running the
107
+ # is_master command on the replica set.
108
+ #
109
+ # @param nodes [Array] An array of arrays, each of which specifies a host and port.
110
+ # @param opts [Hash] Any of the available options that can be passed to Connection.new.
111
+ #
112
+ # @option options [String] :rs_name (nil) The name of the replica set to connect to. An exception will be
113
+ # raised if unable to connect to a replica set with this name.
114
+ # @option options [Boolean] :read_secondary (false) When true, this connection object will pick a random slave
115
+ # to send reads to.
123
116
  #
124
117
  # @example
125
- # Connection.paired([["db1.example.com", 27017],
126
- # ["db2.example.com", 27017]],
127
- # :pool_size => 20, :timeout => 5)
118
+ # Connection.multi([["db1.example.com", 27017], ["db2.example.com", 27017]])
119
+ #
120
+ # @example This connection will read from a random secondary node.
121
+ # Connection.multi([["db1.example.com", 27017], ["db2.example.com", 27017], ["db3.example.com", 27017]],
122
+ # :read_secondary => true)
128
123
  #
129
124
  # @return [Mongo::Connection]
130
- def self.paired(nodes, opts={})
131
- unless nodes.length == 2 && nodes.all? {|n| n.is_a? Array}
132
- raise MongoArgumentError, "Connection.paired requires that exactly two nodes be specified."
133
- end
134
- # Block returns an array, the first element being an array of nodes and the second an array
135
- # of authorizations for the database.
136
- new(nil, nil, opts) do |con|
137
- [con.pair_val_to_connection(nodes[0]), con.pair_val_to_connection(nodes[1])]
138
- end
125
+ #
126
+ # @deprecated
127
+ def self.multi(nodes, opts={})
128
+ warn "Connection.multi is now deprecated. Please use ReplSetConnection.new instead."
129
+
130
+ nodes << opts
131
+ ReplSetConnection.new(*nodes)
139
132
  end
140
133
 
141
134
  # Initialize a connection to MongoDB using the MongoDB URI spec:
@@ -147,11 +140,43 @@ module Mongo
147
140
  #
148
141
  # @return [Mongo::Connection]
149
142
  def self.from_uri(uri, opts={})
150
- new(nil, nil, opts) do |con|
151
- 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."
152
152
  end
153
153
  end
154
154
 
155
+ # Fsync, then lock the mongod process against writes. Use this to get
156
+ # the datafiles in a state safe for snapshotting, backing up, etc.
157
+ #
158
+ # @return [BSON::OrderedHash] the command response
159
+ def lock!
160
+ cmd = BSON::OrderedHash.new
161
+ cmd[:fsync] = 1
162
+ cmd[:lock] = true
163
+ self['admin'].command(cmd)
164
+ end
165
+
166
+ # Is this database locked against writes?
167
+ #
168
+ # @return [Boolean]
169
+ def locked?
170
+ self['admin']['$cmd.sys.inprog'].find_one['fsyncLock'] == 1
171
+ end
172
+
173
+ # Unlock a previously fsync-locked mongod process.
174
+ #
175
+ # @return [BSON::OrderedHash] command response
176
+ def unlock!
177
+ self['admin']['$cmd.sys.unlock'].find_one
178
+ end
179
+
155
180
  # Apply each of the saved database authentications.
156
181
  #
157
182
  # @return [Boolean] returns true if authentications exist and succeeed, false
@@ -172,6 +197,9 @@ module Mongo
172
197
  # specificed in the list of auths. This method is called automatically
173
198
  # by DB#authenticate.
174
199
  #
200
+ # Note: this method will not actually issue an authentication command. To do that,
201
+ # either run Connection#apply_saved_authentication or DB#authenticate.
202
+ #
175
203
  # @param [String] db_name
176
204
  # @param [String] username
177
205
  # @param [String] password
@@ -214,9 +242,9 @@ module Mongo
214
242
  #
215
243
  # @return [Hash]
216
244
  def database_info
217
- doc = self['admin'].command({:listDatabases => 1}, false, true)
218
- returning({}) do |info|
219
- doc['databases'].each { |db| info[db['name']] = db['sizeOnDisk'].to_i }
245
+ doc = self['admin'].command({:listDatabases => 1})
246
+ doc['databases'].each_with_object({}) do |db, info|
247
+ info[db['name']] = db['sizeOnDisk'].to_i
220
248
  end
221
249
  end
222
250
 
@@ -236,7 +264,7 @@ module Mongo
236
264
  #
237
265
  # @core databases db-instance_method
238
266
  def db(db_name, options={})
239
- DB.new(db_name, self, options.merge(:logger => @logger))
267
+ DB.new(db_name, self, options)
240
268
  end
241
269
 
242
270
  # Shortcut for returning a database. Use DB#db to accept options.
@@ -247,7 +275,7 @@ module Mongo
247
275
  #
248
276
  # @core databases []-instance_method
249
277
  def [](db_name)
250
- DB.new(db_name, self, :logger => @logger)
278
+ DB.new(db_name, self, :safe => @safe)
251
279
  end
252
280
 
253
281
  # Drop a database.
@@ -266,7 +294,7 @@ module Mongo
266
294
  # @param [String] username username for authentication against from_db (>=1.3.x).
267
295
  # @param [String] password password for authentication against from_db (>=1.3.x).
268
296
  def copy_database(from, to, from_host="localhost", username=nil, password=nil)
269
- oh = OrderedHash.new
297
+ oh = BSON::OrderedHash.new
270
298
  oh[:copydb] = 1
271
299
  oh[:fromhost] = from_host
272
300
  oh[:fromdb] = from
@@ -275,33 +303,22 @@ module Mongo
275
303
  unless username && password
276
304
  raise MongoArgumentError, "Both username and password must be supplied for authentication."
277
305
  end
278
- nonce_cmd = OrderedHash.new
306
+ nonce_cmd = BSON::OrderedHash.new
279
307
  nonce_cmd[:copydbgetnonce] = 1
280
308
  nonce_cmd[:fromhost] = from_host
281
- result = self["admin"].command(nonce_cmd, true, true)
309
+ result = self["admin"].command(nonce_cmd)
282
310
  oh[:nonce] = result["nonce"]
283
311
  oh[:username] = username
284
312
  oh[:key] = Mongo::Support.auth_key(username, password, oh[:nonce])
285
313
  end
286
- self["admin"].command(oh, true, true)
314
+ self["admin"].command(oh)
287
315
  end
288
316
 
289
- # Increment and return the next available request id.
290
- #
291
- # return [Integer]
292
- def get_request_id
293
- request_id = ''
294
- @id_lock.synchronize do
295
- request_id = @@current_request_id += 1
296
- end
297
- request_id
298
- end
299
-
300
- # Get the build information for the current connection.
317
+ # Get the build information for the current connection.
301
318
  #
302
319
  # @return [Hash]
303
320
  def server_info
304
- self["admin"].command({:buildinfo => 1}, false, true)
321
+ self["admin"].command({:buildinfo => 1})
305
322
  end
306
323
 
307
324
  # Get the build version of the current server.
@@ -319,24 +336,20 @@ module Mongo
319
336
  @slave_ok
320
337
  end
321
338
 
322
-
323
- ## Connections and pooling ##
324
-
325
339
  # Send a message to MongoDB, adding the necessary headers.
326
340
  #
327
341
  # @param [Integer] operation a MongoDB opcode.
328
342
  # @param [BSON::ByteBuffer] message a message to send to the database.
329
- # @param [String] log_message text version of +message+ for logging.
330
343
  #
331
- # @return [True]
344
+ # @return [Integer] number of bytes sent
332
345
  def send_message(operation, message, log_message=nil)
333
- @logger.debug(" MONGODB #{log_message || message}") if @logger
334
346
  begin
335
- packed_message = add_message_headers(operation, message).to_s
336
- socket = checkout
347
+ add_message_headers(message, operation)
348
+ packed_message = message.to_s
349
+ socket = checkout_writer
337
350
  send_message_on_socket(packed_message, socket)
338
351
  ensure
339
- checkin(socket)
352
+ checkin_writer(socket)
340
353
  end
341
354
  end
342
355
 
@@ -346,55 +359,62 @@ module Mongo
346
359
  # @param [Integer] operation a MongoDB opcode.
347
360
  # @param [BSON::ByteBuffer] message a message to send to the database.
348
361
  # @param [String] db_name the name of the database. used on call to get_last_error.
349
- # @param [String] log_message text version of +message+ for logging.
362
+ # @param [Hash] last_error_params parameters to be sent to getLastError. See DB#error for
363
+ # available options.
350
364
  #
351
- # @return [Array]
352
- # An array whose indexes include [0] documents returned, [1] number of document received,
353
- # and [3] a cursor_id.
354
- def send_message_with_safe_check(operation, message, db_name, log_message=nil)
355
- message_with_headers = add_message_headers(operation, message)
356
- message_with_check = last_error_message(db_name)
357
- @logger.debug(" MONGODB #{log_message || message}") if @logger
365
+ # @see DB#get_last_error for valid last error params.
366
+ #
367
+ # @return [Hash] The document returned by the call to getlasterror.
368
+ def send_message_with_safe_check(operation, message, db_name, log_message=nil, last_error_params=false)
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
358
377
  begin
359
- sock = checkout
360
- packed_message = message_with_headers.append!(message_with_check).to_s
361
- docs = num_received = cursor_id = ''
362
- @safe_mutex.synchronize do
378
+ sock = checkout_writer
379
+ @safe_mutexes[sock].synchronize do
363
380
  send_message_on_socket(packed_message, sock)
364
- docs, num_received, cursor_id = receive(sock)
381
+ docs, num_received, cursor_id = receive(sock, last_error_id)
365
382
  end
366
383
  ensure
367
- checkin(sock)
384
+ checkin_writer(sock)
368
385
  end
369
- if num_received == 1 && error = docs[0]['err']
370
- raise Mongo::OperationFailure, error
386
+
387
+ if num_received == 1 && (error = docs[0]['err'] || docs[0]['errmsg'])
388
+ close if error == "not master"
389
+ error = "wtimeout" if error == "timeout"
390
+ raise Mongo::OperationFailure, docs[0]['code'].to_s + ': ' + error
371
391
  end
372
- [docs, num_received, cursor_id]
392
+
393
+ docs[0]
373
394
  end
374
395
 
375
396
  # Sends a message to the database and waits for the response.
376
397
  #
377
398
  # @param [Integer] operation a MongoDB opcode.
378
399
  # @param [BSON::ByteBuffer] message a message to send to the database.
379
- # @param [String] log_message text version of +message+ for logging.
380
400
  # @param [Socket] socket a socket to use in lieu of checking out a new one.
381
401
  #
382
402
  # @return [Array]
383
403
  # An array whose indexes include [0] documents returned, [1] number of document received,
384
404
  # and [3] a cursor_id.
385
- def receive_message(operation, message, log_message=nil, socket=nil)
386
- packed_message = add_message_headers(operation, message).to_s
387
- @logger.debug(" MONGODB #{log_message || message}") if @logger
405
+ def receive_message(operation, message, log_message=nil, socket=nil, command=false)
406
+ request_id = add_message_headers(message, operation)
407
+ packed_message = message.to_s
388
408
  begin
389
- sock = socket || checkout
409
+ sock = socket || (command ? checkout_writer : checkout_reader)
390
410
 
391
411
  result = ''
392
- @safe_mutex.synchronize do
412
+ @safe_mutexes[sock].synchronize do
393
413
  send_message_on_socket(packed_message, sock)
394
- result = receive(sock)
414
+ result = receive(sock, request_id)
395
415
  end
396
416
  ensure
397
- checkin(sock)
417
+ command ? checkin_writer(sock) : checkin_reader(sock)
398
418
  end
399
419
  result
400
420
  end
@@ -402,282 +422,305 @@ module Mongo
402
422
  # Create a new socket and attempt to connect to master.
403
423
  # If successful, sets host and port to master and returns the socket.
404
424
  #
425
+ # If connecting to a replica set, this method will replace the
426
+ # initially-provided seed list with any nodes known to the set.
427
+ #
405
428
  # @raise [ConnectionFailure] if unable to connect to any host or port.
406
- def connect_to_master
407
- close
408
- @host = @port = nil
409
- for node_pair in @nodes
410
- host, port = *node_pair
411
- begin
412
- socket = TCPSocket.new(host, port)
413
- socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
414
-
415
- # If we're connected to master, set the @host and @port
416
- result = self['admin'].command({:ismaster => 1}, false, false, socket)
417
- if result['ok'] == 1 && ((is_master = result['ismaster'] == 1) || @slave_ok)
418
- @host, @port = host, port
419
- apply_saved_authentication
420
- end
429
+ def connect
430
+ reset_connection
421
431
 
422
- # Note: slave_ok can be true only when connecting to a single node.
423
- if @nodes.length == 1 && !is_master && !@slave_ok
424
- raise ConfigurationError, "Trying to connect directly to slave; " +
425
- "if this is what you want, specify :slave_ok => true."
426
- end
432
+ config = check_is_master(@host_to_try)
433
+ if is_primary?(config)
434
+ set_primary(@host_to_try)
435
+ end
427
436
 
428
- break if is_master || @slave_ok
429
- rescue SocketError, SystemCallError, IOError => ex
430
- socket.close if socket
431
- close
432
- false
433
- end
437
+ if !connected?
438
+ raise ConnectionFailure, "Failed to connect to a master node at #{@host_to_try[0]}:#{@host_to_try[1]}"
434
439
  end
435
- raise ConnectionFailure, "failed to connect to any given host:port" unless socket
436
440
  end
437
441
 
438
- # Are we connected to MongoDB? This is determined by checking whether
439
- # host and port have values, since they're set to nil on calls to #close.
442
+ def connecting?
443
+ @nodes_to_try.length > 0
444
+ end
445
+
446
+ # It's possible that we defined connected as all nodes being connected???
447
+ # NOTE: Do check if this needs to be more stringent.
448
+ # Probably not since if any node raises a connection failure, all nodes will be closed.
440
449
  def connected?
441
- @host && @port
450
+ @primary_pool && @primary_pool.host && @primary_pool.port
442
451
  end
443
452
 
444
453
  # Close the connection to the database.
445
454
  def close
446
- @sockets.each do |sock|
447
- sock.close
448
- end
449
- @host = @port = nil
450
- @sockets.clear
451
- @checked_out.clear
455
+ @primary_pool.close if @primary_pool
456
+ @primary_pool = nil
452
457
  end
453
458
 
454
- ## Configuration helper methods
459
+ # Checkout a socket for reading (i.e., a secondary node).
460
+ # Note: this is overridden in ReplSetConnection.
461
+ def checkout_reader
462
+ connect unless connected?
463
+ @primary_pool.checkout
464
+ end
455
465
 
456
- # Returns an array of host-port pairs.
457
- #
458
- # @private
459
- def format_pair(pair_or_host, port)
460
- case pair_or_host
461
- when String
462
- [[pair_or_host, port ? port.to_i : DEFAULT_PORT]]
463
- when nil
464
- [['localhost', DEFAULT_PORT]]
465
- end
466
+ # Checkout a socket for writing (i.e., a primary node).
467
+ # Note: this is overridden in ReplSetConnection.
468
+ def checkout_writer
469
+ connect unless connected?
470
+ @primary_pool.checkout
466
471
  end
467
472
 
468
- # Convert an argument containing a host name string and a
469
- # port number integer into a [host, port] pair array.
470
- #
471
- # @private
472
- def pair_val_to_connection(a)
473
- case a
474
- when nil
475
- ['localhost', DEFAULT_PORT]
476
- when String
477
- [a, DEFAULT_PORT]
478
- when Integer
479
- ['localhost', a]
480
- when Array
481
- a
473
+ # Checkin a socket used for reading.
474
+ # Note: this is overridden in ReplSetConnection.
475
+ def checkin_reader(socket)
476
+ if @primary_pool
477
+ @primary_pool.checkin(socket)
482
478
  end
483
479
  end
484
480
 
485
- # Parse a MongoDB URI. This method is used by Connection.from_uri.
486
- # Returns an array of nodes and an array of db authorizations, if applicable.
487
- #
488
- # @private
489
- def parse_uri(string)
490
- if string =~ /^mongodb:\/\//
491
- string = string[10..-1]
492
- else
493
- raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
481
+ # Checkin a socket used for writing.
482
+ # Note: this is overridden in ReplSetConnection.
483
+ def checkin_writer(socket)
484
+ if @primary_pool
485
+ @primary_pool.checkin(socket)
494
486
  end
487
+ end
495
488
 
496
- nodes = []
497
- auths = []
498
- specs = string.split(',')
499
- specs.each do |spec|
500
- matches = MONGODB_URI_MATCHER.match(spec)
501
- if !matches
502
- raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
503
- end
489
+ protected
504
490
 
505
- uname = matches[2]
506
- pwd = matches[3]
507
- host = matches[4]
508
- port = matches[6] || DEFAULT_PORT
509
- if !(port.to_s =~ /^\d+$/)
510
- raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits."
511
- end
512
- port = port.to_i
513
- db = matches[8]
514
-
515
- if (uname || pwd || db) && !(uname && pwd && db)
516
- raise MongoArgumentError, "MongoDB URI must include all three of username, password, " +
517
- "and db if any one of these is specified."
518
- else
519
- add_auth(db, uname, pwd)
520
- end
491
+ # Generic initialization code.
492
+ # @protected
493
+ def setup(options)
494
+ # Authentication objects
495
+ @auths = options.fetch(:auths, [])
521
496
 
522
- nodes << [host, port]
523
- end
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
524
522
 
525
- nodes
523
+ should_connect = options.fetch(:connect, true)
524
+ connect if should_connect
526
525
  end
527
526
 
528
- private
527
+ ## Configuration helper methods
529
528
 
530
- # Return a socket to the pool.
531
- def checkin(socket)
532
- @connection_mutex.synchronize do
533
- @checked_out.delete(socket)
534
- @queue.signal
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]
535
540
  end
536
- true
537
541
  end
538
542
 
539
- # Adds a new socket to the pool and checks it out.
543
+ private
544
+
545
+ ## Methods for establishing a connection:
546
+
547
+ # If a ConnectionFailure is raised, this method will be called
548
+ # to close the connection and reset connection values.
549
+ # TODO: evaluate whether this method is actually necessary
550
+ def reset_connection
551
+ close
552
+ @primary = nil
553
+ end
554
+
555
+ # Primary is defined as either a master node or a slave if
556
+ # :slave_ok has been set to +true+.
540
557
  #
541
- # This method is called exclusively from #checkout;
542
- # therefore, it runs within a mutex.
543
- def checkout_new_socket
558
+ # If a primary node is discovered, we set the the @host and @port and
559
+ # apply any saved authentication.
560
+ # TODO: simplify
561
+ def is_primary?(config)
562
+ config && (config['ismaster'] == 1 || config['ismaster'] == true) || @slave_ok
563
+ end
564
+
565
+ def check_is_master(node)
544
566
  begin
545
- socket = TCPSocket.new(@host, @port)
546
- socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
547
- rescue => ex
548
- raise ConnectionFailure, "Failed to connect socket: #{ex}"
549
- end
550
- @sockets << socket
551
- @checked_out << socket
552
- socket
553
- end
554
-
555
- # Checks out the first available socket from the pool.
556
- #
557
- # This method is called exclusively from #checkout;
558
- # therefore, it runs within a mutex.
559
- def checkout_existing_socket
560
- socket = (@sockets - @checked_out).first
561
- @checked_out << socket
562
- socket
563
- end
564
-
565
- # Check out an existing socket or create a new socket if the maximum
566
- # pool size has not been exceeded. Otherwise, wait for the next
567
- # available socket.
568
- def checkout
569
- connect_to_master if !connected?
570
- start_time = Time.now
571
- loop do
572
- if (Time.now - start_time) > @timeout
573
- raise ConnectionTimeoutError, "could not obtain connection within " +
574
- "#{@timeout} seconds. The max pool size is currently #{@size}; " +
575
- "consider increasing the pool size or timeout."
576
- end
567
+ host, port = *node
568
+ socket = TCPSocket.new(host, port)
569
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
577
570
 
578
- @connection_mutex.synchronize do
579
- socket = if @checked_out.size < @sockets.size
580
- checkout_existing_socket
581
- elsif @sockets.size < @size
582
- checkout_new_socket
583
- end
571
+ config = self['admin'].command({:ismaster => 1}, :sock => socket)
572
+ rescue OperationFailure, SocketError, SystemCallError, IOError => ex
573
+ close
574
+ ensure
575
+ socket.close if socket
576
+ end
584
577
 
585
- return socket if socket
578
+ config
579
+ end
586
580
 
587
- # Otherwise, wait
588
- if @logger
589
- @logger.warn "Waiting for available connection; #{@checked_out.size} of #{@size} connections checked out."
590
- end
591
- @queue.wait(@connection_mutex)
592
- end
593
- end
581
+ # Set the specified node as primary, and
582
+ # apply any saved authentication credentials.
583
+ def set_primary(node)
584
+ host, port = *node
585
+ @primary = [host, port]
586
+ @primary_pool = Pool.new(self, host, port, :size => @pool_size, :timeout => @timeout)
587
+ apply_saved_authentication
594
588
  end
595
589
 
596
- def receive(sock)
597
- receive_header(sock)
590
+
591
+ ## Low-level connection methods.
592
+
593
+ def receive(sock, expected_response)
594
+ begin
595
+ receive_header(sock, expected_response)
598
596
  number_received, cursor_id = receive_response_header(sock)
599
597
  read_documents(number_received, cursor_id, sock)
598
+ rescue Mongo::ConnectionFailure => ex
599
+ close
600
+ raise ex
601
+ end
600
602
  end
601
603
 
602
- def receive_header(sock)
603
- header = BSON::ByteBuffer.new
604
- header.put_array(receive_message_on_socket(16, sock).unpack("C*"))
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}"
609
+ end
610
+
605
611
  unless header.size == STANDARD_HEADER_SIZE
606
612
  raise "Short read for DB response header: " +
607
613
  "expected #{STANDARD_HEADER_SIZE} bytes, saw #{header.size}"
608
614
  end
609
- header.rewind
610
- size = header.get_int
611
- request_id = header.get_int
612
- response_to = header.get_int
613
- op = header.get_int
615
+ nil
614
616
  end
615
617
 
616
618
  def receive_response_header(sock)
617
- header_buf = BSON::ByteBuffer.new
618
- header_buf.put_array(receive_message_on_socket(RESPONSE_HEADER_SIZE, sock).unpack("C*"))
619
+ header_buf = receive_message_on_socket(RESPONSE_HEADER_SIZE, sock)
619
620
  if header_buf.length != RESPONSE_HEADER_SIZE
620
621
  raise "Short read for DB response header; " +
621
622
  "expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}"
622
623
  end
623
- header_buf.rewind
624
- result_flags = header_buf.get_int
625
- cursor_id = header_buf.get_long
626
- starting_from = header_buf.get_int
627
- number_remaining = header_buf.get_int
624
+ flags, cursor_id_a, cursor_id_b, starting_from, number_remaining = header_buf.unpack('VVVVV')
625
+ check_response_flags(flags)
626
+ cursor_id = (cursor_id_b << 32) + cursor_id_a
628
627
  [number_remaining, cursor_id]
629
628
  end
630
629
 
630
+ def check_response_flags(flags)
631
+ if flags & Mongo::Constants::REPLY_CURSOR_NOT_FOUND != 0
632
+ raise Mongo::OperationFailure, "Query response returned CURSOR_NOT_FOUND. " +
633
+ "Either an invalid cursor was specified, or the cursor may have timed out on the server."
634
+ elsif flags & Mongo::Constants::REPLY_QUERY_FAILURE != 0
635
+ # Getting odd failures when a exception is raised here.
636
+ end
637
+ end
638
+
631
639
  def read_documents(number_received, cursor_id, sock)
632
640
  docs = []
633
641
  number_remaining = number_received
634
642
  while number_remaining > 0 do
635
- buf = BSON::ByteBuffer.new
636
- buf.put_array(receive_message_on_socket(4, sock).unpack("C*"))
637
- buf.rewind
638
- size = buf.get_int
639
- buf.put_array(receive_message_on_socket(size - 4, sock).unpack("C*"), 4)
643
+ buf = receive_message_on_socket(4, sock)
644
+ size = buf.unpack('V')[0]
645
+ buf << receive_message_on_socket(size - 4, sock)
640
646
  number_remaining -= 1
641
- buf.rewind
642
647
  docs << BSON::BSON_CODER.deserialize(buf)
643
648
  end
644
649
  [docs, number_received, cursor_id]
645
650
  end
646
651
 
647
- def last_error_message(db_name)
648
- message = BSON::ByteBuffer.new
652
+ # Constructs a getlasterror message. This method is used exclusively by
653
+ # Connection#send_message_with_safe_check.
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)
649
657
  message.put_int(0)
650
658
  BSON::BSON_RUBY.serialize_cstr(message, "#{db_name}.$cmd")
651
659
  message.put_int(0)
652
660
  message.put_int(-1)
653
- message.put_array(BSON::BSON_CODER.serialize({:getlasterror => 1}, false).unpack("C*"))
654
- add_message_headers(Mongo::Constants::OP_QUERY, message)
661
+ cmd = BSON::OrderedHash.new
662
+ cmd[:getlasterror] = 1
663
+ if opts.is_a?(Hash)
664
+ opts.assert_valid_keys(:w, :wtimeout, :fsync)
665
+ cmd.merge!(opts)
666
+ end
667
+ message.put_binary(BSON::BSON_CODER.serialize(cmd, false).to_s)
668
+ nil
655
669
  end
656
670
 
657
671
  # Prepares a message for transmission to MongoDB by
658
672
  # constructing a valid message header.
659
- def add_message_headers(operation, message)
660
- headers = BSON::ByteBuffer.new
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)
678
+ headers = [
679
+ # Message size.
680
+ 16 + message.size,
661
681
 
662
- # Message size.
663
- headers.put_int(16 + message.size)
682
+ # Unique request id.
683
+ request_id = get_request_id,
664
684
 
665
- # Unique request id.
666
- headers.put_int(get_request_id)
685
+ # Response id.
686
+ 0,
667
687
 
668
- # Response id.
669
- headers.put_int(0)
688
+ # Opcode.
689
+ operation
690
+ ].pack('VVVV')
670
691
 
671
- # Opcode.
672
- headers.put_int(operation)
673
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
674
706
  end
675
707
 
676
708
  # Low-level method for sending a message on a socket.
677
709
  # Requires a packed message and an available socket,
710
+ #
711
+ # @return [Integer] number of bytes sent
678
712
  def send_message_on_socket(packed_message, socket)
679
713
  begin
680
- socket.send(packed_message, 0)
714
+ total_bytes_sent = socket.send(packed_message, 0)
715
+ if total_bytes_sent != packed_message.size
716
+ packed_message.slice!(0, total_bytes_sent)
717
+ while packed_message.size > 0
718
+ byte_sent = socket.send(packed_message, 0)
719
+ total_bytes_sent += byte_sent
720
+ packed_message.slice!(0, byte_sent)
721
+ end
722
+ end
723
+ total_bytes_sent
681
724
  rescue => ex
682
725
  close
683
726
  raise ConnectionFailure, "Operation failed with the following exception: #{ex}"
@@ -687,12 +730,16 @@ module Mongo
687
730
  # Low-level method for receiving data from socket.
688
731
  # Requires length and an available socket.
689
732
  def receive_message_on_socket(length, socket)
690
- message = ""
691
733
  begin
692
- while message.length < length do
693
- chunk = socket.recv(length - message.length)
694
- raise ConnectionFailure, "connection closed" unless chunk.length > 0
695
- message += chunk
734
+ message = socket.read(length)
735
+ raise ConnectionFailure, "connection closed" unless message.length > 0
736
+ if message.length < length
737
+ chunk = new_binary_string
738
+ while message.length < length
739
+ socket.read(length - message.length, chunk)
740
+ raise ConnectionFailure, "connection closed" unless chunk.length > 0
741
+ message << chunk
742
+ end
696
743
  end
697
744
  rescue => ex
698
745
  close
@@ -700,5 +747,17 @@ module Mongo
700
747
  end
701
748
  message
702
749
  end
750
+
751
+ if defined?(Encoding)
752
+ BINARY_ENCODING = Encoding.find("binary")
753
+
754
+ def new_binary_string
755
+ "".force_encoding(BINARY_ENCODING)
756
+ end
757
+ else
758
+ def new_binary_string
759
+ ""
760
+ end
761
+ end
703
762
  end
704
763
  end