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