mongo 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/README.md +9 -6
  2. data/Rakefile +3 -4
  3. data/docs/HISTORY.md +20 -2
  4. data/docs/READ_PREFERENCE.md +39 -0
  5. data/docs/RELEASES.md +1 -1
  6. data/docs/REPLICA_SETS.md +23 -2
  7. data/docs/TAILABLE_CURSORS.md +51 -0
  8. data/docs/TUTORIAL.md +4 -4
  9. data/docs/WRITE_CONCERN.md +5 -2
  10. data/lib/mongo.rb +7 -22
  11. data/lib/mongo/collection.rb +96 -29
  12. data/lib/mongo/connection.rb +107 -62
  13. data/lib/mongo/cursor.rb +136 -57
  14. data/lib/mongo/db.rb +26 -5
  15. data/lib/mongo/exceptions.rb +17 -1
  16. data/lib/mongo/gridfs/grid.rb +1 -1
  17. data/lib/mongo/repl_set_connection.rb +273 -156
  18. data/lib/mongo/util/logging.rb +42 -0
  19. data/lib/mongo/util/node.rb +183 -0
  20. data/lib/mongo/util/pool.rb +76 -13
  21. data/lib/mongo/util/pool_manager.rb +208 -0
  22. data/lib/mongo/util/ssl_socket.rb +38 -0
  23. data/lib/mongo/util/support.rb +9 -1
  24. data/lib/mongo/util/timeout.rb +42 -0
  25. data/lib/mongo/version.rb +3 -0
  26. data/mongo.gemspec +2 -2
  27. data/test/bson/binary_test.rb +1 -1
  28. data/test/bson/bson_string_test.rb +30 -0
  29. data/test/bson/bson_test.rb +6 -3
  30. data/test/bson/byte_buffer_test.rb +1 -1
  31. data/test/bson/hash_with_indifferent_access_test.rb +1 -1
  32. data/test/bson/json_test.rb +1 -1
  33. data/test/bson/object_id_test.rb +2 -18
  34. data/test/bson/ordered_hash_test.rb +38 -3
  35. data/test/bson/test_helper.rb +46 -0
  36. data/test/bson/timestamp_test.rb +32 -10
  37. data/test/collection_test.rb +89 -3
  38. data/test/connection_test.rb +35 -20
  39. data/test/cursor_test.rb +63 -2
  40. data/test/db_test.rb +12 -2
  41. data/test/pool_test.rb +21 -0
  42. data/test/replica_sets/connect_test.rb +26 -13
  43. data/test/replica_sets/connection_string_test.rb +1 -4
  44. data/test/replica_sets/count_test.rb +1 -0
  45. data/test/replica_sets/insert_test.rb +1 -0
  46. data/test/replica_sets/pooled_insert_test.rb +4 -1
  47. data/test/replica_sets/query_secondaries.rb +2 -1
  48. data/test/replica_sets/query_test.rb +2 -1
  49. data/test/replica_sets/read_preference_test.rb +43 -0
  50. data/test/replica_sets/refresh_test.rb +123 -0
  51. data/test/replica_sets/replication_ack_test.rb +9 -4
  52. data/test/replica_sets/rs_test_helper.rb +2 -2
  53. data/test/timeout_test.rb +14 -0
  54. data/test/tools/repl_set_manager.rb +134 -23
  55. data/test/unit/collection_test.rb +6 -8
  56. data/test/unit/connection_test.rb +4 -4
  57. data/test/unit/cursor_test.rb +23 -5
  58. data/test/unit/db_test.rb +2 -0
  59. data/test/unit/grid_test.rb +2 -0
  60. data/test/unit/node_test.rb +73 -0
  61. data/test/unit/pool_manager_test.rb +47 -0
  62. data/test/unit/read_test.rb +101 -0
  63. metadata +214 -138
  64. data/lib/mongo/test.rb +0 -20
  65. data/test/async/collection_test.rb +0 -224
  66. data/test/async/connection_test.rb +0 -24
  67. data/test/async/cursor_test.rb +0 -162
  68. data/test/async/worker_pool_test.rb +0 -99
  69. data/test/load/resque/load.rb +0 -21
  70. data/test/load/resque/processor.rb +0 -26
  71. data/test/load/unicorn/unicorn.rb +0 -29
  72. data/test/tools/load.rb +0 -58
  73. data/test/tools/sharding_manager.rb +0 -202
  74. data/test/tools/test.rb +0 -4
  75. data/test/unit/repl_set_connection_test.rb +0 -59
