jonbell-mongo 1.3.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. data/LICENSE.txt +190 -0
  2. data/README.md +333 -0
  3. data/Rakefile +215 -0
  4. data/bin/mongo_console +21 -0
  5. data/docs/CREDITS.md +123 -0
  6. data/docs/FAQ.md +116 -0
  7. data/docs/GridFS.md +158 -0
  8. data/docs/HISTORY.md +263 -0
  9. data/docs/RELEASES.md +33 -0
  10. data/docs/REPLICA_SETS.md +72 -0
  11. data/docs/TUTORIAL.md +247 -0
  12. data/docs/WRITE_CONCERN.md +28 -0
  13. data/lib/mongo.rb +97 -0
  14. data/lib/mongo/collection.rb +895 -0
  15. data/lib/mongo/connection.rb +926 -0
  16. data/lib/mongo/cursor.rb +474 -0
  17. data/lib/mongo/db.rb +617 -0
  18. data/lib/mongo/exceptions.rb +71 -0
  19. data/lib/mongo/gridfs/grid.rb +107 -0
  20. data/lib/mongo/gridfs/grid_ext.rb +57 -0
  21. data/lib/mongo/gridfs/grid_file_system.rb +146 -0
  22. data/lib/mongo/gridfs/grid_io.rb +485 -0
  23. data/lib/mongo/gridfs/grid_io_fix.rb +38 -0
  24. data/lib/mongo/repl_set_connection.rb +356 -0
  25. data/lib/mongo/util/conversions.rb +89 -0
  26. data/lib/mongo/util/core_ext.rb +60 -0
  27. data/lib/mongo/util/pool.rb +177 -0
  28. data/lib/mongo/util/server_version.rb +71 -0
  29. data/lib/mongo/util/support.rb +82 -0
  30. data/lib/mongo/util/uri_parser.rb +185 -0
  31. data/mongo.gemspec +34 -0
  32. data/test/auxillary/1.4_features.rb +166 -0
  33. data/test/auxillary/authentication_test.rb +68 -0
  34. data/test/auxillary/autoreconnect_test.rb +41 -0
  35. data/test/auxillary/fork_test.rb +30 -0
  36. data/test/auxillary/repl_set_auth_test.rb +58 -0
  37. data/test/auxillary/slave_connection_test.rb +36 -0
  38. data/test/auxillary/threaded_authentication_test.rb +101 -0
  39. data/test/bson/binary_test.rb +15 -0
  40. data/test/bson/bson_test.rb +654 -0
  41. data/test/bson/byte_buffer_test.rb +208 -0
  42. data/test/bson/hash_with_indifferent_access_test.rb +38 -0
  43. data/test/bson/json_test.rb +17 -0
  44. data/test/bson/object_id_test.rb +154 -0
  45. data/test/bson/ordered_hash_test.rb +210 -0
  46. data/test/bson/timestamp_test.rb +24 -0
  47. data/test/collection_test.rb +910 -0
  48. data/test/connection_test.rb +324 -0
  49. data/test/conversions_test.rb +119 -0
  50. data/test/cursor_fail_test.rb +75 -0
  51. data/test/cursor_message_test.rb +43 -0
  52. data/test/cursor_test.rb +483 -0
  53. data/test/db_api_test.rb +738 -0
  54. data/test/db_connection_test.rb +15 -0
  55. data/test/db_test.rb +315 -0
  56. data/test/grid_file_system_test.rb +259 -0
  57. data/test/grid_io_test.rb +209 -0
  58. data/test/grid_test.rb +258 -0
  59. data/test/load/thin/load.rb +24 -0
  60. data/test/load/unicorn/load.rb +23 -0
  61. data/test/replica_sets/connect_test.rb +112 -0
  62. data/test/replica_sets/connection_string_test.rb +32 -0
  63. data/test/replica_sets/count_test.rb +35 -0
  64. data/test/replica_sets/insert_test.rb +53 -0
  65. data/test/replica_sets/pooled_insert_test.rb +55 -0
  66. data/test/replica_sets/query_secondaries.rb +108 -0
  67. data/test/replica_sets/query_test.rb +51 -0
  68. data/test/replica_sets/replication_ack_test.rb +66 -0
  69. data/test/replica_sets/rs_test_helper.rb +27 -0
  70. data/test/safe_test.rb +68 -0
  71. data/test/support/hash_with_indifferent_access.rb +186 -0
  72. data/test/support/keys.rb +45 -0
  73. data/test/support_test.rb +18 -0
  74. data/test/test_helper.rb +102 -0
  75. data/test/threading/threading_with_large_pool_test.rb +90 -0
  76. data/test/threading_test.rb +87 -0
  77. data/test/tools/auth_repl_set_manager.rb +14 -0
  78. data/test/tools/repl_set_manager.rb +266 -0
  79. data/test/unit/collection_test.rb +130 -0
  80. data/test/unit/connection_test.rb +85 -0
  81. data/test/unit/cursor_test.rb +109 -0
  82. data/test/unit/db_test.rb +94 -0
  83. data/test/unit/grid_test.rb +49 -0
  84. data/test/unit/pool_test.rb +9 -0
  85. data/test/unit/repl_set_connection_test.rb +59 -0
  86. data/test/unit/safe_test.rb +125 -0
  87. data/test/uri_test.rb +91 -0
  88. metadata +224 -0
