mongo-lyon 1.2.4

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