kbaum-mongo 0.18.3p

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