mongo 0.17.1 → 0.18

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