mongo 1.6.4 → 1.7.0.rc0

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.
Files changed (50) hide show
  1. data/README.md +13 -13
  2. data/Rakefile +7 -10
  3. data/docs/{GridFS.md → GRID_FS.md} +0 -0
  4. data/docs/HISTORY.md +16 -0
  5. data/docs/READ_PREFERENCE.md +70 -10
  6. data/docs/TUTORIAL.md +2 -2
  7. data/lib/mongo.rb +2 -0
  8. data/lib/mongo/collection.rb +62 -11
  9. data/lib/mongo/connection.rb +31 -41
  10. data/lib/mongo/cursor.rb +42 -86
  11. data/lib/mongo/db.rb +10 -8
  12. data/lib/mongo/networking.rb +30 -65
  13. data/lib/mongo/repl_set_connection.rb +91 -170
  14. data/lib/mongo/sharded_connection.rb +221 -0
  15. data/lib/mongo/util/node.rb +29 -36
  16. data/lib/mongo/util/pool.rb +10 -3
  17. data/lib/mongo/util/pool_manager.rb +77 -90
  18. data/lib/mongo/util/sharding_pool_manager.rb +143 -0
  19. data/lib/mongo/util/support.rb +22 -2
  20. data/lib/mongo/util/tcp_socket.rb +10 -15
  21. data/lib/mongo/util/uri_parser.rb +17 -10
  22. data/lib/mongo/version.rb +1 -1
  23. data/test/collection_test.rb +133 -1
  24. data/test/connection_test.rb +50 -4
  25. data/test/db_api_test.rb +3 -3
  26. data/test/db_test.rb +6 -1
  27. data/test/replica_sets/basic_test.rb +3 -6
  28. data/test/replica_sets/complex_connect_test.rb +14 -2
  29. data/test/replica_sets/complex_read_preference_test.rb +237 -0
  30. data/test/replica_sets/connect_test.rb +47 -67
  31. data/test/replica_sets/count_test.rb +1 -1
  32. data/test/replica_sets/cursor_test.rb +70 -0
  33. data/test/replica_sets/read_preference_test.rb +171 -118
  34. data/test/replica_sets/refresh_test.rb +3 -3
  35. data/test/replica_sets/refresh_with_threads_test.rb +2 -2
  36. data/test/replica_sets/rs_test_helper.rb +2 -2
  37. data/test/sharded_cluster/basic_test.rb +112 -0
  38. data/test/sharded_cluster/mongo_config_test.rb +126 -0
  39. data/test/sharded_cluster/sc_test_helper.rb +39 -0
  40. data/test/test_helper.rb +3 -3
  41. data/test/threading/threading_with_large_pool_test.rb +1 -1
  42. data/test/tools/mongo_config.rb +307 -0
  43. data/test/tools/repl_set_manager.rb +12 -12
  44. data/test/unit/collection_test.rb +1 -1
  45. data/test/unit/cursor_test.rb +11 -6
  46. data/test/unit/db_test.rb +4 -0
  47. data/test/unit/grid_test.rb +2 -0
  48. data/test/unit/read_test.rb +39 -8
  49. data/test/uri_test.rb +4 -8
  50. metadata +144 -127
