kbaum-mongo 0.18.3p

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 (72) hide show
  1. data/LICENSE.txt +202 -0
  2. data/README.rdoc +339 -0
  3. data/Rakefile +138 -0
  4. data/bin/bson_benchmark.rb +59 -0
  5. data/bin/fail_if_no_c.rb +11 -0
  6. data/examples/admin.rb +42 -0
  7. data/examples/capped.rb +22 -0
  8. data/examples/cursor.rb +48 -0
  9. data/examples/gridfs.rb +88 -0
  10. data/examples/index_test.rb +126 -0
  11. data/examples/info.rb +31 -0
  12. data/examples/queries.rb +70 -0
  13. data/examples/simple.rb +24 -0
  14. data/examples/strict.rb +35 -0
  15. data/examples/types.rb +36 -0
  16. data/lib/mongo/collection.rb +609 -0
  17. data/lib/mongo/connection.rb +672 -0
  18. data/lib/mongo/cursor.rb +403 -0
  19. data/lib/mongo/db.rb +555 -0
  20. data/lib/mongo/exceptions.rb +66 -0
  21. data/lib/mongo/gridfs/chunk.rb +91 -0
  22. data/lib/mongo/gridfs/grid.rb +79 -0
  23. data/lib/mongo/gridfs/grid_file_system.rb +101 -0
  24. data/lib/mongo/gridfs/grid_io.rb +338 -0
  25. data/lib/mongo/gridfs/grid_store.rb +580 -0
  26. data/lib/mongo/gridfs.rb +25 -0
  27. data/lib/mongo/types/binary.rb +52 -0
  28. data/lib/mongo/types/code.rb +36 -0
  29. data/lib/mongo/types/dbref.rb +40 -0
  30. data/lib/mongo/types/min_max_keys.rb +58 -0
  31. data/lib/mongo/types/objectid.rb +180 -0
  32. data/lib/mongo/types/regexp_of_holding.rb +45 -0
  33. data/lib/mongo/util/bson_c.rb +18 -0
  34. data/lib/mongo/util/bson_ruby.rb +606 -0
  35. data/lib/mongo/util/byte_buffer.rb +222 -0
  36. data/lib/mongo/util/conversions.rb +87 -0
  37. data/lib/mongo/util/ordered_hash.rb +140 -0
  38. data/lib/mongo/util/server_version.rb +69 -0
  39. data/lib/mongo/util/support.rb +26 -0
  40. data/lib/mongo.rb +63 -0
  41. data/mongo-ruby-driver.gemspec +28 -0
  42. data/test/auxillary/autoreconnect_test.rb +42 -0
  43. data/test/binary_test.rb +15 -0
  44. data/test/bson_test.rb +427 -0
  45. data/test/byte_buffer_test.rb +81 -0
  46. data/test/chunk_test.rb +82 -0
  47. data/test/collection_test.rb +515 -0
  48. data/test/connection_test.rb +160 -0
  49. data/test/conversions_test.rb +120 -0
  50. data/test/cursor_test.rb +379 -0
  51. data/test/db_api_test.rb +780 -0
  52. data/test/db_connection_test.rb +16 -0
  53. data/test/db_test.rb +272 -0
  54. data/test/grid_file_system_test.rb +210 -0
  55. data/test/grid_io_test.rb +78 -0
  56. data/test/grid_store_test.rb +334 -0
  57. data/test/grid_test.rb +87 -0
  58. data/test/objectid_test.rb +125 -0
  59. data/test/ordered_hash_test.rb +172 -0
  60. data/test/replica/count_test.rb +34 -0
  61. data/test/replica/insert_test.rb +50 -0
  62. data/test/replica/pooled_insert_test.rb +54 -0
  63. data/test/replica/query_test.rb +39 -0
  64. data/test/slave_connection_test.rb +36 -0
  65. data/test/test_helper.rb +42 -0
  66. data/test/threading/test_threading_large_pool.rb +90 -0
  67. data/test/threading_test.rb +87 -0
  68. data/test/unit/collection_test.rb +61 -0
  69. data/test/unit/connection_test.rb +117 -0
  70. data/test/unit/cursor_test.rb +93 -0
  71. data/test/unit/db_test.rb +98 -0
  72. metadata +127 -0
