mongo-lyon 1.2.4

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