@@ -0,0 +1,42 @@
1
+ module Mongo
2
+ module Logging
3
+
4
+ # Log a message with the given level.
5
+ def log(level, msg)
6
+ return unless @logger
7
+ case level
8
+ when :debug then
9
+ @logger.debug "MONGODB [DEBUG] #{msg}"
10
+ when :warn then
11
+ @logger.warn "MONGODB [WARNING] #{msg}"
12
+ when :error then
13
+ @logger.error "MONGODB [ERROR] #{msg}"
14
+ when :fatal then
15
+ @logger.fatal "MONGODB [FATAL] #{msg}"
16
+ else
17
+ @logger.info "MONGODB [INFO] #{msg}"
18
+ end
19
+ end
20
+
21
+ # Execute the block and log the operation described by name and payload.
22
+ def instrument(name, payload = {}, &blk)
23
+ res = yield
24
+ log_operation(name, payload)
25
+ res
26
+ end
27
+
28
+ protected
29
+
30
+ def log_operation(name, payload)
31
+ @logger ||= nil
32
+ return unless @logger
33
+ msg = "#{payload[:database]}['#{payload[:collection]}'].#{name}("
34
+ msg += payload.values_at(:selector, :document, :documents, :fields ).compact.map(&:inspect).join(', ') + ")"
35
+ msg += ".skip(#{payload[:skip]})" if payload[:skip]
36
+ msg += ".limit(#{payload[:limit]})" if payload[:limit]
37
+ msg += ".sort(#{payload[:order]})" if payload[:order]
38
+ @logger.debug "MONGODB #{msg}"
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,183 @@
1
+ module Mongo
2
+ class Node
3
+
4
+ attr_accessor :host, :port, :address, :config, :connection, :socket
5
+
6
+ def initialize(connection, data)
7
+ @connection = connection
8
+ if data.is_a?(String)
9
+ @host, @port = split_nodes(data)
10
+ else
11
+ @host = data[0]
12
+ @port = data[1].nil? ? Connection::DEFAULT_PORT : data[1].to_i
13
+ end
14
+ @address = "#{host}:#{port}"
15
+ @config = nil
16
+ end
17
+
18
+ def eql?(other)
19
+ other.is_a?(Node) && host == other.host && port == other.port
20
+ end
21
+ alias :== :eql?
22
+
23
+ def host_string
24
+ address
25
+ end
26
+
27
+ def inspect
28
+ "<Mongo::Node:0x#{self.object_id.to_s(16)} @host=#{@host} @port=#{@port}>"
29
+ end
30
+
31
+ # Create a connection to the provided node,
32
+ # and, if successful, return the socket. Otherwise,
33
+ # return nil.
34
+ def connect
35
+ begin
36
+ socket = nil
37
+ if @connection.connect_timeout
38
+ Mongo::TimeoutHandler.timeout(@connection.connect_timeout, OperationTimeout) do
39
+ socket = @connection.socket_class.new(@host, @port)
40
+ end
41
+ else
42
+ socket = @connection.socket_class.new(@host, @port)
43
+ end
44
+
45
+ if socket.nil?
46
+ return nil
47
+ else
48
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
49
+ end
50
+ rescue OperationTimeout, OperationFailure, SocketError, SystemCallError, IOError => ex
51
+ @connection.log(:debug, "Failed connection to #{host_string} with #{ex.class}, #{ex.message}.")
52
+ socket.close if socket
53
+ return nil
54
+ end
55
+
56
+ @socket = socket
57
+ end
58
+
59
+ def close
60
+ if @socket
61
+ @socket.close
62
+ @socket = nil
63
+ @config = nil
64
+ end
65
+ end
66
+
67
+ def connected?
68
+ @socket != nil
69
+ end
70
+
71
+ def active?
72
+ begin
73
+ result = @connection['admin'].command({:ping => 1}, :socket => @socket)
74
+ return result['ok'] == 1
75
+ rescue OperationFailure, SocketError, SystemCallError, IOError => ex
76
+ return nil
77
+ end
78
+ end
79
+
80
+ # Get the configuration for the provided node as returned by the
81
+ # ismaster command. Additionally, check that the replica set name
82
+ # matches with the name provided.
83
+ def set_config
84
+ begin
85
+ @config = @connection['admin'].command({:ismaster => 1}, :socket => @socket)
86
+
87
+ if @config['msg'] && @logger
88
+ @connection.log(:warn, "#{config['msg']}")
89
+ end
90
+
91
+ check_set_membership(config)
92
+ check_set_name(config)
93
+ rescue ConnectionFailure, OperationFailure, SocketError, SystemCallError, IOError => ex
94
+ @connection.log(:warn, "Attempted connection to node #{host_string} raised " +
95
+ "#{ex.class}: #{ex.message}")
96
+ return nil
97
+ end
98
+
99
+ @config
100
+ end
101
+
102
+ # Return a list of replica set nodes from the config.
103
+ # Note: this excludes arbiters.
104
+ def node_list
105
+ connect unless connected?
106
+ set_config unless @config
107
+
108
+ return [] unless config
109
+
110
+ nodes = []
111
+ nodes += config['hosts'] if config['hosts']
112
+ nodes += config['passives'] if config['passives']
113
+ nodes
114
+ end
115
+
116
+ def arbiters
117
+ connect unless connected?
118
+ set_config unless @config
119
+ return [] unless config['arbiters']
120
+
121
+ config['arbiters'].map do |arbiter|
122
+ split_nodes(arbiter)
123
+ end
124
+ end
125
+
126
+ def tags
127
+ connect unless connected?
128
+ set_config unless @config
129
+ return {} unless config['tags'] && !config['tags'].empty?
130
+
131
+ config['tags']
132
+ end
133
+
134
+ def primary?
135
+ @config['ismaster'] == true || @config['ismaster'] == 1
136
+ end
137
+
138
+ def secondary?
139
+ @config['secondary'] == true || @config['secondary'] == 1
140
+ end
141
+
142
+ def host_port
143
+ [@host, @port]
144
+ end
145
+
146
+ def hash
147
+ address.hash
148
+ end
149
+
150
+ private
151
+
152
+ def split_nodes(host_string)
153
+ data = host_string.split(":")
154
+ host = data[0]
155
+ port = data[1].nil? ? Connection::DEFAULT_PORT : data[1].to_i
156
+
157
+ [host, port]
158
+ end
159
+
160
+ # Ensure that this node is a member of a replica set.
161
+ def check_set_membership(config)
162
+ if !config['hosts']
163
+ message = "Will not connect to #{host_string} because it's not a member " +
164
+ "of a replica set."
165
+ raise ConnectionFailure, message
166
+ end
167
+ end
168
+
169
+ # Ensure that this node is part of a replica set of the expected name.
170
+ def check_set_name(config)
171
+ if @connection.replica_set_name
172
+ if !config['setName']
173
+ @connection.log(:warn, "Could not verify replica set name for member #{host_string} " +
174
+ "because ismaster does not return name in this version of MongoDB")
175
+ elsif @connection.replica_set_name != config['setName']
176
+ message = "Attempting to connect to replica set '#{config['setName']}' on member #{host_string} " +
177
+ "but expected '#{@connection.replica_set_name}'"
178
+ raise ReplicaSetConnectionError, message
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -17,8 +17,9 @@
17
17
 
