mongo 1.3.1 → 1.4.0

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 (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