@@ -0,0 +1,143 @@
1
+
2
+ module Mongo
3
+ module ShardingNode
4
+ def set_config
5
+ begin
6
+ @config = @connection['admin'].command({:ismaster => 1}, :socket => @socket)
7
+
8
+ # warning: instance variable @logger not initialized
9
+ #if @config['msg'] && @logger
10
+ # @connection.log(:warn, "#{config['msg']}")
11
+ #end
12
+
13
+ rescue ConnectionFailure, OperationFailure, OperationTimeout, SocketError, SystemCallError, IOError => ex
14
+ @connection.log(:warn, "Attempted connection to node #{host_string} raised " +
15
+ "#{ex.class}: #{ex.message}")
16
+
17
+ # Socket may already be nil from issuing command
18
+ if @socket && !@socket.closed?
19
+ @socket.close
20
+ end
21
+
22
+ return nil
23
+ end
24
+
25
+ @config
26
+ end
27
+
28
+ # Return a list of sharded cluster nodes from the config - currently just the current node.
29
+ def node_list
30
+ connect unless connected?
31
+ set_config unless @config
32
+
33
+ return [] unless config
34
+
35
+ ["#{@host}:#{@port}"]
36
+ end
37
+
38
+ end
39
+
40
+ class ShardingPoolManager < PoolManager
41
+
42
+ attr_reader :connection, :primary, :primary_pool, :hosts, :nodes,
43
+ :max_bson_size, :members
44
+
45
+ # Create a new set of connection pools.
46
+ #
47
+ # The pool manager will by default use the original seed list passed
48
+ # to the connection objects, accessible via connection.seeds. In addition,
49
+ # the user may pass an additional list of seeds nodes discovered in real
50
+ # time. The union of these lists will be used when attempting to connect,
51
+ # with the newly-discovered nodes being used first.
52
+ def initialize(connection, seeds=[])
53
+ super
54
+ end
55
+
56
+ def inspect
57
+ "<Mongo::ShardingPoolManager:0x#{self.object_id.to_s(16)} @seeds=#{@seeds}>"
58
+ end
59
+
60
+ # "Best" should be the member with the fastest ping time
61
+ # but connect/connect_to_members reinitializes @members
62
+ def best(members)
63
+ Array(members.first)
64
+ end
65
+
66
+ def connect
67
+ close if @previously_connected
68
+
69
+ initialize_data
70
+ members = connect_to_members
71
+ initialize_pools(best(members))
72
+
73
+ @members = members
74
+ @previously_connected = true
75
+ end
76
+
77
+ # We want to refresh to the member with the fastest ping time
78
+ # but also want to minimize refreshes
79
+ # We're healthy if the primary is pingable. If this isn't the case,
80
+ # or the members have changed, set @refresh_required to true, and return.
81
+ # The config.mongos find can't be part of the connect call chain due to infinite recursion
82
+ def check_connection_health
83
+ begin
84
+ seeds = @connection['config']['mongos'].find.to_a.map{|doc| doc['_id']}
85
+ if @seeds != seeds
86
+ @seeds = seeds
87
+ @refresh_required = true
88
+ end
89
+ rescue Mongo::OperationFailure
90
+ @refresh_required = true
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ # Connect to each member of the sharded cluster
97
+ # as reported by the given seed node, and return
98
+ # as a list of Mongo::Node objects.
99
+ def connect_to_members
100
+ members = []
101
+
102
+ seed = get_valid_seed_node
103
+
104
+ seed.node_list.each do |host|
105
+ node = Mongo::Node.new(self.connection, host)
106
+ node.extend ShardingNode
107
+ if node.connect && node.set_config
108
+ members << node
109
+ end
110
+ end
111
+ seed.close
112
+
113
+ if members.empty?
114
+ raise ConnectionFailure, "Failed to connect to any given member."
115
+ end
116
+
117
+ members
118
+ end
119
+
120
+ # Iterate through the list of provided seed
121
+ # nodes until we've gotten a response from the
122
+ # sharded cluster we're trying to connect to.
123
+ #
124
+ # If we don't get a response, raise an exception.
125
+ def get_valid_seed_node
126
+ @seeds.each do |seed|
127
+ node = Mongo::Node.new(self.connection, seed)
128
+ node.extend ShardingNode
129
+ if !node.connect
130
+ next
131
+ elsif node.set_config && node.healthy?
132
+ return node
133
+ else
134
+ node.close
135
+ end
136
+ end
137
+
138
+ raise ConnectionFailure, "Cannot connect to a sharded cluster using seeds " +
139
+ "#{@seeds.map {|s| "#{s[0]}:#{s[1]}" }.join(', ')}"
140
+ end
141
+
142
+ end
143
+ end
@@ -23,6 +23,13 @@ module Mongo
23
23
  include Mongo::Conversions