@@ -0,0 +1,672 @@
1
+ # --
2
+ # Copyright (C) 2008-2010 10gen Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ++
16
+
17
+ require 'set'
18
+ require 'socket'
19
+ require 'thread'
20
+
21
+ module Mongo
22
+
23
+ # Instantiates and manages connections to MongoDB.
24
+ class Connection
25
+
26
+ # Abort connections if a ConnectionError is raised.
27
+ Thread.abort_on_exception = true
28
+
29
+ DEFAULT_PORT = 27017
30
+ STANDARD_HEADER_SIZE = 16
31
+ RESPONSE_HEADER_SIZE = 20
32
+
33
+ MONGODB_URI_MATCHER = /(([.\w\d]+):([\w\d]+)@)?([.\w\d]+)(:([\w\d]+))?(\/([-\d\w]+))?/
34
+ MONGODB_URI_SPEC = "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/database]"
35
+
36
+ attr_reader :logger, :size, :host, :port, :nodes, :auths, :sockets, :checked_out
37
+
38
+ # Counter for generating unique request ids.
39
+ @@current_request_id = 0
40
+
41
+ # Create a connection to MongoDB. Specify either one or a pair of servers,
42
+ # along with a maximum connection pool size and timeout.
43
+ #
44
+ # If connecting to just one server, you may specify whether connection to slave is permitted.
45
+ # In all cases, the default host is "localhost" and the default port is 27017.
46
+ #
47
+ # When specifying a pair, +pair_or_host+, is a hash with two keys: :left and :right. Each key maps to either
48
+ # * a server name, in which case port is 27017,
49
+ # * a port number, in which case the server is "localhost", or
50
+ # * an array containing [server_name, port_number]
51
+ #
52
+ # Note that there are a few issues when using connection pooling with Ruby 1.9 on Windows. These
53
+ # should be resolved in the next release.
54
+ #
55
+ # @param [String, Hash] pair_or_host See explanation above.
56
+ # @param [Integer] port specify a port number here if only one host is being specified. Leave nil if
57
+ # specifying a pair of servers in +pair_or_host+.
58
+ #
59
+ # @option options [Boolean] :slave_ok (false) Must be set to +true+ when connecting
60
+ # to a single, slave node.
61
+ # @option options [Logger, #debug] :logger (nil) Logger instance to receive driver operation log.
62
+ # @option options [Boolean] :auto_reconnect DEPRECATED. See http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby
63
+ # @option options [Integer] :pool_size (1) The maximum number of socket connections that can be opened to the database.
64
+ # @option options [Float] :timeout (5.0) When all of the connections to the pool are checked out,
65
+ # this is the number of seconds to wait for a new connection to be released before throwing an exception.
66
+ #
67
+ #
68
+ # @example localhost, 27017
69
+ # Connection.new
70
+ #
71
+ # @example localhost, 27017
72
+ # Connection.new("localhost")
73
+ #
74
+ # @example localhost, 3000, max 5 connections, with max 5 seconds of wait time.
75
+ # Connection.new("localhost", 3000, :pool_size => 5, :timeout => 5)
76
+ #
77
+ # @example localhost, 3000, where this node may be a slave
78
+ # Connection.new("localhost", 3000, :slave_ok => true)
79
+ #
80
+ # @example DEPRECATED. To initialize a paired connection, use Connection.paired instead.
81
+ # Connection.new({:left => ["db1.example.com", 27017],
82
+ # :right => ["db2.example.com", 27017]})
83
+ #
84
+ # @example DEPRECATED. To initialize a paired connection, use Connection.paired instead.
85
+ # Connection.new({:left => ["db1.example.com", 27017],
86
+ # :right => ["db2.example.com", 27017]}, nil,
87
+ # :pool_size => 20, :timeout => 5)
88
+ #
89
+ # @see http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby Replica pairs in Ruby
90
+ #
91
+ # @core connections
92
+ def initialize(pair_or_host=nil, port=nil, options={})
93
+ if block_given?
94
+ @nodes, @auths = yield self
95
+ else
96
+ @nodes = format_pair(pair_or_host, port)
97
+ end
98
+
99
+ # Host and port of current master.
100
+ @host = @port = nil
101
+
102
+ # Lock for request ids.
103
+ @id_lock = Mutex.new
104
+
105
+ # Pool size and timeout.
106
+ @size = options[:pool_size] || 1
107
+ @timeout = options[:timeout] || 5.0
108
+
109
+ # Mutex for synchronizing pool access
110
+ @connection_mutex = Mutex.new
111
+ @safe_mutex = Mutex.new
112
+
113
+ # Condition variable for signal and wait
114
+ @queue = ConditionVariable.new
115
+
116
+ @sockets = []
117
+ @checked_out = []
118
+
119
+ if options[:auto_reconnect]
120
+ warn(":auto_reconnect is deprecated. see http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby")
121
+ end
122
+
123
+ # slave_ok can be true only if one node is specified
124
+ @slave_ok = options[:slave_ok] && @nodes.length == 1
125
+ @logger = options[:logger] || nil
126
+ @options = options
127
+
128
+ should_connect = options[:connect].nil? ? true : options[:connect]
129
+ if should_connect
130
+ connect_to_master
131
+ authenticate_databases if @auths
132
+ end
133
+ end
134
+
135
+ # Initialize a paired connection to MongoDB.
136
+ #
137
+ # @param nodes [Array] An array of arrays, each of which specified a host and port.
138
+ # @param opts Takes the same options as Connection.new
139
+ #
140
+ # @example
141
+ # Connection.new([["db1.example.com", 27017],
142
+ # ["db2.example.com", 27017]])
143
+ #
144
+ # @example
145
+ # Connection.new([["db1.example.com", 27017],
146
+ # ["db2.example.com", 27017]],
147
+ # :pool_size => 20, :timeout => 5)
148
+ #
149
+ # @return [Mongo::Connection]
150
+ def self.paired(nodes, opts={})
151
+ unless nodes.length == 2 && nodes.all? {|n| n.is_a? Array}
152
+ raise MongoArgumentError, "Connection.paired requires that exactly two nodes be specified."
153
+ end
154
+ # Block returns an array, the first element being an array of nodes and the second an array
155
+ # of authorizations for the database.
156
+ new(nil, nil, opts) do |con|
157
+ [[con.pair_val_to_connection(nodes[0]), con.pair_val_to_connection(nodes[1])], []]
158
+ end
159
+ end
160
+
161
+ # Initialize a connection to MongoDB using the MongoDB URI spec:
162
+ #
163
+ # @param uri [String]
164
+ # A string of the format mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/database]
165
+ #
166
+ # @param opts Any of the options available for Connection.new
167
+ #
168
+ # @return [Mongo::Connection]
169
+ def self.from_uri(uri, opts={})
170
+ new(nil, nil, opts) do |con|
171
+ con.parse_uri(uri)
172
+ end
173
+ end
174
+
175
+ # Return a hash with all database names
176
+ # and their respective sizes on disk.
177
+ #
178
+ # @return [Hash]
179
+ def database_info
180
+ doc = self['admin'].command(:listDatabases => 1)
181
+ returning({}) do |info|
182
+ doc['databases'].each { |db| info[db['name']] = db['sizeOnDisk'].to_i }
183
+ end
184
+ end
185
+
186
+ # Return an array of database names.
187
+ #
188
+ # @return [Array]
189
+ def database_names
190
+ database_info.keys
191
+ end
192
+
193
+ # Return a database with the given name.
194
+ # See DB#new for valid options hash parameters.
195
+ #
196
+ # @param [String] db_name a valid database name.
197
+ #
198
+ # @return [Mongo::DB]
199
+ #
200
+ # @core databases db-instance_method
201
+ def db(db_name, options={})
202
+ DB.new(db_name, self, options.merge(:logger => @logger))
203
+ end
204
+
205
+ # Shortcut for returning a database. Use DB#db to accept options.
206
+ #
207
+ # @param [String] db_name a valid database name.
208
+ #
209
+ # @return [Mongo::DB]
210
+ #
211
+ # @core databases []-instance_method
212
+ def [](db_name)
213
+ DB.new(db_name, self, :logger => @logger)
214
+ end
215
+
216
+ # Drop a database.
217
+ #
218
+ # @param [String] name name of an existing database.
219
+ def drop_database(name)
220
+ self[name].command(:dropDatabase => 1)
221
+ end
222
+
223
+ # Copy the database +from+ on the local server to +to+ on the specified +host+.
224
+ # +host+ defaults to 'localhost' if no value is provided.
225
+ #
226
+ # @param [String] from name of the database to copy from.
227
+ # @param [String] to name of the database to copy to.
228
+ # @param [String] from_host host of the 'from' database.
229
+ def copy_database(from, to, from_host="localhost")
230
+ oh = OrderedHash.new
231
+ oh[:copydb] = 1
232
+ oh[:fromhost] = from_host
233
+ oh[:fromdb] = from
234
+ oh[:todb] = to
235
+ self["admin"].command(oh)
236
+ end
237
+
238
+ # Increment and return the next available request id.
239
+ #
240
+ # return [Integer]
241
+ def get_request_id
242
+ request_id = ''
243
+ @id_lock.synchronize do
244
+ request_id = @@current_request_id += 1
245
+ end
246
+ request_id
247
+ end
248
+
249
+ # Get the build information for the current connection.
250
+ #
251
+ # @return [Hash]
252
+ def server_info
253
+ db("admin").command({:buildinfo => 1}, {:admin => true, :check_response => true})
254
+ end
255
+
256
+ # Get the build version of the current server.
257
+ #
258
+ # @return [Mongo::ServerVersion]
259
+ # object allowing easy comparability of version.
260
+ def server_version
261
+ ServerVersion.new(server_info["version"])
262
+ end
263
+
264
+ # Is it okay to connect to a slave?
265
+ #
266
+ # @return [Boolean]
267
+ def slave_ok?
268
+ @slave_ok
269
+ end
270
+
271
+
272
+ ## Connections and pooling ##
273
+
274
+ # Send a message to MongoDB, adding the necessary headers.
275
+ #
276
+ # @param [Integer] operation a MongoDB opcode.
277
+ # @param [ByteBuffer] message a message to send to the database.
278
+ # @param [String] log_message text version of +message+ for logging.
279
+ #
280
+ # @return [True]
281
+ def send_message(operation, message, log_message=nil)
282
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
283
+ begin
284
+ packed_message = add_message_headers(operation, message).to_s
285
+ socket = checkout
286
+ send_message_on_socket(packed_message, socket)
287
+ ensure
288
+ checkin(socket)
289
+ end
290
+ end
291
+
292
+ # Sends a message to the database, waits for a response, and raises
293
+ # an exception if the operation has failed.
294
+ #
295
+ # @param [Integer] operation a MongoDB opcode.
296
+ # @param [ByteBuffer] message a message to send to the database.
297
+ # @param [String] db_name the name of the database. used on call to get_last_error.
298
+ # @param [String] log_message text version of +message+ for logging.
299
+ #
300
+ # @return [Array]
301
+ # An array whose indexes include [0] documents returned, [1] number of document received,
302
+ # and [3] a cursor_id.
303
+ def send_message_with_safe_check(operation, message, db_name, log_message=nil)
304
+ message_with_headers = add_message_headers(operation, message)
305
+ message_with_check = last_error_message(db_name)
306
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
307
+ begin
308
+ sock = checkout
309
+ packed_message = message_with_headers.append!(message_with_check).to_s
310
+ docs = num_received = cursor_id = ''
311
+ @safe_mutex.synchronize do
312
+ send_message_on_socket(packed_message, sock)
313
+ docs, num_received, cursor_id = receive(sock)
314
+ end
315
+ ensure
316
+ checkin(sock)
317
+ end
318
+ if num_received == 1 && error = docs[0]['err']
319
+ raise Mongo::OperationFailure, error
320
+ end
321
+ [docs, num_received, cursor_id]
322
+ end
323
+
324
+ # Sends a message to the database and waits for the response.
325
+ #
326
+ # @param [Integer] operation a MongoDB opcode.
327
+ # @param [ByteBuffer] message a message to send to the database.
328
+ # @param [String] log_message text version of +message+ for logging.
329
+ # @param [Socket] socket a socket to use in lieu of checking out a new one.
330
+ #
331
+ # @return [Array]
332
+ # An array whose indexes include [0] documents returned, [1] number of document received,
333
+ # and [3] a cursor_id.
334
+ def receive_message(operation, message, log_message=nil, socket=nil)
335
+ packed_message = add_message_headers(operation, message).to_s
336
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
337
+ begin
338
+ sock = socket || checkout
339
+
340
+ result = ''
341
+ @safe_mutex.synchronize do
342
+ send_message_on_socket(packed_message, sock)
343
+ result = receive(sock)
344
+ end
345
+ ensure
346
+ checkin(sock)
347
+ end
348
+ result
349
+ end
350
+
351
+ # Create a new socket and attempt to connect to master.
352
+ # If successful, sets host and port to master and returns the socket.
353
+ #
354
+ # @raise [ConnectionFailure] if unable to connect to any host or port.
355
+ def connect_to_master
356
+ close
357
+ @host = @port = nil
358
+ for node_pair in @nodes
359
+ host, port = *node_pair
360
+ begin
361
+ socket = TCPSocket.new(host, port)
362
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
363
+
364
+ # If we're connected to master, set the @host and @port
365
+ result = self['admin'].command({:ismaster => 1}, false, false, socket)
366
+ if result['ok'] == 1 && ((is_master = result['ismaster'] == 1) || @slave_ok)
367
+ @host, @port = host, port
368
+ end
369
+
370
+ # Note: slave_ok can be true only when connecting to a single node.
371
+ if @nodes.length == 1 && !is_master && !@slave_ok
372
+ raise ConfigurationError, "Trying to connect directly to slave; " +
373
+ "if this is what you want, specify :slave_ok => true."
374
+ end
375
+
376
+ break if is_master || @slave_ok
377
+ rescue SocketError, SystemCallError, IOError => ex
378
+ socket.close if socket
379
+ close
380
+ false
381
+ end
382
+ end
383
+ raise ConnectionFailure, "failed to connect to any given host:port" unless socket
384
+ end
385
+
386
+ # Are we connected to MongoDB? This is determined by checking whether
387
+ # host and port have values, since they're set to nil on calls to #close.
388
+ def connected?
389
+ @host && @port
390
+ end
391
+
392
+ # Close the connection to the database.
393
+ def close
394
+ @sockets.each do |sock|
395
+ sock.close
396
+ end
397
+ @host = @port = nil
398
+ @sockets.clear
399
+ @checked_out.clear
400
+ end
401
+
402
+ ## Configuration helper methods
403
+
404
+ # Returns an array of host-port pairs.
405
+ #
406
+ # @private
407
+ def format_pair(pair_or_host, port)
408
+ case pair_or_host
409
+ when String
410
+ [[pair_or_host, port ? port.to_i : DEFAULT_PORT]]
411
+ when Hash
412
+ warn "Initializing a paired connection with Connection.new is deprecated. Use Connection.pair instead."
413
+ connections = []
414
+ connections << pair_val_to_connection(pair_or_host[:left])
415
+ connections << pair_val_to_connection(pair_or_host[:right])
416
+ connections
417
+ when nil
418
+ [['localhost', DEFAULT_PORT]]
419
+ end
420
+ end
421
+
422
+ # Convert an argument containing a host name string and a
423
+ # port number integer into a [host, port] pair array.
424
+ #
425
+ # @private
426
+ def pair_val_to_connection(a)
427
+ case a
428
+ when nil
429
+ ['localhost', DEFAULT_PORT]
430
+ when String
431
+ [a, DEFAULT_PORT]
432
+ when Integer
433
+ ['localhost', a]
434
+ when Array
435
+ a
436
+ end
437
+ end
438
+
439
+ # Parse a MongoDB URI. This method is used by Connection.from_uri.
440
+ # Returns an array of nodes and an array of db authorizations, if applicable.
441
+ #
442
+ # @private
443
+ def parse_uri(string)
444
+ if string =~ /^mongodb:\/\//
445
+ string = string[10..-1]
446
+ else
447
+ raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
448
+ end
449
+
450
+ nodes = []
451
+ auths = []
452
+ specs = string.split(',')
453
+ specs.each do |spec|
454
+ matches = MONGODB_URI_MATCHER.match(spec)
455
+ if !matches
456
+ raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
457
+ end
458
+
459
+ uname = matches[2]
460
+ pwd = matches[3]
461
+ host = matches[4]
462
+ port = matches[6] || DEFAULT_PORT
463
+ if !(port.to_s =~ /^\d+$/)
464
+ raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits."
465
+ end
466
+ port = port.to_i
467
+ db = matches[8]
468
+
469
+ if (uname || pwd || db) && !(uname && pwd && db)
470
+ raise MongoArgumentError, "MongoDB URI must include all three of username, password, " +
471
+ "and db if any one of these is specified."
472
+ else
473
+ auths << [uname, pwd, db]
474
+ end
475
+
476
+ nodes << [host, port]
477
+ end
478
+
479
+ [nodes, auths]
480
+ end
481
+
482
+ private
483
+
484
+ # Return a socket to the pool.
485
+ def checkin(socket)
486
+ @connection_mutex.synchronize do
487
+ @checked_out.delete(socket)
488
+ @queue.signal
489
+ end
490
+ true
491
+ end
492
+
493
+ # Adds a new socket to the pool and checks it out.
494
+ #
495
+ # This method is called exclusively from #checkout;
496
+ # therefore, it runs within a mutex.
497
+ def checkout_new_socket
498
+ begin
499
+ socket = TCPSocket.new(@host, @port)
500
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
501
+ rescue => ex
502
+ raise ConnectionFailure, "Failed to connect socket: #{ex}"
503
+ end
504
+ @sockets << socket
505
+ @checked_out << socket
506
+ socket
507
+ end
508
+
509
+ # Checks out the first available socket from the pool.
510
+ #
511
+ # This method is called exclusively from #checkout;
512
+ # therefore, it runs within a mutex.
513
+ def checkout_existing_socket
514
+ socket = (@sockets - @checked_out).first
515
+ @checked_out << socket
516
+ socket
517
+ end
518
+
519
+ # Check out an existing socket or create a new socket if the maximum
520
+ # pool size has not been exceeded. Otherwise, wait for the next
521
+ # available socket.
522
+ def checkout
523
+ connect_to_master if !connected?
524
+ start_time = Time.now
525
+ loop do
526
+ if (Time.now - start_time) > @timeout
527
+ raise ConnectionTimeoutError, "could not obtain connection within " +
528
+ "#{@timeout} seconds. The max pool size is currently #{@size}; " +
529
+ "consider increasing the pool size or timeout."
530
+ end
531
+
532
+ @connection_mutex.synchronize do
533
+ socket = if @checked_out.size < @sockets.size
534
+ checkout_existing_socket
535
+ elsif @sockets.size < @size
536
+ checkout_new_socket
537
+ end
538
+
539
+ return socket if socket
540
+
541
+ # Otherwise, wait
542
+ if @logger
543
+ @logger.warn "Waiting for available connection; #{@checked_out.size} of #{@size} connections checked out."
544
+ end
545
+ @queue.wait(@connection_mutex)
546
+ end
547
+ end
548
+ end
549
+
550
+ def receive(sock)
551
+ receive_header(sock)
552
+ number_received, cursor_id = receive_response_header(sock)
553
+ read_documents(number_received, cursor_id, sock)
554
+ end
555
+
556
+ def receive_header(sock)
557
+ header = ByteBuffer.new
558
+ header.put_array(receive_message_on_socket(16, sock).unpack("C*"))
559
+ unless header.size == STANDARD_HEADER_SIZE
560
+ raise "Short read for DB response header: " +
561
+ "expected #{STANDARD_HEADER_SIZE} bytes, saw #{header.size}"
562
+ end
563
+ header.rewind
564
+ size = header.get_int
565
+ request_id = header.get_int
566
+ response_to = header.get_int
567
+ op = header.get_int
568
+ end
569
+
570
+ def receive_response_header(sock)
571
+ header_buf = ByteBuffer.new
572
+ header_buf.put_array(receive_message_on_socket(RESPONSE_HEADER_SIZE, sock).unpack("C*"))
573
+ if header_buf.length != RESPONSE_HEADER_SIZE
574
+ raise "Short read for DB response header; " +
575
+ "expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}"
576
+ end
577
+ header_buf.rewind
578
+ result_flags = header_buf.get_int
579
+ cursor_id = header_buf.get_long
580
+ starting_from = header_buf.get_int
581
+ number_remaining = header_buf.get_int
582
+ [number_remaining, cursor_id]
583
+ end
584
+
585
+ def read_documents(number_received, cursor_id, sock)
586
+ docs = []
587
+ number_remaining = number_received
588
+ while number_remaining > 0 do
589
+ buf = ByteBuffer.new
590
+ buf.put_array(receive_message_on_socket(4, sock).unpack("C*"))
591
+ buf.rewind
592
+ size = buf.get_int
593
+ buf.put_array(receive_message_on_socket(size - 4, sock).unpack("C*"), 4)
594
+ number_remaining -= 1
595
+ buf.rewind
596
+ docs << BSON.deserialize(buf)
597
+ end
598
+ [docs, number_received, cursor_id]
599
+ end
600
+
601
+ def last_error_message(db_name)
602
+ message = ByteBuffer.new
603
+ message.put_int(0)
604
+ BSON_RUBY.serialize_cstr(message, "#{db_name}.$cmd")
605
+ message.put_int(0)
606
+ message.put_int(-1)
607
+ message.put_array(BSON.serialize({:getlasterror => 1}, false).unpack("C*"))
608
+ add_message_headers(Mongo::Constants::OP_QUERY, message)
609
+ end
610
+
611
+ # Prepares a message for transmission to MongoDB by
612
+ # constructing a valid message header.
613
+ def add_message_headers(operation, message)
614
+ headers = ByteBuffer.new
615
+
616
+ # Message size.
617
+ headers.put_int(16 + message.size)
618
+
619
+ # Unique request id.
620
+ headers.put_int(get_request_id)
621
+
622
+ # Response id.
623
+ headers.put_int(0)
624
+
625
+ # Opcode.
626
+ headers.put_int(operation)
627
+ message.prepend!(headers)
628
+ end
629
+
630
+ # Low-level method for sending a message on a socket.
631
+ # Requires a packed message and an available socket,
632
+ def send_message_on_socket(packed_message, socket)
633
+ begin
634
+ socket.send(packed_message, 0)
635
+ rescue => ex
636
+ close
637
+ raise ConnectionFailure, "Operation failed with the following exception: #{ex}"
638
+ end
639
+ end
640
+
641
+ # Low-level method for receiving data from socket.
642
+ # Requires length and an available socket.
643
+ def receive_message_on_socket(length, socket)
644
+ message = ""
645
+ begin
646
+ while message.length < length do
647
+ chunk = socket.recv(length - message.length)
648
+ raise ConnectionFailure, "connection closed" unless chunk.length > 0
649
+ message += chunk
650
+ end
651
+ rescue => ex
652
+ close
653
+ raise ConnectionFailure, "Operation failed with the following exception: #{ex}"
654
+ end
655
+ message
656
+ end
657
+
658
+ # Authenticate for any auth info provided on instantiating the connection.
659
+ # Only called when a MongoDB URI has been used to instantiate the connection, and
660
+ # when that connection specifies databases and authentication credentials.
661
+ #
662
+ # @raise [MongoDBError]
663
+ def authenticate_databases
664
+ @auths.each do |auth|
665
+ user = auth[0]
666
+ pwd = auth[1]
667
+ db_name = auth[2]
668
+ self.db(db_name).authenticate(user, pwd)
669
+ end
670
+ end
671
+ end
672
+ end