18
18
  module Mongo
19
19
  class Pool
20
+ PING_ATTEMPTS = 6
20
21
 
21
- attr_accessor :host, :port, :size, :timeout, :safe, :checked_out
22
+ attr_accessor :host, :port, :size, :timeout, :safe, :checked_out, :connection
22
23
 
23
24
  # Create a new pool of connections.
24
25
  #
@@ -27,6 +28,9 @@ module Mongo
27
28
 
28
29
  @host, @port = host, port
29
30
 
31
+ # A Mongo::Node object.
32
+ @node = opts[:node]
33
+
30
34
  # Pool size and timeout.
31
35
  @size = opts[:size] || 1
32
36
  @timeout = opts[:timeout] || 5.0
@@ -43,20 +47,77 @@ module Mongo
43
47
  @sockets = []
44
48
  @pids = {}
45
49
  @checked_out = []
50
+ @ping_time = nil
51
+ @last_ping = nil
46
52
  end
47
53
 
48
54
  def close
49
- @sockets.each do |sock|
50
- begin
51
- sock.close
52
- rescue IOError => ex
53
- warn "IOError when attempting to close socket connected to #{@host}:#{@port}: #{ex.inspect}"
55
+ @connection_mutex.synchronize do
56
+ @sockets.each do |sock|
57
+ begin
58
+ sock.close
59
+ rescue IOError => ex
60
+ warn "IOError when attempting to close socket connected to #{@host}:#{@port}: #{ex.inspect}"
61
+ end
54
62
  end
63
+ @host = @port = nil
64
+ @sockets.clear
65
+ @pids.clear
66
+ @checked_out.clear
55
67
  end
