jonbell-mongo 1.3.1.2

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 (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