mongo 1.6.4 → 1.7.0.rc0

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