56
- @host = @port = nil
57
- @sockets.clear
58
- @pids.clear
59
- @checked_out.clear
68
+ end
69
+
70
+ def inspect
71
+ "#<Mongo::Pool:0x#{self.object_id.to_s(16)} @host=#{@host} @port=#{port} " +
72
+ "@ping_time=#{@ping_time} #{@checked_out.size}/#{@size} sockets available.>"
73
+ end
74
+
75
+ def host_string
76
+ "#{@host}:#{@port}"
77
+ end
78
+
79
+ def host_port
80
+ [@host, @port]
81
+ end
82
+
83
+ # Refresh ping time only if we haven't
84
+ # checked within the last five minutes.
85
+ def ping_time
86
+ if !@last_ping
87
+ @last_ping = Time.now
88
+ @ping_time = refresh_ping_time
89
+ elsif Time.now - @last_ping > 300
90
+ @last_ping = Time.now
91
+ @ping_time = refresh_ping_time
92
+ else
93
+ @ping_time
94
+ end
95
+ end
96
+
97
+ # Return the time it takes on average
98
+ # to do a round-trip against this node.
99
+ def refresh_ping_time
100
+ trials = []
101
+ begin
102
+ PING_ATTEMPTS.times do
103
+ t1 = Time.now
104
+ self.connection['admin'].command({:ping => 1}, :socket => @node.socket)
105
+ trials << (Time.now - t1) * 1000
106
+ end
107
+ rescue OperationFailure, SocketError, SystemCallError, IOError => ex
108
+ return nil
109
+ end
110
+
111
+ trials.sort!
112
+
113
+ # Delete shortest and longest times
114
+ trials.delete_at(trials.length-1)
115
+ trials.delete_at(0)
116
+
117
+ total = 0.0
118
+ trials.each { |t| total += t }
119
+
120
+ (total / trials.length).ceil
60
121
  end
61
122
 
62
123
  # Return a socket to the pool.
@@ -74,10 +135,12 @@ module Mongo
74
135
  # therefore, it runs within a mutex.
75
136
  def checkout_new_socket
76
137
  begin
77
- socket = TCPSocket.new(@host, @port)
78
- socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
138
+ socket = self.connection.socket_class.new(@host, @port)
139
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
79
140
  rescue => ex
141
+ socket.close if socket
80
142
  raise ConnectionFailure, "Failed to connect to host #{@host} and port #{@port}: #{ex}"
143
+ @node.close if @node
81
144
  end
82
145
 
83
146
  # If any saved authentications exist, we want to apply those
@@ -128,7 +191,7 @@ module Mongo
128
191
  if @pids[socket] != Process.pid
129
192
  @pids[socket] = nil
130
193
  @sockets.delete(socket)
131
- socket.close
194
+ socket.close if socket
132
195
  checkout_new_socket
133
196
  else
134
197
  @checked_out << socket
