mongo 0.17.1 → 0.18

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.
@@ -1,34 +1,33 @@
1
1
  = Introduction
2
2
 
3
- This is a Ruby driver for MongoDB[http://www.mongodb.org].
3
+ This is the 10gen-supported Ruby driver for MongoDB[http://www.mongodb.org].
4
4
 
5
- Here is a quick code sample. See the files in the "examples" subdirectory for
6
- many more.
5
+ Here is a quick code sample. See the MongoDB Ruby Tutorial
6
+ (http://www.mongodb.org/display/DOCS/Ruby+Tutorial) for much more.
7
7
 
8
+ require 'rubygems'
8
9
  require 'mongo'
9
-
10
10
  include Mongo
11
11
 
12
- db = Connection.new('localhost').db('sample-db')
13
- coll = db.collection('test')
14
-
15
- coll.remove
16
- 3.times { |i| coll.insert({'a' => i+1}) }
17
- puts "There are #{coll.count()} records. Here they are:"
18
- coll.find().each { |doc| puts doc.inspect }
12
+ @db = Connection.new.db('sample-db')
13
+ @coll = db.collection('test')
19
14
 
20
- This driver also includes an implementation of a GridStore class, a Ruby
21
- interface to Mongo's GridFS storage.
15
+ @coll.remove
16
+ 3.times do |i|
17
+ @coll.insert({'a' => i+1})
18
+ end
19
+ puts "There are #{@coll.count()} records. Here they are:"
20
+ @coll.find().each { |doc| puts doc.inspect }
22
21
 
23
22
  = Installation
24
23
 
25
24
  The driver's gems are hosted on Gemcutter[http://gemcutter.org]. If you haven't
26
- installed a gem from Gemcutter before you'll need to set up Gemcutter first
25
+ installed a gem from Gemcutter before, you'll need to set up Gemcutter first:
27
26
 
28
27
  $ gem install gemcutter
29
28
  $ gem tumble
30
29
 
31
- If (or once) you have Gemcutter setup, install the "mongo" gem by typing
30
+ Once you've installed Gemcutter, install the mongo gem as follows:
32
31
 
33
32
  $ gem install mongo
34
33
 
@@ -54,11 +53,14 @@ That's all there is to it!
54
53
 
55
54
  = Examples
56
55
 
57
- There are many examples in the "examples" subdirectory. Samples include using
58
- the driver and using the GridFS class GridStore. Mongo must be running for
56
+ For extensive examples, see the MongoDB Ruby Tutorial
57
+ (http://www.mongodb.org/display/DOCS/Ruby+Tutorial).
58
+
59
+ Bundled with the dirver are many examples in the "examples" subdirectory. Samples include using
60
+ the driver and using the GridFS class GridStore. MongoDB must be running for
59
61
  these examples to work, of course.
60
62
 
61
- Here's how to start mongo and run the "simple.rb" example:
63
+ Here's how to start MongoDB and run the "simple.rb" example:
62
64
 
63
65
  $ cd path/to/mongo
64
66
  $ ./mongod run
@@ -68,33 +70,15 @@ Here's how to start mongo and run the "simple.rb" example:
68
70
 
69
71
  See also the test code, especially test/test_db_api.rb.
70
72
 
71
- = The Driver
72
-
73
- Here is some simple example code:
74
-
75
- require 'rubygems' # not required for Ruby 1.9
76
- require 'mongo'
77
-
78
- include Mongo
79
- db = Connection.new.db('my-db-name')
80
- things = db.collection('things')
81
-
82
- things.remove
83
- things.insert('a' => 42)
84
- things.insert('a' => 99, 'b' => Time.now)
85
- puts things.count # => 2
86
- puts things.find('a' => 42).next_object.inspect # {"a"=>42}
87
-
88
-
89
73
  = GridStore
90
74
 
91
- The GridStore class is a Ruby implementation of Mongo's GridFS file storage
92
- system. An instance of GridStore is like an IO object. See the rdocs for
75
+ The GridStore class is a Ruby implementation of MongoDB's GridFS file storage
76
+ system. An instance of GridStore is like an IO object. See the RDocs for
93
77
  details, and see examples/gridfs.rb for code that uses many of the GridStore
94
- features like metadata, content type, rewind/seek/tell, etc.
78
+ features (metadata, content type, rewind/seek/tell, etc).
95
79
 
96
80
  Note that the GridStore class is not automatically required when you require
97
- 'mongo'. You need to require 'mongo/gridfs'.
81
+ 'mongo'. You also need to require 'mongo/gridfs'
98
82
 
99
83
  Example code:
100
84
 
@@ -112,12 +96,31 @@ Example code:
112
96
  }
113
97
 
114
98
 
115
-
116
99
  = Notes
117
100
 
118
101
  == Thread Safety
119
102
 
120
- mongo-ruby-driver is thread safe.
103
+ The driver is thread safe.
104
+
105
+ == Connection Pooling
106
+
107
+ As of 0.18, the driver implements connection pooling. By default, only one
108
+ socket connection will be opened to MongoDB. However, if you're running a
109
+ multi-threaded application, you can specify a maximum pool size and a maximum
110
+ timeout for waiting for old connections to be released to the pool.
111
+
112
+ To set up a pooled connection to a single MongoDB instance:
113
+
114
+ @conn = Connection.new("localhost", 27017, :pool_size => 5, :timeout => 5)
115
+
116
+ A pooled connection to a paired instance would look like this:
117
+
118
+ @conn = Connection.new({:left => ["db1.example.com", 27017],
119
+ :right => ["db2.example.com", 27017]}, nil,
120
+ :pool_size => 20, :timeout => 5)
121
+
122
+ Though the pooling architecure will undoubtedly evolve, it owes much credit
123
+ to the connection pooling implementations in ActiveRecord and PyMongo.
121
124
 
122
125
  == Using with Phusion Passenger
123
126
 
@@ -255,7 +258,26 @@ If you have the source code, you can run the tests.
255
258
 
256
259
  $ rake test
257
260
 
258
- The tests now require shoulda and mocha. You can install these gems as
261
+ This will run both unit and functional tests. If you want to run these
262
+ individually:
263
+
264
+ $ rake test:unit
265
+ $ rake test:functional
266
+
267
+
268
+ If you want to test replica pairs, you can run the following tests
269
+ individually:
270
+
271
+ $ rake test:pair_count
272
+ $ rake test:pair_insert
273
+ $ rake test:pair_query
274
+
275
+ It's also possible to test replica pairs with connection pooling:
276
+
277
+ $ rake test:pooled_pair_insert
278
+
279
+
280
+ All tests now require shoulda and mocha. You can install these gems as
259
281
  follows:
260
282
 
261
283
  $ gem install shoulda
@@ -295,7 +317,7 @@ Then open the file html/index.html.
295
317
 
296
318
  = Release Notes
297
319
 
298
- See the git log comments.
320
+ See HISTORY.
299
321
 
300
322
  = Credits
301
323
 
@@ -338,9 +360,6 @@ Cyril Mougel, cyril.mougel@gmail.com
338
360
  Jack Chen, chendo on github
339
361
  * Test case + fix for deserializing pre-epoch Time instances
340
362
 
341
- Kyle Banker, banker on github
342
- * #limit and #skip methods for Cursor instances
343
-
344
363
  Michael Bernstein, mrb on github
345
364
  * #sort method for Cursor instances
346
365
 
@@ -357,6 +376,9 @@ Sunny Hirai
357
376
  * Suggested hashcode fix for Mongo::ObjectID
358
377
  * Noted index ordering bug.
359
378
 
379
+ Christos Trochalakis
380
+ * Added map/reduce helper
381
+
360
382
  = License
361
383
 
362
384
  Copyright 2008-2009 10gen Inc.
data/Rakefile CHANGED
@@ -19,6 +19,7 @@ desc "Test the MongoDB Ruby driver."
19
19
  task :test do
20
20
  Rake::Task['test:unit'].invoke
21
21
  Rake::Task['test:functional'].invoke
22
+ Rake::Task['test:pooled_threading'].invoke
22
23
  end
23
24
 
24
25
  namespace :test do
@@ -31,6 +32,31 @@ namespace :test do
31
32
  t.test_files = FileList['test/test*.rb']
32
33
  t.verbose = true
33
34
  end
35
+
36
+ Rake::TestTask.new(:pooled_threading) do |t|
37
+ t.test_files = FileList['test/threading/*.rb']
38
+ t.verbose = true
39
+ end
40
+
41
+ Rake::TestTask.new(:pair_count) do |t|
42
+ t.test_files = FileList['test/replica/count_test.rb']
43
+ t.verbose = true
44
+ end
45
+
46
+ Rake::TestTask.new(:pair_insert) do |t|
47
+ t.test_files = FileList['test/replica/insert_test.rb']
48
+ t.verbose = true
49
+ end
50
+
51
+ Rake::TestTask.new(:pooled_pair_insert) do |t|
52
+ t.test_files = FileList['test/replica/pooled_insert_test.rb']
53
+ t.verbose = true
54
+ end
55
+
56
+ Rake::TestTask.new(:pair_query) do |t|
57
+ t.test_files = FileList['test/replica/query_test.rb']
58
+ t.verbose = true
59
+ end
34
60
  end
35
61
 
36
62
  desc "Generate documentation"
@@ -1,4 +1,4 @@
1
- $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
 
3
3
  require 'mongo/types/binary'
4
4
  require 'mongo/types/code'
@@ -31,5 +31,5 @@ module Mongo
31
31
  ASCENDING = 1
32
32
  DESCENDING = -1
33
33
 
34
- VERSION = "0.17.1"
34
+ VERSION = "0.18"
35
35
  end
@@ -30,7 +30,7 @@ module Mongo
30
30
  def profiling_level
31
31
  oh = OrderedHash.new
32
32
  oh[:profile] = -1
33
- doc = @db.db_command(oh)
33
+ doc = @db.command(oh)
34
34
  raise "Error with profile command: #{doc.inspect}" unless @db.ok?(doc) && doc['was'].kind_of?(Numeric)
35
35
  case doc['was'].to_i
36
36
  when 0
@@ -57,7 +57,7 @@ module Mongo
57
57
  else
58
58
  raise "Error: illegal profiling level value #{level}"
59
59
  end
60
- doc = @db.db_command(oh)
60
+ doc = @db.command(oh)
61
61
  raise "Error with profile command: #{doc.inspect}" unless @db.ok?(doc)
62
62
  end
63
63
 
@@ -71,7 +71,7 @@ module Mongo
71
71
  # problem or returning an interesting hash (see especially the
72
72
  # 'result' string value) if all is well.
73
73
  def validate_collection(name)
74
- doc = @db.db_command(:validate => name)
74
+ doc = @db.command(:validate => name)
75
75
  raise "Error with validate command: #{doc.inspect}" unless @db.ok?(doc)
76
76
  result = doc['result']
77
77
  raise "Error with validation data: #{doc.inspect}" unless result.kind_of?(String)
@@ -41,6 +41,7 @@ module Mongo
41
41
  end
42
42
 
43
43
  @db, @name = db, name
44
+ @connection = @db.connection
44
45
  @pk_factory = pk_factory || ObjectID
45
46
  @hint = nil
46
47
  end
@@ -109,14 +110,10 @@ module Mongo
109
110
  def find(selector={}, options={})
110
111
  fields = options.delete(:fields)
111
112
  fields = ["_id"] if fields && fields.empty?
112
- skip = options.delete(:offset) || nil
113
- if !skip.nil?
114
- warn "the :offset option to find is deprecated and will be removed. please use :skip instead"
115
- end
116
- skip = options.delete(:skip) || skip || 0
117
- limit = options.delete(:limit) || 0
118
- sort = options.delete(:sort)
119
- hint = options.delete(:hint)
113
+ skip = options.delete(:skip) || skip || 0
114
+ limit = options.delete(:limit) || 0
115
+ sort = options.delete(:sort)
116
+ hint = options.delete(:hint)
120
117
  snapshot = options.delete(:snapshot)
121
118
  if options[:timeout] == false && !block_given?
122
119
  raise ArgumentError, "Timeout can be set to false only when #find is invoked with a block."
@@ -222,17 +219,10 @@ module Mongo
222
219
  BSON.serialize_cstr(message, "#{@db.name}.#{@name}")
223
220
  message.put_int(0)
224
221
  message.put_array(BSON_SERIALIZER.serialize(selector, false).unpack("C*"))
225
- @db.send_message_with_operation(Mongo::Constants::OP_DELETE, message,
222
+ @connection.send_message(Mongo::Constants::OP_DELETE, message,
226
223
  "db.#{@db.name}.remove(#{selector.inspect})")
227
224
  end
228
225
 
229
- # Remove all records.
230
- # DEPRECATED: please use Collection#remove instead.
231
- def clear
232
- warn "Collection#clear is deprecated. Please use Collection#remove instead."
233
- remove({})
234
- end
235
-
236
226
  # Update a single document in this collection.
237
227
  #
238
228
  # :selector :: a hash specifying elements which must be present for a document to be updated. Note:
@@ -261,10 +251,10 @@ module Mongo
261
251
  message.put_array(BSON_SERIALIZER.serialize(selector, false).unpack("C*"))
262
252
  message.put_array(BSON_SERIALIZER.serialize(document, false).unpack("C*"))
263
253
  if options[:safe]
264
- @db.send_message_with_safe_check(Mongo::Constants::OP_UPDATE, message,
254
+ @connection.send_message_with_safe_check(Mongo::Constants::OP_UPDATE, message, @db.name,
265
255
  "db.#{@name}.update(#{selector.inspect}, #{document.inspect})")
266
256
  else
267
- @db.send_message_with_operation(Mongo::Constants::OP_UPDATE, message,
257
+ @connection.send_message(Mongo::Constants::OP_UPDATE, message,
268
258
  "db.#{@name}.update(#{selector.inspect}, #{document.inspect})")
269
259
  end
270
260
  end
@@ -308,8 +298,44 @@ module Mongo
308
298
  @db.drop_collection(@name)
309
299
  end
310
300
 
311
- # Perform a query similar to an SQL group by operation.
301
+ # Performs a map/reduce operation on the current collection. Returns a new
302
+ # collection containing the results of the operation.
303
+ #
304
+ # Required:
305
+ # +map+ :: a map function, written in javascript.
306
+ # +reduce+ :: a reduce function, written in javascript.
307
+ #
308
+ # Optional:
309
+ # :query :: a query selector document, like what's passed to #find, to limit
310
+ # the operation to a subset of the collection.
311
+ # :sort :: sort parameters passed to the query.
312
+ # :limit :: number of objects to return from the collection.
313
+ # :finalize :: a javascript function to apply to the result set after the
314
+ # map/reduce operation has finished.
315
+ # :out :: the name of the output collection. if specified, the collection will not be treated as temporary.
316
+ # :keeptemp :: if true, the generated collection will be persisted. default is false.
317
+ # :verbose :: if true, provides statistics on job execution time.
312
318
  #
319
+ # For more information on using map/reduce, see http://www.mongodb.org/display/DOCS/MapReduce
320
+ def map_reduce(map, reduce, options={})
321
+ map = Code.new(map) unless map.is_a?(Code)
322
+ reduce = Code.new(reduce) unless reduce.is_a?(Code)
323
+
324
+ hash = OrderedHash.new
325
+ hash['mapreduce'] = self.name
326
+ hash['map'] = map
327
+ hash['reduce'] = reduce
328
+ hash.merge! options
329
+
330
+ result = @db.command(hash)
331
+ unless result["ok"] == 1
332
+ raise Mongo::OperationFailure, "map-reduce failed: #{result['errmsg']}"
333
+ end
334
+ @db[result["result"]]
335
+ end
336
+ alias :mapreduce :map_reduce
337
+
338
+ # Performs a group query, similar to the 'SQL GROUP BY' operation.
313
339
  # Returns an array of grouped items.
314
340
  #
315
341
  # :keys :: Array of fields to group by
@@ -327,13 +353,9 @@ module Mongo
327
353
  hash[k] = 1
328
354
  end
329
355
 
330
- case reduce
331
- when Code
332
- else
333
- reduce = Code.new(reduce)
334
- end
356
+ reduce = Code.new(reduce) unless reduce.is_a?(Code)
335
357
 
336
- result = @db.db_command({"group" =>
358
+ result = @db.command({"group" =>
337
359
  {
338
360
  "ns" => @name,
339
361
  "$reduce" => reduce,
@@ -384,7 +406,7 @@ function () {
384
406
  return {"result": map.values()};
385
407
  }
386
408
  EOS
387
- return @db.eval(Code.new(group_function, scope))["result"]
409
+ @db.eval(Code.new(group_function, scope))["result"]
388
410
  end
389
411
 
390
412
  # Returns a list of distinct values for +key+ across all
@@ -406,7 +428,7 @@ EOS
406
428
  command[:distinct] = @name
407
429
  command[:key] = key.to_s
408
430
 
409
- @db.db_command(command)["values"]
431
+ @db.command(command)["values"]
410
432
  end
411
433
 
412
434
  # Rename this collection.
@@ -488,10 +510,10 @@ EOS
488
510
  BSON.serialize_cstr(message, "#{@db.name}.#{collection_name}")
489
511
  documents.each { |doc| message.put_array(BSON_SERIALIZER.serialize(doc, check_keys).unpack("C*")) }
490
512
  if safe
491
- @db.send_message_with_safe_check(Mongo::Constants::OP_INSERT, message,
513
+ @connection.send_message_with_safe_check(Mongo::Constants::OP_INSERT, message, @db.name,
492
514
  "db.#{collection_name}.insert(#{documents.inspect})")
493
515
  else
494
- @db.send_message_with_operation(Mongo::Constants::OP_INSERT, message,
516
+ @connection.send_message(Mongo::Constants::OP_INSERT, message,
495
517
  "db.#{collection_name}.insert(#{documents.inspect})")
496
518
  end
497
519
  documents.collect { |o| o[:_id] || o['_id'] }
@@ -14,98 +14,132 @@
14
14
  # limitations under the License.
15
15
  # ++
16
16
 
17
+ require 'set'
18
+ require 'socket'
19
+ require 'monitor'
20
+
17
21
  module Mongo
18
22
 
19
23
  # A connection to MongoDB.
20
24
  class Connection
21
25
 
26
+ # We need to make sure that all connection abort when
27
+ # a ConnectionError is raised.
28
+ Thread.abort_on_exception = true
29
+
22
30
  DEFAULT_PORT = 27017
31
+ STANDARD_HEADER_SIZE = 16
32
+ RESPONSE_HEADER_SIZE = 20
33
+
34
+ attr_reader :logger, :size, :host, :port, :nodes, :sockets, :checked_out, :reserved_connections
35
+
36
+ def slave_ok?
37
+ @slave_ok
38
+ end
39
+
40
+ # Counter for generating unique request ids.
41
+ @@current_request_id = 0
23
42
 
24
- # Create a Mongo database server instance. You specify either one or a
25
- # pair of servers. If one, you also say if connecting to a slave is
26
- # OK. In either case, the host default is "localhost" and port default
27
- # is DEFAULT_PORT.
28
- #
29
- # If you specify a pair, pair_or_host is a hash with two keys :left
30
- # and :right. Each key maps to either
31
- # * a server name, in which case port is DEFAULT_PORT
32
- # * a port number, in which case server is "localhost"
33
- # * an array containing a server name and a port number in that order
34
- #
35
- # +options+ are passed on to each DB instance:
36
- #
37
- # :slave_ok :: Only used if one host is specified. If false, when
38
- # connecting to that host/port a DB object will check to
39
- # see if the server is the master. If it is not, an error
40
- # is thrown.
41
- #
42
- # :auto_reconnect :: If a DB connection gets closed (for example, we
43
- # have a server pair and saw the "not master"
44
- # error, which closes the connection), then
45
- # automatically try to reconnect to the master or
46
- # to the single server we have been given. Defaults
47
- # to +false+.
43
+ # Creates a connection to MongoDB. Specify either one or a pair of servers,
44
+ # along with a maximum connection pool size and timeout.
45
+ #
46
+ # == Connecting
47
+ # If connecting to just one server, you may specify whether connection to slave is permitted.
48
+ #
49
+ # In all cases, the default host is "localhost" and the default port, is 27017.
50
+ #
51
+ # When specifying a pair, pair_or_host, is a hash with two keys: :left and :right. Each key maps to either
52
+ # * a server name, in which case port is 27017,
53
+ # * a port number, in which case the server is "localhost", or
54
+ # * an array containing [server_name, port_number]
55
+ #
56
+ # === Options
57
+ #
58
+ # :slave_ok :: Defaults to +false+. Must be set to +true+ when connecting
59
+ # to a single, slave node.
60
+ #
48
61
  # :logger :: Optional Logger instance to which driver usage information
49
62
  # will be logged.
50
63
  #
51
- # Since that's so confusing, here are a few examples:
64
+ # :auto_reconnect :: DEPRECATED. See http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby
52
65
  #
53
- # Connection.new # localhost, DEFAULT_PORT, !slave
54
- # Connection.new("localhost") # localhost, DEFAULT_PORT, !slave
55
- # Connection.new("localhost", 3000) # localhost, 3000, slave not ok
56
- # # localhost, 3000, slave ok
57
- # Connection.new("localhost", 3000, :slave_ok => true)
58
- # # localhost, DEFAULT_PORT, auto reconnect
59
- # Connection.new(nil, nil, :auto_reconnect => true)
66
+ # :pool_size :: The maximum number of socket connections that can be opened
67
+ # that can be opened to the database.
68
+ #
69
+ # :timeout :: When all of the connections to the pool are checked out,
70
+ # this is the number of seconds to wait for a new connection
71
+ # to be released before throwing an exception.
72
+ #
73
+ #
74
+ # === Examples:
75
+ #
76
+ # # localhost, 27017
77
+ # Connection.new
78
+ #
79
+ # # localhost, 27017
80
+ # Connection.new("localhost")
81
+ #
82
+ # # localhost, 3000, max 5 connections, with max 5 seconds of wait time.
83
+ # Connection.new("localhost", 3000, :pool_size => 5, :timeout => 5)
60
84
  #
61
- # # A pair of servers. DB will always talk to the master. On socket
62
- # # error or "not master" error, we will auto-reconnect to the
63
- # # current master.
64
- # Connection.new({:left => ["db1.example.com", 3000],
65
- # :right => "db2.example.com"}, # DEFAULT_PORT
66
- # nil, :auto_reconnect => true)
85
+ # # localhost, 3000, where this node may be a slave
86
+ # Connection.new("localhost", 3000, :slave_ok => true)
67
87
  #
68
- # # Here, :right is localhost/DEFAULT_PORT. No auto-reconnect.
69
- # Connection.new({:left => ["db1.example.com", 3000]})
88
+ # # A pair of servers. The driver will always talk to master.
89
+ # # On connection errors, Mongo::ConnectionFailure will be raised.
90
+ # # See http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby
91
+ # Connection.new({:left => ["db1.example.com", 27017],
92
+ # :right => ["db2.example.com", 27017]})
70
93
  #
71
- # When a DB object first connects to a pair, it will find the master
72
- # instance and connect to that one.
94
+ # A pair of servers, with connection pooling. Not the nil param placeholder for port.
95
+ # Connection.new({:left => ["db1.example.com", 27017],
96
+ # :right => ["db2.example.com", 27017]}, nil,
97
+ # :pool_size => 20, :timeout => 5)
73
98
  def initialize(pair_or_host=nil, port=nil, options={})
74
- @pair = case pair_or_host
75
- when String
76
- [[pair_or_host, port ? port.to_i : DEFAULT_PORT]]
77
- when Hash
78
- connections = []
79
- connections << pair_val_to_connection(pair_or_host[:left])
80
- connections << pair_val_to_connection(pair_or_host[:right])
81
- connections
82
- when nil
83
- [['localhost', DEFAULT_PORT]]
84
- end
85
-
86
- @options = options
87
- end
88
-
89
- # Return the Mongo::DB named +db_name+. The slave_ok and
90
- # auto_reconnect options passed in via #new may be overridden here.
91
- # See DB#new for other options you can pass in.
92
- def db(db_name, options={})
93
- DB.new(db_name, @pair, @options.merge(options))
94
- end
95
-
96
- def logger
97
- @options[:logger]
99
+ @nodes = format_pair(pair_or_host)
100
+
101
+ # Host and port of current master.
102
+ @host = @port = nil
103
+
104
+ # Lock for request ids.
105
+ @id_lock = Mutex.new
106
+
107
+ # Pool size and timeout.
108
+ @size = options[:pool_size] || 1
109
+ @timeout = options[:timeout] || 1.0
110
+
111
+ # Cache of reserved sockets mapped to threads
112
+ @reserved_connections = {}
113
+
114
+ # Mutex for synchronizing pool access
115
+ @connection_mutex = Monitor.new
116
+
117
+ # Condition variable for signal and wait
118
+ @queue = @connection_mutex.new_cond
119
+
120
+ @sockets = []
121
+ @checked_out = []
122
+
123
+ if options[:auto_reconnect]
124
+ warn(":auto_reconnect is deprecated. see http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby")
125
+ end
126
+
127
+ # Slave ok can be true only if one node is specified
128
+ @slave_ok = options[:slave_ok] && @nodes.length == 1
129
+ @logger = options[:logger] || nil
130
+ @options = options
131
+
132
+ should_connect = options[:connect].nil? ? true : options[:connect]
133
+ connect_to_master if should_connect
98
134
  end
99
135
 
100
- # Returns a hash containing database names as keys and disk space for
101
- # each as values.
136
+ # Returns a hash with all database names and their respective sizes on
137
+ # disk.
102
138
  def database_info
103
- doc = single_db_command('admin', :listDatabases => 1)
104
- h = {}
105
- doc['databases'].each { |db|
106
- h[db['name']] = db['sizeOnDisk'].to_i
107
- }
108
- h
139
+ doc = self['admin'].command(:listDatabases => 1)
140
+ returning({}) do |info|
141
+ doc['databases'].each { |db| info[db['name']] = db['sizeOnDisk'].to_i }
142
+ end
109
143
  end
110
144
 
111
145
  # Returns an array of database names.
@@ -113,9 +147,20 @@ module Mongo
113
147
  database_info.keys
114
148
  end
115
149
 
150
+ # Returns the database named +db_name+. The slave_ok and
151
+ # See DB#new for other options you can pass in.
152
+ def db(db_name, options={})
153
+ DB.new(db_name, self, options.merge(:logger => @logger))
154
+ end
155
+
156
+ # Returns the database named +db_name+.
157
+ def [](db_name)
158
+ DB.new(db_name, self, :logger => @logger)
159
+ end
160
+
116
161
  # Drops the database +name+.
117
162
  def drop_database(name)
118
- single_db_command(name, :dropDatabase => 1)
163
+ self[name].command(:dropDatabase => 1)
119
164
  end
120
165
 
121
166
  # Copies the database +from+ on the local server to +to+ on the specified +host+.
@@ -126,22 +171,373 @@ module Mongo
126
171
  oh[:fromhost] = host
127
172
  oh[:fromdb] = from
128
173
  oh[:todb] = to
129
- single_db_command('admin', oh)
174
+ self["admin"].command(oh)
175
+ end
176
+
177
+ # Increments and returns the next available request id.
178
+ def get_request_id
179
+ request_id = ''
180
+ @id_lock.synchronize do
181
+ request_id = @@current_request_id += 1
182
+ end
183
+ request_id
130
184
  end
131
185
 
132
- # Return the build information for the current connection.
186
+ # Returns the build information for the current connection.
133
187
  def server_info
134
188
  db("admin").command({:buildinfo => 1}, {:admin => true, :check_response => true})
135
189
  end
136
190
 
137
- # Returns the build version of the current server, using
138
- # a ServerVersion object for comparability.
191
+ # Gets the build version of the current server.
192
+ # Returns a ServerVersion object for comparability.
139
193
  def server_version
140
194
  ServerVersion.new(server_info["version"])
141
195
  end
142
196
 
143
- protected
144
197
 
198
+ ## Connections and pooling ##
199
+
200
+ # Sends a message to MongoDB.
201
+ #
202
+ # Takes a MongoDB opcode, +operation+, a message of class ByteBuffer,
203
+ # +message+, and an optional formatted +log_message+.
204
+ # Sends the message to the databse, adding the necessary headers.
205
+ def send_message(operation, message, log_message=nil)
206
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
207
+ packed_message = add_message_headers(operation, message).to_s
208
+ socket = checkout
209
+ send_message_on_socket(packed_message, socket)
210
+ checkin(socket)
211
+ end
212
+
213
+ # Sends a message to the database, waits for a response, and raises
214
+ # and exception if the operation has failed.
215
+ #
216
+ # Takes a MongoDB opcode, +operation+, a message of class ByteBuffer,
217
+ # +message+, the +db_name+, and an optional formatted +log_message+.
218
+ # Sends the message to the databse, adding the necessary headers.
219
+ def send_message_with_safe_check(operation, message, db_name, log_message=nil)
220
+ message_with_headers = add_message_headers(operation, message)
221
+ message_with_check = last_error_message(db_name)
222
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
223
+ sock = checkout
224
+ packed_message = message_with_headers.append!(message_with_check).to_s
225
+ send_message_on_socket(packed_message, sock)
226
+ docs, num_received, cursor_id = receive(sock)
227
+ checkin(sock)
228
+ if num_received == 1 && error = docs[0]['err']
229
+ raise Mongo::OperationFailure, error
230
+ end
231
+ [docs, num_received, cursor_id]
232
+ end
233
+
234
+ # Sends a message to the database and waits for the response.
235
+ #
236
+ # Takes a MongoDB opcode, +operation+, a message of class ByteBuffer,
237
+ # +message+, and an optional formatted +log_message+. This method
238
+ # also takes an options socket for internal use with #connect_to_master.
239
+ def receive_message(operation, message, log_message=nil, socket=nil)
240
+ packed_message = add_message_headers(operation, message).to_s
241
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
242
+ sock = socket || checkout
243
+
244
+ send_message_on_socket(packed_message, sock)
245
+ result = receive(sock)
246
+ checkin(sock)
247
+ result
248
+ end
249
+
250
+ # Creates a new socket and tries to connect to master.
251
+ # If successful, sets @host and @port to master and returns the socket.
252
+ def connect_to_master
253
+ close
254
+ @host = @port = nil
255
+ for node_pair in @nodes
256
+ host, port = *node_pair
257
+ begin
258
+ socket = TCPSocket.new(host, port)
259
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
260
+
261
+ # If we're connected to master, set the @host and @port
262
+ result = self['admin'].command({:ismaster => 1}, false, false, socket)
263
+ if result['ok'] == 1 && ((is_master = result['ismaster'] == 1) || @slave_ok)
264
+ @host, @port = host, port
265
+ end
266
+
267
+ # Note: slave_ok can be true only when connecting to a single node.
268
+ if @nodes.length == 1 && !is_master && !@slave_ok
269
+ raise ConfigurationError, "Trying to connect directly to slave; " +
270
+ "if this is what you want, specify :slave_ok => true."
271
+ end
272
+
273
+ break if is_master || @slave_ok
274
+ rescue SocketError, SystemCallError, IOError => ex
275
+ socket.close if socket
276
+ false
277
+ end
278
+ end
279
+ raise ConnectionFailure, "failed to connect to any given host:port" unless socket
280
+ end
281
+
282
+ # Are we connected to MongoDB? This is determined by checking whether
283
+ # @host and @port have values, since they're set to nil on calls to #close.
284
+ def connected?
285
+ @host && @port
286
+ end
287
+
288
+ # Close the connection to the database.
289
+ def close
290
+ @sockets.each do |sock|
291
+ sock.close
292
+ end
293
+ @host = @port = nil
294
+ @sockets.clear
295
+ @checked_out.clear
296
+ @reserved_connections.clear
297
+ end
298
+
299
+ private
300
+
301
+ # Get a socket from the pool, mapped to the current thread.
302
+ def checkout
303
+ #return @socket ||= checkout_new_socket if @size == 1
304
+ if sock = @reserved_connections[Thread.current.object_id]
305
+ sock
306
+ else
307
+ sock = obtain_socket
308
+ @reserved_connections[Thread.current.object_id] = sock
309
+ end
310
+ sock
311
+ end
312
+
313
+ # Return a socket to the pool.
314
+ def checkin(socket)
315
+ @connection_mutex.synchronize do
316
+ @reserved_connections.delete Thread.current.object_id
317
+ @checked_out.delete(socket)
318
+ @queue.signal
319
+ end
320
+ true
321
+ end
322
+
323
+ # Releases the connection for any dead threads.
324
+ # Called when the connection pool grows too large to free up more sockets.
325
+ def clear_stale_cached_connections!
326
+ keys = @reserved_connections.keys
327
+
328
+ Thread.list.each do |thread|
329
+ keys.delete(thread.object_id) if thread.alive?
330
+ end
331
+
332
+ keys.each do |key|
333
+ next unless @reserved_connections.has_key?(key)
334
+ checkin(@reserved_connections[key])
335
+ @reserved_connections.delete(key)
336
+ end
337
+ end
338
+
339
+ # Adds a new socket to the pool and checks it out.
340
+ #
341
+ # This method is called exclusively from #obtain_socket;
342
+ # therefore, it runs within a mutex, as it must.
343
+ def checkout_new_socket
344
+ begin
345
+ socket = TCPSocket.new(@host, @port)
346
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
347
+ rescue => ex
348
+ raise ConnectionFailure, "Failed to connect socket: #{ex}"
349
+ end
350
+ @sockets << socket
351
+ @checked_out << socket
352
+ socket
353
+ end
354
+
355
+ # Checks out the first available socket from the pool.
356
+ #
357
+ # This method is called exclusively from #obtain_socket;
358
+ # therefore, it runs within a mutex, as it must.
359
+ def checkout_existing_socket
360
+ socket = (@sockets - @checked_out).first
361
+ @checked_out << socket
362
+ socket
363
+ end
364
+
365
+ # Check out an existing socket or create a new socket if the maximum
366
+ # pool size has not been exceeded. Otherwise, wait for the next
367
+ # available socket.
368
+ def obtain_socket
369
+ @connection_mutex.synchronize do
370
+ connect_to_master if !connected?
371
+
372
+ loop do
373
+ socket = if @checked_out.size < @sockets.size
374
+ checkout_existing_socket
375
+ elsif @sockets.size < @size
376
+ checkout_new_socket
377
+ end
378
+
379
+ return socket if socket
380
+
381
+ # Try to clear out any stale threads to free up some connections
382
+ clear_stale_cached_connections!
383
+ next if @checked_out.size < @sockets.size
384
+
385
+ # Otherwise, wait.
386
+ if wait
387
+ next
388
+ else
389
+
390
+ # Try to clear stale threads once more before failing.
391
+ clear_stale_cached_connections!
392
+ if @size == @sockets.size
393
+ raise ConnectionTimeoutError, "could not obtain connection within " +
394
+ "#{@timeout} seconds. The max pool size is currently #{@size}; " +
395
+ "consider increasing it."
396
+ end
397
+ end # if
398
+ end # loop
399
+ end # synchronize
400
+ end
401
+
402
+ if RUBY_VERSION >= '1.9'
403
+ # Ruby 1.9's Condition Variables don't support timeouts yet;
404
+ # until they do, we'll make do with this hack.
405
+ def wait
406
+ Timeout.timeout(@timeout) do
407
+ @queue.wait
408
+ end
409
+ end
410
+ else
411
+ def wait
412
+ @queue.wait(@timeout)
413
+ end
414
+ end
415
+
416
+ def receive(sock)
417
+ receive_header(sock)
418
+ number_received, cursor_id = receive_response_header(sock)
419
+ read_documents(number_received, cursor_id, sock)
420
+ end
421
+
422
+ def receive_header(sock)
423
+ header = ByteBuffer.new
424
+ header.put_array(receive_message_on_socket(16, sock).unpack("C*"))
425
+ unless header.size == STANDARD_HEADER_SIZE
426
+ raise "Short read for DB response header: " +
427
+ "expected #{STANDARD_HEADER_SIZE} bytes, saw #{header.size}"
428
+ end
429
+ header.rewind
430
+ size = header.get_int
431
+ request_id = header.get_int
432
+ response_to = header.get_int
433
+ op = header.get_int
434
+ end
435
+
436
+ def receive_response_header(sock)
437
+ header_buf = ByteBuffer.new
438
+ header_buf.put_array(receive_message_on_socket(RESPONSE_HEADER_SIZE, sock).unpack("C*"))
439
+ if header_buf.length != RESPONSE_HEADER_SIZE
440
+ raise "Short read for DB response header; " +
441
+ "expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}"
442
+ end
443
+ header_buf.rewind
444
+ result_flags = header_buf.get_int
445
+ cursor_id = header_buf.get_long
446
+ starting_from = header_buf.get_int
447
+ number_remaining = header_buf.get_int
448
+ [number_remaining, cursor_id]
449
+ end
450
+
451
+ def read_documents(number_received, cursor_id, sock)
452
+ docs = []
453
+ number_remaining = number_received
454
+ while number_remaining > 0 do
455
+ buf = ByteBuffer.new
456
+ buf.put_array(receive_message_on_socket(4, sock).unpack("C*"))
457
+ buf.rewind
458
+ size = buf.get_int
459
+ buf.put_array(receive_message_on_socket(size - 4, sock).unpack("C*"), 4)
460
+ number_remaining -= 1
461
+ buf.rewind
462
+ docs << BSON.new.deserialize(buf)
463
+ end
464
+ [docs, number_received, cursor_id]
465
+ end
466
+
467
+ def last_error_message(db_name)
468
+ message = ByteBuffer.new
469
+ message.put_int(0)
470
+ BSON.serialize_cstr(message, "#{db_name}.$cmd")
471
+ message.put_int(0)
472
+ message.put_int(-1)
473
+ message.put_array(BSON_SERIALIZER.serialize({:getlasterror => 1}, false).unpack("C*"))
474
+ add_message_headers(Mongo::Constants::OP_QUERY, message)
475
+ end
476
+
477
+ # Prepares a message for transmission to MongoDB by
478
+ # constructing a valid message header.
479
+ def add_message_headers(operation, message)
480
+ headers = ByteBuffer.new
481
+
482
+ # Message size.
483
+ headers.put_int(16 + message.size)
484
+
485
+ # Unique request id.
486
+ headers.put_int(get_request_id)
487
+
488
+ # Response id.
489
+ headers.put_int(0)
490
+
491
+ # Opcode.
492
+ headers.put_int(operation)
493
+ message.prepend!(headers)
494
+ end
495
+
496
+ # Low-level method for sending a message on a socket.
497
+ # Requires a packed message and an available socket,
498
+ def send_message_on_socket(packed_message, socket)
499
+ begin
500
+ socket.send(packed_message, 0)
501
+ rescue => ex
502
+ close
503
+ raise ConnectionFailure, "Operation failed with the following exception: #{ex}"
504
+ end
505
+ end
506
+
507
+ # Low-level method for receiving data from socket.
508
+ # Requires length and an available socket.
509
+ def receive_message_on_socket(length, socket)
510
+ message = ""
511
+ begin
512
+ while message.length < length do
513
+ chunk = socket.recv(length - message.length)
514
+ raise ConnectionFailure, "connection closed" unless chunk.length > 0
515
+ message += chunk
516
+ end
517
+ rescue => ex
518
+ raise ConnectionFailure, "Operation failed with the following exception: #{ex}"
519
+ end
520
+ message
521
+ end
522
+
523
+
524
+ ## Private helper methods
525
+
526
+ # Returns an array of host-port pairs.
527
+ def format_pair(pair_or_host)
528
+ case pair_or_host
529
+ when String
530
+ [[pair_or_host, port ? port.to_i : DEFAULT_PORT]]
531
+ when Hash
532
+ connections = []
533
+ connections << pair_val_to_connection(pair_or_host[:left])
534
+ connections << pair_val_to_connection(pair_or_host[:right])
535
+ connections
536
+ when nil
537
+ [['localhost', DEFAULT_PORT]]
538
+ end
539
+ end
540
+
145
541
  # Turns an array containing a host name string and a
146
542
  # port number integer into a [host, port] pair array.
147
543
  def pair_val_to_connection(a)
@@ -157,19 +553,5 @@ module Mongo
157
553
  end
158
554
  end
159
555
 
160
- # Send cmd (a hash, possibly ordered) to the admin database and return
161
- # the answer. Raises an error unless the return is "ok" (DB#ok?
162
- # returns +true+).
163
- def single_db_command(db_name, cmd)
164
- db = nil
165
- begin
166
- db = db(db_name)
167
- doc = db.db_command(cmd)
168
- raise "error retrieving database info: #{doc.inspect}" unless db.ok?(doc)
169
- doc
170
- ensure
171
- db.close if db
172
- end
173
- end
174
556
  end
175
557
  end