24
24
  extend self
25
25
 
26
+ READ_PREFERENCES = [:primary, :primary_preferred, :secondary, :secondary_preferred, :nearest]
27
+
28
+ # Commands that may be sent to replica-set secondaries, depending on
29
+ # read preference and tags. All other commands are always run on the primary.
30
+ SECONDARY_OK_COMMANDS = ['group', 'aggregate', 'collstats', 'dbstats', 'count', 'distinct',
31
+ 'geonear', 'geosearch', 'geowalk']
32
+
26
33
  # Generate an MD5 for authentication.
27
34
  #
28
35
  # @param [String] username
@@ -58,12 +65,25 @@ module Mongo
58
65
  db_name
59
66
  end
60
67
 
68
+ def secondary_ok?(selector)
69
+ command = selector.keys.first.to_s.downcase
70
+
71
+ if command == 'mapreduce'
72
+ map_reduce = selector[command]
73
+ if map_reduce && map_reduce.is_a?(Hash) && map_reduce.has_key?('out')
74
+ map_reduce['out'] == 'inline' ? false : true
75
+ end
76
+ else
77
+ SECONDARY_OK_COMMANDS.member?(command)
78
+ end
79
+ end
80
+
61
81
  def validate_read_preference(value)
62
- if [:primary, :secondary, :secondary_only, nil].include?(value)
82
+ if READ_PREFERENCES.include?(value)
63
83
  return true
64
84
  else
65
85
  raise MongoArgumentError, "#{value} is not a valid read preference. " +
66
- "Please specify either :primary or :secondary or :secondary_only."
86
+ "Please specify one of the following read preferences as a symbol: #{READ_PREFERENCES}"
67
87
  end
68
88
  end
69
89
 
@@ -43,23 +43,18 @@ module Mongo
43
43
  # Block on data to read for @op_timeout seconds
44
44
  begin
45
45
  ready = IO.select([@socket], nil, [@socket], @op_timeout)
46
+ unless ready
47
+ raise OperationTimeout
48
+ end
46
49
  rescue IOError
47
- raise OperationFailure
50
+ raise ConnectionFailure
48
51
  end
49
- if ready
50
- begin
51
- @socket.sysread(maxlen, buffer)
52
- rescue SystemCallError => ex
53
- # Needed because sometimes JRUBY doesn't throw Errno::ECONNRESET as it should
54
- # http://jira.codehaus.org/browse/JRUBY-6180
55
- raise ConnectionFailure, ex
56
- rescue Errno::ENOTCONN, Errno::EBADF, Errno::ECONNRESET, Errno::EPIPE, Errno::ETIMEDOUT, EOFError => ex
57
- raise ConnectionFailure, ex
58
- rescue Errno::EINTR, Errno::EIO, IOError => ex
59
- raise OperationFailure, ex
60
- end
61
- else
62
- raise OperationTimeout
52
+
53
+ # Read data from socket
54
+ begin
55
+ @socket.sysread(maxlen, buffer)
56
+ rescue SystemCallError, IOError => ex
57
+ raise ConnectionFailure, ex
63
58
  end
64
59
  end
65
60
 
@@ -76,7 +76,7 @@ module Mongo
76
76
  :wtimeoutms => lambda {|arg| arg.to_i }
77
77
  }
78
78
 
79
- attr_reader :nodes, :auths, :connect, :replicaset, :slaveok, :safe, :w, :wtimeout, :fsync, :journal, :connecttimeoutms, :sockettimeoutms, :wtimeoutms
79
+ attr_reader :auths, :connect, :replicaset, :slaveok, :safe, :w, :wtimeout, :fsync, :journal, :connecttimeoutms, :sockettimeoutms, :wtimeoutms
80
80
 