@@ -0,0 +1,208 @@
1
+ module Mongo
2
+ class PoolManager
3
+
4
+ attr_reader :connection, :seeds, :arbiters, :primary, :secondaries,
5
+ :primary_pool, :read_pool, :secondary_pools, :hosts, :nodes, :max_bson_size,
6
+ :tags_to_pools, :members
7
+
8
+ def initialize(connection, seeds)
9
+ @connection = connection
10
+ @seeds = seeds
11
+ @refresh_node = nil
12
+ @previously_connected = false
13
+ end
14
+
15
+ def inspect
16
+ "<Mongo::PoolManager:0x#{self.object_id.to_s(16)} @seeds=#{@seeds}>"
17
+ end
18
+
19
+ def connect
20
+ if @previously_connected
21
+ close
22
+ end
23
+
24
+ initialize_data
25
+ members = connect_to_members
26
+ initialize_pools(members)
27
+ update_seed_list(members)
28
+
29
+ @members = members
30
+ @previously_connected = true
31
+ end
32
+
33
+ def healthy?
34
+ if !@refresh_node || !refresh_node.set_config
35
+ return false
36
+ end
37
+
38
+ #if refresh_node.node_list
39
+ end
40
+
41
+ def close
42
+ begin
43
+ if @primary_pool
44
+ @primary_pool.close
45
+ end
46
+
47
+ if @secondary_pools
48
+ @secondary_pools.each do |pool|
49
+ pool.close
50
+ end
51
+ end
52
+
53
+ if @members
54
+ @members.each do |member|
55
+ member.close
56
+ end
57
+ end
58
+
59
+ rescue ConnectionFailure
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def initialize_data
66
+ @primary = nil
67
+ @primary_pool = nil
68
+ @read_pool = nil
69
+ @arbiters = []
70
+ @secondaries = []
71
+ @secondary_pools = []
72
+ @hosts = Set.new
73
+ @members = Set.new
74
+ @tags_to_pools = {}
75
+ end
76
+
77
+ # Connect to each member of the replica set
78
+ # as reported by the given seed node, and return
79
+ # as a list of Mongo::Node objects.
80
+ def connect_to_members
81
+ members = []
82
+
83
+ seed = get_valid_seed_node
84
+
85
+ seed.node_list.each do |host|
86
+ node = Mongo::Node.new(self.connection, host)
87
+ if node.connect && node.set_config
88
+ members << node
89
+ end
90
+ end
91
+ seed.close
92
+
93
+ if members.empty?
94
+ raise ConnectionFailure, "Failed to connect to any given member."
95
+ end
96
+
97
+ members
98
+ end
99
+
100
+ def associate_tags_with_pool(tags, pool)
101
+ tags.each_key do |key|
102
+ @tags_to_pools[{key => tags[key]}] ||= []
103
+ @tags_to_pools[{key => tags[key]}] << pool
104
+ end
105
+ end
106
+
107
+ # Initialize the connection pools for the primary and secondary nodes.
108
+ def initialize_pools(members)
109
+ members.each do |member|
110
+ @hosts << member.host_string
111
+
112
+ if member.primary?
113
+ @primary = member.host_port
114
+ @primary_pool = Pool.new(self.connection, member.host, member.port,
115
+ :size => self.connection.pool_size,
116
+ :timeout => self.connection.connect_timeout,
117
+ :node => member)
118
+ associate_tags_with_pool(member.tags, @primary_pool)
119
+ elsif member.secondary? && !@secondaries.include?(member.host_port)
120
+ @secondaries << member.host_port
121
+ pool = Pool.new(self.connection, member.host, member.port,
122
+ :size => self.connection.pool_size,
123
+ :timeout => self.connection.connect_timeout,
124
+ :node => member)
125
+ @secondary_pools << pool
126
+ associate_tags_with_pool(member.tags, pool)
127
+ end
128
+ end
129
+
130
+
131
+ @max_bson_size = members.first.config['maxBsonObjectSize'] ||
132
+ Mongo::DEFAULT_MAX_BSON_SIZE
133
+ @arbiters = members.first.arbiters
134
+
135
+ set_read_pool
136
+ set_primary_tag_pools
137
+ end
138
+
139
+ # If there's more than one pool associated with
140
+ # a given tag, choose a close one using the bucket method.
141
+ def set_primary_tag_pools
142
+ @tags_to_pools.each do |k, pool_list|
143
+ if pool_list.length == 1
144
+ @tags_to_pools[k] = pool_list.first
145
+ else
146
+ @tags_to_pools[k] = nearby_pool_from_set(pool_list)
147
+ end
148
+ end
149
+ end
150
+
151
+ # Pick a node from the set of possible secondaries.
152
+ # If more than one node is available, use the ping
153
+ # time to figure out which nodes to choose from.
154
+ def set_read_pool
155
+ if @secondary_pools.empty?
156
+ @read_pool = @primary_pool
157
+ elsif @secondary_pools.size == 1
158
+ @read_pool = @secondary_pools[0]
159
+ else
160
+ @read_pool = nearby_pool_from_set(@secondary_pools)
161
+ end
162
+ end
163
+
164
+ def nearby_pool_from_set(pool_set)
165
+ ping_ranges = Array.new(3) { |i| Array.new }
166
+ pool_set.each do |pool|
167
+ case pool.ping_time
168
+ when 0..150
169
+ ping_ranges[0] << pool
170
+ when 150..1000
171
+ ping_ranges[1] << pool
172
+ else
173
+ ping_ranges[2] << pool
174
+ end
175
+ end
176
+
177
+ for list in ping_ranges do
178
+ break if !list.empty?
179
+ end
180
+
181
+ list[rand(list.length)]
182
+ end
183
+
184
+ # Iterate through the list of provided seed
185
+ # nodes until we've gotten a response from the
186
+ # replica set we're trying to connect to.
187
+ #
188
+ # If we don't get a response, raise an exception.
189
+ def get_valid_seed_node
190
+ @seeds.each do |seed|
191
+ node = Mongo::Node.new(self.connection, seed)
192
+ if node.connect && node.set_config
193
+ return node
194
+ else
195
+ node.close
196
+ end
197
+ end
198
+
199
+ raise ConnectionFailure, "Cannot connect to a replica set using seeds " +
200
+ "#{@seeds.map {|s| "#{s[0]}:#{s[1]}" }.join(', ')}"
201
+ end
202
+
203
+ def update_seed_list(members)
204
+ @seeds = members.map { |n| n.host_port }
205
+ end
206
+
207
+ end
208
+ end