@@ -0,0 +1,926 @@
1
+ # encoding: UTF-8
2
+
3
+ # --
4
+ # Copyright (C) 2008-2011 10gen Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ # ++
18
+
19
+ require 'set'
20
+ require 'socket'
21
+ require 'thread'
22
+
23
+ module Mongo
24
+
25
+ # Instantiates and manages connections to MongoDB.
26
+ class Connection
27
+ TCPSocket = ::TCPSocket
28
+ Mutex = ::Mutex
29
+ ConditionVariable = ::ConditionVariable
30
+
31
+ # Abort connections if a ConnectionError is raised.
32
+ Thread.abort_on_exception = true
33
+
34
+ DEFAULT_PORT = 27017
35
+ STANDARD_HEADER_SIZE = 16
36
+ RESPONSE_HEADER_SIZE = 20
37
+
38
+ attr_reader :logger, :size, :auths, :primary, :safe, :primary_pool, :host_to_try, :pool_size
39
+
40
+ # Counter for generating unique request ids.
41
+ @@current_request_id = 0
42
+
43
+ # Create a connection to single MongoDB instance.
44
+ #
45
+ # You may specify whether connection to slave is permitted.
46
+ # In all cases, the default host is "localhost" and the default port is 27017.
47
+ #
48
+ # If you're connecting to a replica set, you'll need to use ReplSetConnection.new instead.
49
+ #
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.
54
+ #
55
+ # @param [String, Hash] host.
56
+ # @param [Integer] port specify a port number here if only one host is being specified.
57
+ #
58
+ # @option opts [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.
62
+ # @option opts [Boolean] :slave_ok (false) Must be set to +true+ when connecting
63
+ # to a single, slave node.
64
+ # @option opts [Logger, #debug] :logger (nil) A Logger instance for debugging driver ops. Note that
65
+ # logging negatively impacts performance; therefore, it should not be used for high-performance apps.
66
+ # @option opts [Integer] :pool_size (1) The maximum number of socket connections allowed per
67
+ # connection pool. Note: this setting is relevant only for multi-threaded applications.
68
+ # @option opts [Float] :timeout (5.0) When all of the connections a pool are checked out,
69
+ # this is the number of seconds to wait for a new connection to be released before throwing an exception.
70
+ # Note: this setting is relevant only for multi-threaded applications (which in Ruby are rare).
71
+ # @option opts [Float] :op_timeout (nil) The number of seconds to wait for a read operation to time out.
72
+ # Disabled by default.
73
+ # @option opts [Float] :connect_timeout (nil) The number of seconds to wait before timing out a
74
+ # connection attempt.
75
+ #
76
+ # @example localhost, 27017
77
+ # Connection.new
78
+ #
79
+ # @example localhost, 27017
80
+ # Connection.new("localhost")
81
+ #
82
+ # @example localhost, 3000, max 5 connections, with max 5 seconds of wait time.
83
+ # Connection.new("localhost", 3000, :pool_size => 5, :timeout => 5)
84
+ #
85
+ # @example localhost, 3000, where this node may be a slave
86
+ # Connection.new("localhost", 3000, :slave_ok => true)
87
+ #
88
+ # @see http://api.mongodb.org/ruby/current/file.REPLICA_SETS.html Replica sets in Ruby
89
+ #
90
+ # @raise [ReplicaSetConnectionError] This is raised if a replica set name is specified and the
91
+ # driver fails to connect to a replica set with that name.
92
+ #
93
+ # @core connections
94
+ def initialize(host=nil, port=nil, opts={})
95
+ @host_to_try = format_pair(host, port)
96
+
97
+ # Host and port of current master.
98
+ @host = @port = nil
99
+
100
+ # slave_ok can be true only if one node is specified
101
+ @slave_ok = opts[:slave_ok]
102
+
103
+ setup(opts)
104
+ end
105
+
106
+ # DEPRECATED
107
+ #
108
+ # Initialize a connection to a MongoDB replica set using an array of seed nodes.
109
+ #
110
+ # The seed nodes specified will be used on the initial connection to the replica set, but note
111
+ # that this list of nodes will be replced by the list of canonical nodes returned by running the
112
+ # is_master command on the replica set.
113
+ #
114
+ # @param nodes [Array] An array of arrays, each of which specifies a host and port.
115
+ # @param opts [Hash] Any of the available options that can be passed to Connection.new.
116
+ #
117
+ # @option opts [String] :rs_name (nil) The name of the replica set to connect to. An exception will be
118
+ # raised if unable to connect to a replica set with this name.
119
+ # @option opts [Boolean] :read_secondary (false) When true, this connection object will pick a random slave
120
+ # to send reads to.
121
+ #
122
+ # @example
123
+ # Connection.multi([["db1.example.com", 27017], ["db2.example.com", 27017]])
124
+ #
125
+ # @example This connection will read from a random secondary node.
126
+ # Connection.multi([["db1.example.com", 27017], ["db2.example.com", 27017], ["db3.example.com", 27017]],
127
+ # :read_secondary => true)
128
+ #
129
+ # @return [Mongo::Connection]
130
+ #
131
+ # @deprecated
132
+ def self.multi(nodes, opts={})
133
+ warn "Connection.multi is now deprecated. Please use ReplSetConnection.new instead."
134
+
135
+ nodes << opts
136
+ ReplSetConnection.new(*nodes)
137
+ end
138
+
139
+ # Initialize a connection to MongoDB using the MongoDB URI spec:
140
+ #
141
+ # @param uri [String]
142
+ # A string of the format mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/database]
143
+ #
144
+ # @param opts Any of the options available for Connection.new
145
+ #
146
+ # @return [Mongo::Connection, Mongo::ReplSetConnection]
147
+ def self.from_uri(string, extra_opts={})
148
+ uri = URIParser.new(string)
149
+ opts = uri.connection_options
150
+ opts.merge!(extra_opts)
151
+
152
+ if uri.nodes.length == 1
153
+ opts.merge!({:auths => uri.auths})
154
+ Connection.new(uri.nodes[0][0], uri.nodes[0][1], opts)
155
+ elsif uri.nodes.length > 1
156
+ nodes = uri.nodes.clone
157
+ nodes_with_opts = nodes << opts
158
+ ReplSetConnection.new(*nodes_with_opts)
159
+ else
160
+ raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node."
161
+ end
162
+ end
163
+
164
+ # The host name used for this connection.
165
+ #
166
+ # @return [String]
167
+ def host
168
+ @primary_pool.host
169
+ end
170
+
171
+ # The port used for this connection.
172
+ #
173
+ # @return [Integer]
174
+ def port
175
+ @primary_pool.port
176
+ end
177
+
178
+ # Fsync, then lock the mongod process against writes. Use this to get
179
+ # the datafiles in a state safe for snapshotting, backing up, etc.
180
+ #
181
+ # @return [BSON::OrderedHash] the command response
182
+ def lock!
183
+ cmd = BSON::OrderedHash.new
184
+ cmd[:fsync] = 1
185
+ cmd[:lock] = true
186
+ self['admin'].command(cmd)
187
+ end
188
+
189
+ # Is this database locked against writes?
190
+ #
191
+ # @return [Boolean]
192
+ def locked?
193
+ self['admin']['$cmd.sys.inprog'].find_one['fsyncLock'] == 1
194
+ end
195
+
196
+ # Unlock a previously fsync-locked mongod process.
197
+ #
198
+ # @return [BSON::OrderedHash] command response
199
+ def unlock!
200
+ self['admin']['$cmd.sys.unlock'].find_one
201
+ end
202
+
203
+ # Apply each of the saved database authentications.
204
+ #
205
+ # @return [Boolean] returns true if authentications exist and succeeed, false
206
+ # if none exists.
207
+ #
208
+ # @raise [AuthenticationError] raises an exception if any one
209
+ # authentication fails.
210
+ def apply_saved_authentication(opts={})
211
+ return false if @auths.empty?
212
+ @auths.each do |auth|
213
+ self[auth['db_name']].issue_authentication(auth['username'], auth['password'], false,
214
+ :socket => opts[:socket])
215
+ end
216
+ true
217
+ end
218
+
219
+ # Save an authentication to this connection. When connecting,
220
+ # the connection will attempt to re-authenticate on every db
221
+ # specificed in the list of auths. This method is called automatically
222
+ # by DB#authenticate.
223
+ #
224
+ # Note: this method will not actually issue an authentication command. To do that,
225
+ # either run Connection#apply_saved_authentication or DB#authenticate.
226
+ #
227
+ # @param [String] db_name
228
+ # @param [String] username
229
+ # @param [String] password
230
+ #
231
+ # @return [Hash] a hash representing the authentication just added.
232
+ def add_auth(db_name, username, password)
233
+ remove_auth(db_name)
234
+ auth = {}
235
+ auth['db_name'] = db_name
236
+ auth['username'] = username
237
+ auth['password'] = password
238
+ @auths << auth
239
+ auth
240
+ end
241
+
242
+ # Remove a saved authentication for this connection.
243
+ #
244
+ # @param [String] db_name
245
+ #
246
+ # @return [Boolean]
247
+ def remove_auth(db_name)
248
+ return unless @auths
249
+ if @auths.reject! { |a| a['db_name'] == db_name }
250
+ true
251
+ else
252
+ false
253
+ end
254
+ end
255
+
256
+ # Remove all authenication information stored in this connection.
257
+ #
258
+ # @return [true] this operation return true because it always succeeds.
259
+ def clear_auths
260
+ @auths = []
261
+ true
262
+ end
263
+
264
+ def authenticate_pools
265
+ @primary_pool.authenticate_existing
266
+ end
267
+
268
+ def logout_pools(db)
269
+ @primary_pool.logout_existing(db)
270
+ end
271
+
272
+ # Return a hash with all database names
273
+ # and their respective sizes on disk.
274
+ #
275
+ # @return [Hash]
276
+ def database_info
277
+ doc = self['admin'].command({:listDatabases => 1})
278
+ doc['databases'].each_with_object({}) do |db, info|
279
+ info[db['name']] = db['sizeOnDisk'].to_i
280
+ end
281
+ end
282
+
283
+ # Return an array of database names.
284
+ #
285
+ # @return [Array]
286
+ def database_names
287
+ database_info.keys
288
+ end
289
+
290
+ # Return a database with the given name.
291
+ # See DB#new for valid options hash parameters.
292
+ #
293
+ # @param [String] db_name a valid database name.
294
+ # @param [Hash] opts options to be passed to the DB constructor.
295
+ #
296
+ # @return [Mongo::DB]
297
+ #
298
+ # @core databases db-instance_method
299
+ def db(db_name, opts={})
300
+ DB.new(db_name, self, opts)
301
+ end
302
+
303
+ # Shortcut for returning a database. Use DB#db to accept options.
304
+ #
305
+ # @param [String] db_name a valid database name.
306
+ #
307
+ # @return [Mongo::DB]
308
+ #
309
+ # @core databases []-instance_method
310
+ def [](db_name)
311
+ DB.new(db_name, self, :safe => @safe)
312
+ end
313
+
314
+ # Drop a database.
315
+ #
316
+ # @param [String] name name of an existing database.
317
+ def drop_database(name)
318
+ self[name].command(:dropDatabase => 1)
319
+ end
320
+
321
+ # Copy the database +from+ to +to+ on localhost. The +from+ database is
322
+ # assumed to be on localhost, but an alternate host can be specified.
323
+ #
324
+ # @param [String] from name of the database to copy from.
325
+ # @param [String] to name of the database to copy to.
326
+ # @param [String] from_host host of the 'from' database.
327
+ # @param [String] username username for authentication against from_db (>=1.3.x).
328
+ # @param [String] password password for authentication against from_db (>=1.3.x).
329
+ def copy_database(from, to, from_host="localhost", username=nil, password=nil)
330
+ oh = BSON::OrderedHash.new
331
+ oh[:copydb] = 1
332
+ oh[:fromhost] = from_host
333
+ oh[:fromdb] = from
334
+ oh[:todb] = to
335
+ if username || password
336
+ unless username && password
337
+ raise MongoArgumentError, "Both username and password must be supplied for authentication."
338
+ end
339
+ nonce_cmd = BSON::OrderedHash.new
340
+ nonce_cmd[:copydbgetnonce] = 1
341
+ nonce_cmd[:fromhost] = from_host
342
+ result = self["admin"].command(nonce_cmd)
343
+ oh[:nonce] = result["nonce"]
344
+ oh[:username] = username
345
+ oh[:key] = Mongo::Support.auth_key(username, password, oh[:nonce])
346
+ end
347
+ self["admin"].command(oh)
348
+ end
349
+
350
+ # Checks if a server is alive. This command will return immediately
351
+ # even if the server is in a lock.
352
+ #
353
+ # @return [Hash]
354
+ def ping
355
+ self["admin"].command({:ping => 1})
356
+ end
357
+
358
+ # Get the build information for the current connection.
359
+ #
360
+ # @return [Hash]
361
+ def server_info
362
+ self["admin"].command({:buildinfo => 1})
363
+ end
364
+
365
+
366
+ # Get the build version of the current server.
367
+ #
368
+ # @return [Mongo::ServerVersion]
369
+ # object allowing easy comparability of version.
370
+ def server_version
371
+ ServerVersion.new(server_info["version"])
372
+ end
373
+
374
+ # Is it okay to connect to a slave?
375
+ #
376
+ # @return [Boolean]
377
+ def slave_ok?
378
+ @slave_ok
379
+ end
380
+
381
+ # Send a message to MongoDB, adding the necessary headers.
382
+ #
383
+ # @param [Integer] operation a MongoDB opcode.
384
+ # @param [BSON::ByteBuffer] message a message to send to the database.
385
+ #
386
+ # @option opts [Symbol] :connection (:writer) The connection to which
387
+ # this message should be sent. Valid options are :writer and :reader.
388
+ #
389
+ # @return [Integer] number of bytes sent
390
+ def send_message(operation, message, opts={})
391
+ if opts.is_a?(String)
392
+ warn "Connection#send_message no longer takes a string log message. " +
393
+ "Logging is now handled within the Collection and Cursor classes."
394
+ opts = {}
395
+ end
396
+
397
+ connection = opts.fetch(:connection, :writer)
398
+
399
+ begin
400
+ add_message_headers(message, operation)
401
+ packed_message = message.to_s
402
+
403
+ if connection == :writer
404
+ socket = checkout_writer
405
+ else
406
+ socket = checkout_reader
407
+ end
408
+
409
+ send_message_on_socket(packed_message, socket)
410
+ ensure
411
+ if connection == :writer
412
+ checkin_writer(socket)
413
+ else
414
+ checkin_reader(socket)
415
+ end
416
+ end
417
+ end
418
+
419
+ # Sends a message to the database, waits for a response, and raises
420
+ # an exception if the operation has failed.
421
+ #
422
+ # @param [Integer] operation a MongoDB opcode.
423
+ # @param [BSON::ByteBuffer] message a message to send to the database.
424
+ # @param [String] db_name the name of the database. used on call to get_last_error.
425
+ # @param [Hash] last_error_params parameters to be sent to getLastError. See DB#error for
426
+ # available options.
427
+ #
428
+ # @see DB#get_last_error for valid last error params.
429
+ #
430
+ # @return [Hash] The document returned by the call to getlasterror.
431
+ def send_message_with_safe_check(operation, message, db_name, log_message=nil, last_error_params=false)
432
+ docs = num_received = cursor_id = ''
433
+ add_message_headers(message, operation)
434
+
435
+ last_error_message = BSON::ByteBuffer.new
436
+ build_last_error_message(last_error_message, db_name, last_error_params)
437
+ last_error_id = add_message_headers(last_error_message, Mongo::Constants::OP_QUERY)
438
+
439
+ packed_message = message.append!(last_error_message).to_s
440
+ begin
441
+ sock = checkout_writer
442
+ @safe_mutexes[sock].synchronize do
443
+ send_message_on_socket(packed_message, sock)
444
+ docs, num_received, cursor_id = receive(sock, last_error_id)
445
+ end
446
+ ensure
447
+ checkin_writer(sock)
448
+ end
449
+
450
+ if num_received == 1 && (error = docs[0]['err'] || docs[0]['errmsg'])
451
+ close if error == "not master"
452
+ error = "wtimeout" if error == "timeout"
453
+ raise Mongo::OperationFailure, docs[0]['code'].to_s + ': ' + error
454
+ end
455
+
456
+ docs[0]
457
+ end
458
+
459
+ # Sends a message to the database and waits for the response.
460
+ #
461
+ # @param [Integer] operation a MongoDB opcode.
462
+ # @param [BSON::ByteBuffer] message a message to send to the database.
463
+ # @param [String] log_message this is currently a no-op and will be removed.
464
+ # @param [Socket] socket a socket to use in lieu of checking out a new one.
465
+ # @param [Boolean] command (false) indicate whether this is a command. If this is a command,
466
+ # the message will be sent to the primary node.
467
+ #
468
+ # @return [Array]
469
+ # An array whose indexes include [0] documents returned, [1] number of document received,
470
+ # and [3] a cursor_id.
471
+ def receive_message(operation, message, log_message=nil, socket=nil, command=false)
472
+ request_id = add_message_headers(message, operation)
473
+ packed_message = message.to_s
474
+ begin
475
+ if socket
476
+ sock = socket
477
+ checkin = false
478
+ else
479
+ sock = (command ? checkout_writer : checkout_reader)
480
+ checkin = true
481
+ end
482
+
483
+ result = ''
484
+ @safe_mutexes[sock].synchronize do
485
+ send_message_on_socket(packed_message, sock)
486
+ result = receive(sock, request_id)
487
+ end
488
+ ensure
489
+ if checkin
490
+ command ? checkin_writer(sock) : checkin_reader(sock)
491
+ end
492
+ end
493
+ result
494
+ end
495
+
496
+ # Create a new socket and attempt to connect to master.
497
+ # If successful, sets host and port to master and returns the socket.
498
+ #
499
+ # If connecting to a replica set, this method will replace the
500
+ # initially-provided seed list with any nodes known to the set.
501
+ #
502
+ # @raise [ConnectionFailure] if unable to connect to any host or port.
503
+ def connect
504
+ close
505
+
506
+ config = check_is_master(@host_to_try)
507
+ if config
508
+ if config['ismaster'] == 1 || config['ismaster'] == true
509
+ @read_primary = true
510
+ elsif @slave_ok
511
+ @read_primary = false
512
+ end
513
+
514
+ set_primary(@host_to_try)
515
+ end
516
+
517
+ if connected?
518
+ BSON::BSON_CODER.update_max_bson_size(self)
519
+ else
520
+ raise ConnectionFailure, "Failed to connect to a master node at #{@host_to_try[0]}:#{@host_to_try[1]}"
521
+ end
522
+ end
523
+ alias :reconnect :connect
524
+
525
+ def connecting?
526
+ @nodes_to_try.length > 0
527
+ end
528
+
529
+ # It's possible that we defined connected as all nodes being connected???
530
+ # NOTE: Do check if this needs to be more stringent.
531
+ # Probably not since if any node raises a connection failure, all nodes will be closed.
532
+ def connected?
533
+ @primary_pool && @primary_pool.host && @primary_pool.port
534
+ end
535
+
536
+ # Determine if the connection is active. In a normal case the *server_info* operation
537
+ # will be performed without issues, but if the connection was dropped by the server or
538
+ # for some reason the sockets are unsynchronized, a ConnectionFailure will be raised and
539
+ # the return will be false.
540
+ #
541
+ # @return [Boolean]
542
+ def active?
543
+ return false unless connected?
544
+
545
+ ping
546
+ true
547
+
548
+ rescue ConnectionFailure
549
+ false
550
+ end
551
+
552
+ # Determine whether we're reading from a primary node. If false,
553
+ # this connection connects to a secondary node and @slave_ok is true.
554
+ #
555
+ # @return [Boolean]
556
+ def read_primary?
557
+ @read_primary
558
+ end
559
+ alias :primary? :read_primary?
560
+
561
+ # Close the connection to the database.
562
+ def close
563
+ @primary_pool.close if @primary_pool
564
+ @primary_pool = nil
565
+ @primary = nil
566
+ end
567
+
568
+ # Returns the maximum BSON object size as returned by the core server.
569
+ # Use the 4MB default when the server doesn't report this.
570
+ #
571
+ # @return [Integer]
572
+ def max_bson_size
573
+ config = self['admin'].command({:ismaster => 1})
574
+ config['maxBsonObjectSize'] || Mongo::DEFAULT_MAX_BSON_SIZE
575
+ end
576
+
577
+ # Checkout a socket for reading (i.e., a secondary node).
578
+ # Note: this is overridden in ReplSetConnection.
579
+ def checkout_reader
580
+ connect unless connected?
581
+ @primary_pool.checkout
582
+ end
583
+
584
+ # Checkout a socket for writing (i.e., a primary node).
585
+ # Note: this is overridden in ReplSetConnection.
586
+ def checkout_writer
587
+ connect unless connected?
588
+ @primary_pool.checkout
589
+ end
590
+
591
+ # Checkin a socket used for reading.
592
+ # Note: this is overridden in ReplSetConnection.
593
+ def checkin_reader(socket)
594
+ if @primary_pool
595
+ @primary_pool.checkin(socket)
596
+ end
597
+ end
598
+
599
+ # Checkin a socket used for writing.
600
+ # Note: this is overridden in ReplSetConnection.
601
+ def checkin_writer(socket)
602
+ if @primary_pool
603
+ @primary_pool.checkin(socket)
604
+ end
605
+ end
606
+
607
+ # Execute the block and log the operation described by name
608
+ # and payload.
609
+ # TODO: Not sure if this should take a block.
610
+ def instrument(name, payload = {}, &blk)
611
+ res = yield
612
+ log_operation(name, payload)
613
+ res
614
+ end
615
+
616
+ protected
617
+
618
+ # Generic initialization code.
619
+ def setup(opts)
620
+ # Authentication objects
621
+ @auths = opts.fetch(:auths, [])
622
+
623
+ # Lock for request ids.
624
+ @id_lock = Mutex.new
625
+
626
+ # Pool size and timeout.
627
+ @pool_size = opts[:pool_size] || 1
628
+ @timeout = opts[:timeout] || 5.0
629
+
630
+ # Timeout on socket read operation.
631
+ @op_timeout = opts[:op_timeout] || nil
632
+
633
+ # Timeout on socket connect.
634
+ @connect_timeout = opts[:connect_timeout] || nil
635
+
636
+
637
+ # Mutex for synchronizing pool access
638
+ @connection_mutex = Mutex.new
639
+
640
+ # Global safe option. This is false by default.
641
+ @safe = opts[:safe] || false
642
+
643
+ # Create a mutex when a new key, in this case a socket,
644
+ # is added to the hash.
645
+ @safe_mutexes = Hash.new { |h, k| h[k] = Mutex.new }
646
+
647
+ # Condition variable for signal and wait
648
+ @queue = ConditionVariable.new
649
+
650
+ # Connection pool for primay node
651
+ @primary = nil
652
+ @primary_pool = nil
653
+
654
+ @logger = opts[:logger] || nil
655
+
656
+ if @logger
657
+ @logger.debug("MongoDB logging. Please note that logging negatively impacts performance " +
658
+ "and should be disabled for high-performance production apps.")
659
+ end
660
+
661
+ should_connect = opts.fetch(:connect, true)
662
+ connect if should_connect
663
+ end
664
+
665
+ ## Configuration helper methods
666
+
667
+ # Returns a host-port pair.
668
+ #
669
+ # @return [Array]
670
+ #
671
+ # @private
672
+ def format_pair(host, port)
673
+ case host
674
+ when String
675
+ [host, port ? port.to_i : DEFAULT_PORT]
676
+ when nil
677
+ ['localhost', DEFAULT_PORT]
678
+ end
679
+ end
680
+
681
+ ## Logging methods
682
+
683
+ def log_operation(name, payload)
684
+ return unless @logger
685
+ msg = "#{payload[:database]}['#{payload[:collection]}'].#{name}("
686
+ msg += payload.values_at(:selector, :document, :documents, :fields ).compact.map(&:inspect).join(', ') + ")"
687
+ msg += ".skip(#{payload[:skip]})" if payload[:skip]
688
+ msg += ".limit(#{payload[:limit]})" if payload[:limit]
689
+ msg += ".sort(#{payload[:order]})" if payload[:order]
690
+ @logger.debug "MONGODB #{msg}"
691
+ end
692
+
693
+ private
694
+
695
+ ## Methods for establishing a connection:
696
+
697
+ # If a ConnectionFailure is raised, this method will be called
698
+ # to close the connection and reset connection values.
699
+ # TODO: evaluate whether this method is actually necessary
700
+ def reset_connection
701
+ close
702
+ end
703
+
704
+ def check_is_master(node)
705
+ begin
706
+ host, port = *node
707
+ socket = nil
708
+
709
+ if @connect_timeout
710
+ Mongo::TimeoutHandler.timeout(@connect_timeout, OperationTimeout) do
711
+ socket = TCPSocket.new(host, port)
712
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
713
+ end
714
+ else
715
+ socket = TCPSocket.new(host, port)
716
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
717
+ end
718
+
719
+ config = self['admin'].command({:ismaster => 1}, :socket => socket)
720
+ rescue OperationFailure, SocketError, SystemCallError, IOError => ex
721
+ close
722
+ ensure
723
+ socket.close if socket
724
+ end
725
+
726
+ config
727
+ end
728
+
729
+ # Set the specified node as primary.
730
+ def set_primary(node)
731
+ host, port = *node
732
+ @primary = [host, port]
733
+ @primary_pool = Pool.new(self, host, port, :size => @pool_size, :timeout => @timeout)
734
+ end
735
+
736
+ ## Low-level connection methods.
737
+
738
+ def receive(sock, expected_response)
739
+ begin
740
+ receive_header(sock, expected_response)
741
+ number_received, cursor_id = receive_response_header(sock)
742
+ read_documents(number_received, cursor_id, sock)
743
+ rescue Mongo::ConnectionFailure => ex
744
+ close
745
+ raise ex
746
+ end
747
+ end
748
+
749
+ def receive_header(sock, expected_response)
750
+ header = receive_message_on_socket(16, sock)
751
+ size, request_id, response_to = header.unpack('VVV')
752
+ if expected_response != response_to
753
+ raise Mongo::ConnectionFailure, "Expected response #{expected_response} but got #{response_to}"
754
+ end
755
+
756
+ unless header.size == STANDARD_HEADER_SIZE
757
+ raise "Short read for DB response header: " +
758
+ "expected #{STANDARD_HEADER_SIZE} bytes, saw #{header.size}"
759
+ end
760
+ nil
761
+ end
762
+
763
+ def receive_response_header(sock)
764
+ header_buf = receive_message_on_socket(RESPONSE_HEADER_SIZE, sock)
765
+ if header_buf.length != RESPONSE_HEADER_SIZE
766
+ raise "Short read for DB response header; " +
767
+ "expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}"
768
+ end
769
+ flags, cursor_id_a, cursor_id_b, starting_from, number_remaining = header_buf.unpack('VVVVV')
770
+ check_response_flags(flags)
771
+ cursor_id = (cursor_id_b << 32) + cursor_id_a
772
+ [number_remaining, cursor_id]
773
+ end
774
+
775
+ def check_response_flags(flags)
776
+ if flags & Mongo::Constants::REPLY_CURSOR_NOT_FOUND != 0
777
+ raise Mongo::OperationFailure, "Query response returned CURSOR_NOT_FOUND. " +
778
+ "Either an invalid cursor was specified, or the cursor may have timed out on the server."
779
+ elsif flags & Mongo::Constants::REPLY_QUERY_FAILURE != 0
780
+ # Getting odd failures when a exception is raised here.
781
+ end
782
+ end
783
+
784
+ def read_documents(number_received, cursor_id, sock)
785
+ docs = []
786
+ number_remaining = number_received
787
+ while number_remaining > 0 do
788
+ buf = receive_message_on_socket(4, sock)
789
+ size = buf.unpack('V')[0]
790
+ buf << receive_message_on_socket(size - 4, sock)
791
+ number_remaining -= 1
792
+ docs << BSON::BSON_CODER.deserialize(buf)
793
+ end
794
+ [docs, number_received, cursor_id]
795
+ end
796
+
797
+ # Constructs a getlasterror message. This method is used exclusively by
798
+ # Connection#send_message_with_safe_check.
799
+ #
800
+ # Because it modifies message by reference, we don't need to return it.
801
+ def build_last_error_message(message, db_name, opts)
802
+ message.put_int(0)
803
+ BSON::BSON_RUBY.serialize_cstr(message, "#{db_name}.$cmd")
804
+ message.put_int(0)
805
+ message.put_int(-1)
806
+ cmd = BSON::OrderedHash.new
807
+ cmd[:getlasterror] = 1
808
+ if opts.is_a?(Hash)
809
+ opts.assert_valid_keys(:w, :wtimeout, :fsync)
810
+ cmd.merge!(opts)
811
+ end
812
+ message.put_binary(BSON::BSON_CODER.serialize(cmd, false).to_s)
813
+ nil
814
+ end
815
+
816
+ # Prepares a message for transmission to MongoDB by
817
+ # constructing a valid message header.
818
+ #
819
+ # Note: this method modifies message by reference.
820
+ #
821
+ # @return [Integer] the request id used in the header
822
+ def add_message_headers(message, operation)
823
+ headers = [
824
+ # Message size.
825
+ 16 + message.size,
826
+
827
+ # Unique request id.
828
+ request_id = get_request_id,
829
+
830
+ # Response id.
831
+ 0,
832
+
833
+ # Opcode.
834
+ operation
835
+ ].pack('VVVV')
836
+
837
+ message.prepend!(headers)
838
+
839
+ request_id
840
+ end
841
+
842
+ # Increment and return the next available request id.
843
+ #
844
+ # return [Integer]
845
+ def get_request_id
846
+ request_id = ''
847
+ @id_lock.synchronize do
848
+ request_id = @@current_request_id += 1
849
+ end
850
+ request_id
851
+ end
852
+
853
+ # Low-level method for sending a message on a socket.
854
+ # Requires a packed message and an available socket,
855
+ #
856
+ # @return [Integer] number of bytes sent
857
+ def send_message_on_socket(packed_message, socket)
858
+ begin
859
+ total_bytes_sent = socket.send(packed_message, 0)
860
+ if total_bytes_sent != packed_message.size
861
+ packed_message.slice!(0, total_bytes_sent)
862
+ while packed_message.size > 0
863
+ byte_sent = socket.send(packed_message, 0)
864
+ total_bytes_sent += byte_sent
865
+ packed_message.slice!(0, byte_sent)
866
+ end
867
+ end
868
+ total_bytes_sent
869
+ rescue => ex
870
+ close
871
+ raise ConnectionFailure, "Operation failed with the following exception: #{ex}"
872
+ end
873
+ end
874
+
875
+ # Low-level method for receiving data from socket.
876
+ # Requires length and an available socket.
877
+ def receive_message_on_socket(length, socket)
878
+ begin
879
+ if @op_timeout
880
+ message = nil
881
+ Mongo::TimeoutHandler.timeout(@op_timeout, OperationTimeout) do
882
+ message = receive_data(length, socket)
883
+ end
884
+ else
885
+ message = receive_data(length, socket)
886
+ end
887
+ rescue => ex
888
+ close
889
+
890
+ if ex.class == OperationTimeout
891
+ raise OperationTimeout, "Timed out waiting on socket read."
892
+ else
893
+ raise ConnectionFailure, "Operation failed with the following exception: #{ex}"
894
+ end
895
+ end
896
+ message
897
+ end
898
+
899
+ def receive_data(length, socket)
900
+ message = new_binary_string
901
+ socket.read(length, message)
902
+ raise ConnectionFailure, "connection closed" unless message && message.length > 0
903
+ if message.length < length
904
+ chunk = new_binary_string
905
+ while message.length < length
906
+ socket.read(length - message.length, chunk)
907
+ raise ConnectionFailure, "connection closed" unless chunk.length > 0
908
+ message << chunk
909
+ end
910
+ end
911
+ message
912
+ end
913
+
914
+ if defined?(Encoding)
915
+ BINARY_ENCODING = Encoding.find("binary")
916
+
917
+ def new_binary_string
918
+ "".force_encoding(BINARY_ENCODING)
919
+ end
920
+ else
921
+ def new_binary_string
922
+ ""
923
+ end
924
+ end
925
+ end
926
+ end