81
81
  # Parse a MongoDB URI. This method is used by Connection.from_uri.
82
82
  # Returns an array of nodes and an array of db authorizations, if applicable.
@@ -87,7 +87,7 @@ module Mongo
87
87
  # @param [Hash,nil] extra_opts Extra options. Will override anything already specified in the URI.
88
88
  #
89
89
  # @core connections
90
- def initialize(uri, extra_opts={})
90
+ def initialize(uri)
91
91
  if uri.start_with?('mongodb://')
92
92
  uri = uri[10..-1]
93
93
  else
@@ -96,7 +96,7 @@ module Mongo
96
96
 
97
97
  hosts, opts = uri.split('?')
98
98
  parse_hosts(hosts)
99
- parse_options(opts, extra_opts)
99
+ parse_options(opts)
100
100
  validate_connect
101
101
  end
102
102
 
@@ -105,11 +105,12 @@ module Mongo
105
105
  # @note Don't confuse this with attribute getter method #connect.
106
106
  #
107
107
  # @return [Connection,ReplSetConnection]
108
- def connection
108
+ def connection(extra_opts)
109
+ opts = connection_options.merge! extra_opts
109
110
  if replicaset?
110
- ReplSetConnection.new(*(nodes+[connection_options]))
111
+ ReplSetConnection.new(nodes, opts)
111
112
  else
112
- Connection.new(host, port, connection_options)
113
+ Connection.new(host, port, opts)
113
114
  end
114
115
  end
115
116
 
@@ -207,6 +208,14 @@ module Mongo
207
208
  opts
208
209
  end
209
210
 
211
+ def nodes
212
+ if @nodes.length == 1
213
+ @nodes
214
+ else
215
+ @nodes.collect {|node| "#{node[0]}:#{node[1]}"}
216
+ end
217
+ end
218
+
210
219
  private
211
220
 
212
221
  def parse_hosts(uri_without_proto)
@@ -254,13 +263,13 @@ module Mongo
254
263
 
255
264
  # This method uses the lambdas defined in OPT_VALID and OPT_CONV to validate
256
265
  # and convert the given options.
257
- def parse_options(string_opts, extra_opts={})
266
+ def parse_options(string_opts)
258
267
  # initialize instance variables for available options
259
268
  OPT_VALID.keys.each { |k| instance_variable_set("@#{k}", nil) }
260
269
 
261
270
  string_opts ||= ''
262
271
 
263
- return if string_opts.empty? && extra_opts.empty?
272
+ return if string_opts.empty?
264
273
 
265
274
  if string_opts.include?(';') and string_opts.include?('&')
266
275
  raise MongoArgumentError, "must not mix URL separators ; and &"
@@ -272,8 +281,6 @@ module Mongo
272
281
  memo
273
282
  end
274
283
 
275
- opts.merge! extra_opts
276
-
277
284
  opts.each do |key, value|
278
285
  if !OPT_ATTRS.include?(key)
279
286
  raise MongoArgumentError, "Invalid Mongo URI option #{key}"
@@ -1,3 +1,3 @@
1
1
  module Mongo
2
- VERSION = "1.6.4"
2
+ VERSION = "1.7.0.rc0"
3
3
  end
@@ -247,7 +247,7 @@ class TestCollection < Test::Unit::TestCase
247
247
  def test_maximum_insert_size
248
248
  docs = []
249
249
  16.times do
250
- docs << {'foo' => 'a' * 1_000_000}
250
+ docs << {'foo' => 'a' * 1024 * 1024}
251
251
  end
252
252
 
253
253
  assert_raise InvalidOperation do
@@ -290,6 +290,16 @@ class TestCollection < Test::Unit::TestCase
290
290
  assert_equal 1, @@test.find_one(:_id => id2)["x"]
291
291
  end
292
292
 
