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.
- data/README.rdoc +69 -47
- data/Rakefile +26 -0
- data/lib/mongo.rb +2 -2
- data/lib/mongo/admin.rb +3 -3
- data/lib/mongo/collection.rb +51 -29
- data/lib/mongo/connection.rb +476 -94
- data/lib/mongo/cursor.rb +19 -17
- data/lib/mongo/db.rb +52 -324
- data/lib/mongo/errors.rb +9 -0
- data/lib/mongo/gridfs/grid_store.rb +1 -1
- data/lib/mongo/util/conversions.rb +4 -36
- data/test/replica/count_test.rb +34 -0
- data/test/replica/insert_test.rb +50 -0
- data/test/replica/pooled_insert_test.rb +54 -0
- data/test/replica/query_test.rb +39 -0
- data/test/test_collection.rb +23 -10
- data/test/test_connection.rb +19 -20
- data/test/test_conversions.rb +2 -2
- data/test/test_cursor.rb +18 -18
- data/test/test_db.rb +26 -35
- data/test/test_db_api.rb +9 -30
- data/test/test_helper.rb +15 -0
- data/test/test_slave_connection.rb +5 -4
- data/test/test_threading.rb +2 -2
- data/test/threading/test_threading_large_pool.rb +90 -0
- data/test/unit/collection_test.rb +13 -15
- data/test/unit/connection_test.rb +122 -0
- data/test/unit/cursor_test.rb +4 -32
- data/test/unit/db_test.rb +4 -32
- metadata +8 -2
data/README.rdoc
CHANGED
@@ -1,34 +1,33 @@
|
|
1
1
|
= Introduction
|
2
2
|
|
3
|
-
This is
|
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
|
6
|
-
|
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
|
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
|
-
|
21
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
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
|
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
|
92
|
-
system. An instance of GridStore is like an IO object. See the
|
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
|
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
|
-
|
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
|
-
|
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
|
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"
|
data/lib/mongo.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
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.
|
34
|
+
VERSION = "0.18"
|
35
35
|
end
|
data/lib/mongo/admin.rb
CHANGED
@@ -30,7 +30,7 @@ module Mongo
|
|
30
30
|
def profiling_level
|
31
31
|
oh = OrderedHash.new
|
32
32
|
oh[:profile] = -1
|
33
|
-
doc = @db.
|
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.
|
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.
|
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)
|
data/lib/mongo/collection.rb
CHANGED
@@ -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
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
#
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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
|
-
@
|
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
|
-
@
|
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'] }
|
data/lib/mongo/connection.rb
CHANGED
@@ -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
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
# and
|
31
|
-
#
|
32
|
-
#
|
33
|
-
# *
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
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
|
-
#
|
64
|
+
# :auto_reconnect :: DEPRECATED. See http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby
|
52
65
|
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
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
|
-
# #
|
62
|
-
#
|
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
|
-
# #
|
69
|
-
#
|
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
|
-
#
|
72
|
-
#
|
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
|
-
@
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
101
|
-
#
|
136
|
+
# Returns a hash with all database names and their respective sizes on
|
137
|
+
# disk.
|
102
138
|
def database_info
|
103
|
-
doc =
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|