293
+ def test_update_check_keys
294
+ @@test.save("x" => 1)
295
+ @@test.update({"x" => 1}, {"$set" => {"a.b" => 2}})
296
+ assert_equal 2, @@test.find_one("x" => 1)["a"]["b"]
297
+
298
+ assert_raise_error BSON::InvalidKeyName, "key a.b must not contain '.'" do
299
+ @@test.update({"x" => 1}, {"a.b" => 3})
300
+ end
301
+ end
302
+
293
303
  if @@version >= "1.1.3"
294
304
  def test_multi_update
295
305
  @@test.save("num" => 10)
@@ -525,6 +535,128 @@ class TestCollection < Test::Unit::TestCase
525
535
  end
526
536
  assert c.closed?
527
537
  end
538
+
539
+ def setup_aggregate_data
540
+ # save some data
541
+ @@test.save( {
542
+ "_id" => 1,
543
+ "title" => "this is my title",
544
+ "author" => "bob",
545
+ "posted" => Time.utc(1500),
546
+ "pageViews" => 5 ,
547
+ "tags" => [ "fun" , "good" , "fun" ],
548
+ "comments" => [
549
+ { "author" => "joe", "text" => "this is cool" },
550
+ { "author" => "sam", "text" => "this is bad" }
551
+ ],
552
+ "other" => { "foo" => 5 }
553
+ } )
554
+
555
+ @@test.save( {
556
+ "_id" => 2,
557
+ "title" => "this is your title",
558
+ "author" => "dave",
559
+ "posted" => Time.utc(1600),
560
+ "pageViews" => 7,
561
+ "tags" => [ "fun" , "nasty" ],
562
+ "comments" => [
563
+ { "author" => "barbara" , "text" => "this is interesting" },
564
+ { "author" => "jenny", "text" => "i like to play pinball", "votes" => 10 }
565
+ ],
566
+ "other" => { "bar" => 14 }
567
+ })
568
+
569
+ @@test.save( {
570
+ "_id" => 3,
571
+ "title" => "this is some other title",
572
+ "author" => "jane",
573
+ "posted" => Time.utc(1700),
574
+ "pageViews" => 6 ,
575
+ "tags" => [ "nasty", "filthy" ],
576
+ "comments" => [
577
+ { "author" => "will" , "text" => "i don't like the color" } ,
578
+ { "author" => "jenny" , "text" => "can i get that in green?" }
579
+ ],
580
+ "other" => { "bar" => 14 }
581
+ })
582
+
583
+ end
584
+
585
+ if @@version > '2.1.1'
586
+ def test_reponds_to_aggregate
587
+ assert_respond_to @@test, :aggregate
588
+ end
589
+
590
+ def test_aggregate_requires_arguments
591
+ assert_raise MongoArgumentError do
592
+ @@test.aggregate()
593
+ end
594
+ end
595
+
596
+ def test_aggregate_requires_valid_arguments
597
+ assert_raise MongoArgumentError do
598
+ @@test.aggregate({})
599
+ end
600
+ end
601
+
602
+ def test_aggregate_pipeline_operator_format
603
+ assert_raise Mongo::OperationFailure do
604
+ @@test.aggregate([{"$project" => "_id"}])
605
+ end
606
+ end
607
+
608
+ def test_aggregate_pipeline_operators_using_strings
609
+ setup_aggregate_data
610
+ desired_results = [ {"_id"=>1, "pageViews"=>5, "tags"=>["fun", "good", "fun"]},
611
+ {"_id"=>2, "pageViews"=>7, "tags"=>["fun", "nasty"]},
612
+ {"_id"=>3, "pageViews"=>6, "tags"=>["nasty", "filthy"]} ]
613
+ results = @@test.aggregate([{"$project" => {"tags" => 1, "pageViews" => 1}}])
614
+ assert_equal desired_results, results
615
+ end
616
+
617
+ def test_aggregate_pipeline_operators_using_symbols
618
+ setup_aggregate_data
619
+ desired_results = [ {"_id"=>1, "pageViews"=>5, "tags"=>["fun", "good", "fun"]},
620
+ {"_id"=>2, "pageViews"=>7, "tags"=>["fun", "nasty"]},
621
+ {"_id"=>3, "pageViews"=>6, "tags"=>["nasty", "filthy"]} ]
622
+ results = @@test.aggregate([{"$project" => {:tags => 1, :pageViews => 1}}])
623
+ assert_equal desired_results, results
624
+ end
625
+
626
+ def test_aggregate_pipeline_multiple_operators
627
+ setup_aggregate_data
628
+ results = @@test.aggregate([{"$project" => {"tags" => 1, "pageViews" => 1}}, {"$match" => {"pageViews" => 7}}])
629
+ assert_equal 1, results.length
630
+ end
631
+
632
+ def test_aggregate_pipeline_unwind
633
+ setup_aggregate_data
634
+ desired_results = [ {"_id"=>1, "title"=>"this is my title", "author"=>"bob", "posted"=>Time.utc(1500),
635
+ "pageViews"=>5, "tags"=>"fun", "comments"=>[{"author"=>"joe", "text"=>"this is cool"},
636
+ {"author"=>"sam", "text"=>"this is bad"}], "other"=>{"foo"=>5 } },
637
+ {"_id"=>1, "title"=>"this is my title", "author"=>"bob", "posted"=>Time.utc(1500),
638
+ "pageViews"=>5, "tags"=>"good", "comments"=>[{"author"=>"joe", "text"=>"this is cool"},
639
+ {"author"=>"sam", "text"=>"this is bad"}], "other"=>{"foo"=>5 } },
640
+ {"_id"=>1, "title"=>"this is my title", "author"=>"bob", "posted"=>Time.utc(1500),
641
+ "pageViews"=>5, "tags"=>"fun", "comments"=>[{"author"=>"joe", "text"=>"this is cool"},
642
+ {"author"=>"sam", "text"=>"this is bad"}], "other"=>{"foo"=>5 } },
643
+ {"_id"=>2, "title"=>"this is your title", "author"=>"dave", "posted"=>Time.utc(1600),
644
+ "pageViews"=>7, "tags"=>"fun", "comments"=>[{"author"=>"barbara", "text"=>"this is interesting"},
645
+ {"author"=>"jenny", "text"=>"i like to play pinball", "votes"=>10 }], "other"=>{"bar"=>14 } },
646
+ {"_id"=>2, "title"=>"this is your title", "author"=>"dave", "posted"=>Time.utc(1600),
647
+ "pageViews"=>7, "tags"=>"nasty", "comments"=>[{"author"=>"barbara", "text"=>"this is interesting"},
648
+ {"author"=>"jenny", "text"=>"i like to play pinball", "votes"=>10 }], "other"=>{"bar"=>14 } },
649
+ {"_id"=>3, "title"=>"this is some other title", "author"=>"jane", "posted"=>Time.utc(1700),
650
+ "pageViews"=>6, "tags"=>"nasty", "comments"=>[{"author"=>"will", "text"=>"i don't like the color"},
651
+ {"author"=>"jenny", "text"=>"can i get that in green?"}], "other"=>{"bar"=>14 } },
652
+ {"_id"=>3, "title"=>"this is some other title", "author"=>"jane", "posted"=>Time.utc(1700),
653
+ "pageViews"=>6, "tags"=>"filthy", "comments"=>[{"author"=>"will", "text"=>"i don't like the color"},
654
+ {"author"=>"jenny", "text"=>"can i get that in green?"}], "other"=>{"bar"=>14 } }
655
+ ]
656
+ results = @@test.aggregate([{"$unwind"=> "$tags"}])
657
+ assert_equal desired_results, results
658
+ end
659
+ end
528
660
 
529
661
  if @@version > "1.1.1"
530
662
  def test_map_reduce