mongo 1.4.0 → 1.5.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 (49) hide show
  1. data/docs/HISTORY.md +15 -0
  2. data/docs/REPLICA_SETS.md +19 -7
  3. data/lib/mongo.rb +1 -0
  4. data/lib/mongo/collection.rb +1 -1
  5. data/lib/mongo/connection.rb +29 -351
  6. data/lib/mongo/cursor.rb +88 -6
  7. data/lib/mongo/gridfs/grid.rb +4 -2
  8. data/lib/mongo/gridfs/grid_file_system.rb +4 -2
  9. data/lib/mongo/networking.rb +345 -0
  10. data/lib/mongo/repl_set_connection.rb +236 -191
  11. data/lib/mongo/util/core_ext.rb +45 -0
  12. data/lib/mongo/util/logging.rb +5 -0
  13. data/lib/mongo/util/node.rb +6 -4
  14. data/lib/mongo/util/pool.rb +73 -26
  15. data/lib/mongo/util/pool_manager.rb +100 -30
  16. data/lib/mongo/util/uri_parser.rb +29 -21
  17. data/lib/mongo/version.rb +1 -1
  18. data/test/bson/binary_test.rb +6 -8
  19. data/test/bson/bson_test.rb +1 -0
  20. data/test/bson/ordered_hash_test.rb +2 -0
  21. data/test/bson/test_helper.rb +0 -17
  22. data/test/collection_test.rb +22 -0
  23. data/test/connection_test.rb +1 -1
  24. data/test/cursor_test.rb +3 -3
  25. data/test/load/thin/load.rb +4 -7
  26. data/test/replica_sets/basic_test.rb +46 -0
  27. data/test/replica_sets/connect_test.rb +35 -58
  28. data/test/replica_sets/count_test.rb +15 -6
  29. data/test/replica_sets/insert_test.rb +6 -7
  30. data/test/replica_sets/query_test.rb +4 -6
  31. data/test/replica_sets/read_preference_test.rb +112 -8
  32. data/test/replica_sets/refresh_test.rb +66 -36
  33. data/test/replica_sets/refresh_with_threads_test.rb +55 -0
  34. data/test/replica_sets/replication_ack_test.rb +3 -6
  35. data/test/replica_sets/rs_test_helper.rb +12 -6
  36. data/test/replica_sets/threading_test.rb +111 -0
  37. data/test/test_helper.rb +9 -2
  38. data/test/threading_test.rb +14 -6
  39. data/test/tools/repl_set_manager.rb +55 -40
  40. data/test/unit/collection_test.rb +2 -1
  41. data/test/unit/connection_test.rb +8 -8
  42. data/test/unit/grid_test.rb +4 -2
  43. data/test/unit/pool_manager_test.rb +1 -0
  44. data/test/unit/read_test.rb +17 -5
  45. data/test/uri_test.rb +9 -4
  46. metadata +13 -28
  47. data/test/replica_sets/connection_string_test.rb +0 -29
  48. data/test/replica_sets/pooled_insert_test.rb +0 -58
  49. data/test/replica_sets/query_secondaries.rb +0 -109
@@ -58,3 +58,48 @@ class String
58
58
  end
59
59
 
60
60
  end
61
+
62
+ #:nodoc:
63
+ class Class
64
+ def mongo_thread_local_accessor name, options = {}
65
+ m = Module.new
66
+ m.module_eval do
67
+ class_variable_set :"@@#{name}", Hash.new {|h,k| h[k] = options[:default] }
68
+ end
69
+ m.module_eval %{
70
+
71
+ def #{name}
72
+ @@#{name}[Thread.current.object_id]
73
+ end
74
+
75
+ def #{name}=(val)
76
+ @@#{name}[Thread.current.object_id] = val
77
+ end
78
+ }
79
+
80
+ class_eval do
81
+ include m
82
+ extend m
83
+ end
84
+ end
85
+ end
86
+
87
+ # Fix a bug in the interaction of
88
+ # mutexes and timeouts in Ruby 1.9.
89
+ # See https://jira.mongodb.org/browse/RUBY-364 for details.
90
+ if RUBY_VERSION > '1.9'
91
+ class Mutex
92
+ def lock_with_hack
93
+ lock_without_hack
94
+ rescue ThreadError => e
95
+ if e.message != "deadlock; recursive locking"
96
+ raise
97
+ else
98
+ unlock
99
+ lock_without_hack
100
+ end
101
+ end
102
+ alias_method :lock_without_hack, :lock
103
+ alias_method :lock, :lock_with_hack
104
+ end
105
+ end
@@ -1,6 +1,11 @@
1
1
  module Mongo
2
2
  module Logging
3
3
 
4
+ def write_logging_startup_message
5
+ log(:warn, "Please note that logging negatively impacts client-side performance. " +
6
+ "You should set your logging level no lower than :info in production.")
7
+ end
8
+
4
9
  # Log a message with the given level.
5
10
  def log(level, msg)
6
11
  return unless @logger
@@ -1,7 +1,8 @@
1
1
  module Mongo
2
2
  class Node
3
3
 
4
- attr_accessor :host, :port, :address, :config, :connection, :socket
4
+ attr_accessor :host, :port, :address, :config, :connection, :socket,
5
+ :last_state
5
6
 
6
7
  def initialize(connection, data)
7
8
  @connection = connection
@@ -13,6 +14,7 @@ module Mongo
13
14
  end
14
15
  @address = "#{host}:#{port}"
15
16
  @config = nil
17
+ @socket = nil
16
18
  end
17
19
 
18
20
  def eql?(other)
@@ -57,11 +59,11 @@ module Mongo
57
59
  end
58
60
 
59
61
  def close
60
- if @socket
62
+ if @socket && !@socket.closed?
61
63
  @socket.close
62
- @socket = nil
63
- @config = nil
64
64
  end
65
+ @socket = nil
66
+ @config = nil
65
67
  end
66
68
 
67
69
  def connected?
@@ -17,12 +17,13 @@
17
17
 
18
18
  module Mongo
19
19
  class Pool
20
- PING_ATTEMPTS = 6
20
+ PING_ATTEMPTS = 6
21
+ MAX_PING_TIME = 1_000_000
21
22
 
22
- attr_accessor :host, :port, :size, :timeout, :safe, :checked_out, :connection
23
+ attr_accessor :host, :port, :address,
24
+ :size, :timeout, :safe, :checked_out, :connection
23
25
 
24
26
  # Create a new pool of connections.
25
- #
26
27
  def initialize(connection, host, port, opts={})
27
28
  @connection = connection
28
29
 
@@ -31,8 +32,11 @@ module Mongo
31
32
  # A Mongo::Node object.
32
33
  @node = opts[:node]
33
34
 
35
+ # The string address
36
+ @address = "#{@host}:#{@port}"
37
+
34
38
  # Pool size and timeout.
35
- @size = opts[:size] || 1
39
+ @size = opts[:size] || 10000
36
40
  @timeout = opts[:timeout] || 5.0
37
41
 
38
42
  # Mutex for synchronizing pool access
@@ -47,26 +51,42 @@ module Mongo
47
51
  @sockets = []
48
52
  @pids = {}
49
53
  @checked_out = []
54
+ @threads = {}
50
55
  @ping_time = nil
51
56
  @last_ping = nil
57
+ @closed = false
58
+ @last_pruning = Time.now
52
59
  end
53
60
 
54
- def close
61
+ # Close this pool.
62
+ #
63
+ # @option opts [Boolean] :soft (false) If true,
64
+ # close only those sockets that are not checked out.
65
+ def close(opts={})
55
66
  @connection_mutex.synchronize do
56
- @sockets.each do |sock|
67
+ if opts[:soft]
68
+ sockets_to_close = @sockets - @checked_out
69
+ else
70
+ sockets_to_close = @sockets
71
+ end
72
+ sockets_to_close.each do |sock|
57
73
  begin
58
- sock.close
74
+ sock.close unless sock.closed?
59
75
  rescue IOError => ex
60
76
  warn "IOError when attempting to close socket connected to #{@host}:#{@port}: #{ex.inspect}"
61
77
  end
62
78
  end
63
- @host = @port = nil
64
79
  @sockets.clear
65
80
  @pids.clear
66
81
  @checked_out.clear
82
+ @closed = true
67
83
  end
68
84
  end
69
85
 
86
+ def closed?
87
+ @closed
88
+ end
89
+
70
90
  def inspect
71
91
  "#<Mongo::Pool:0x#{self.object_id.to_s(16)} @host=#{@host} @port=#{port} " +
72
92
  "@ping_time=#{@ping_time} #{@checked_out.size}/#{@size} sockets available.>"
@@ -98,14 +118,12 @@ module Mongo
98
118
  # to do a round-trip against this node.
99
119
  def refresh_ping_time
100
120
  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
121
+ PING_ATTEMPTS.times do
122
+ t1 = Time.now
123
+ if !self.ping
124
+ return MAX_PING_TIME
125
+ end
126
+ trials << (Time.now - t1) * 1000
109
127
  end
110
128
 
111
129
  trials.sort!
@@ -120,11 +138,22 @@ module Mongo
120
138
  (total / trials.length).ceil
121
139
  end
122
140
 
141
+ def ping
142
+ begin
143
+ return self.connection['admin'].command({:ping => 1}, :socket => @node.socket)
144
+ rescue OperationFailure, SocketError, SystemCallError, IOError => ex
145
+ return false
146
+ end
147
+ end
148
+
123
149
  # Return a socket to the pool.
124
150
  def checkin(socket)
125
151
  @connection_mutex.synchronize do
126
- @checked_out.delete(socket)
127
- @queue.signal
152
+ if @checked_out.delete(socket)
153
+ @queue.signal
154
+ else
155
+ return false
156
+ end
128
157
  end
129
158
  true
130
159
  end
@@ -150,6 +179,7 @@ module Mongo
150
179
  @sockets << socket
151
180
  @pids[socket] = Process.pid
152
181
  @checked_out << socket
182
+ @threads[socket] = Thread.current.object_id
153
183
  socket
154
184
  end
155
185
 
@@ -195,10 +225,26 @@ module Mongo
195
225
  checkout_new_socket
196
226
  else
197
227
  @checked_out << socket
228
+ @threads[socket] = Thread.current.object_id
198
229
  socket
199
230
  end
200
231
  end
201
232
 
233
+ # If we have more sockets than the soft limit specified
234
+ # by the max pool size, then we should prune those
235
+ # extraneous sockets.
236
+ #
237
+ # Note: this must be called from within a mutex.
238
+ def prune
239
+ surplus = @size - @sockets.size
240
+ return if surplus <= 0
241
+ idle_sockets = @sockets - @checked_out
242
+ [surplus, idle_sockets.length].min.times do |n|
243
+ idle_sockets[n].close
244
+ @sockets.delete(idle_sockets[n])
245
+ end
246
+ end
247
+
202
248
  # Check out an existing socket or create a new socket if the maximum
203
249
  # pool size has not been exceeded. Otherwise, wait for the next
204
250
  # available socket.
@@ -213,20 +259,21 @@ module Mongo
213
259
  end
214
260
 
215
261
  @connection_mutex.synchronize do
262
+ #prune
263
+
216
264
  socket = if @checked_out.size < @sockets.size
217
265
  checkout_existing_socket
218
- elsif @sockets.size < @size
266
+ else
219
267
  checkout_new_socket
220
268
  end
221
269
 
222
270
  if socket
223
-
224
- # This calls all procs, in order, scoped to existing sockets.
225
- # At the moment, we use this to lazily authenticate and
226
- # logout existing socket connections.
227
- @socket_ops[socket].reject! do |op|
228
- op.call
229
- end
271
+ # This calls all procs, in order, scoped to existing sockets.
272
+ # At the moment, we use this to lazily authenticate and
273
+ # logout existing socket connections.
274
+ @socket_ops[socket].reject! do |op|
275
+ op.call
276
+ end
230
277
 
231
278
  return socket
232
279
  else
@@ -3,13 +3,13 @@ module Mongo
3
3
 
4
4
  attr_reader :connection, :seeds, :arbiters, :primary, :secondaries,
5
5
  :primary_pool, :read_pool, :secondary_pools, :hosts, :nodes, :max_bson_size,
6
- :tags_to_pools, :members
6
+ :tags_to_pools, :tag_map, :members
7
7
 
8
8
  def initialize(connection, seeds)
9
9
  @connection = connection
10
10
  @seeds = seeds
11
- @refresh_node = nil
12
11
  @previously_connected = false
12
+ @refresh_required = false
13
13
  end
14
14
 
15
15
  def inspect
@@ -25,28 +25,69 @@ module Mongo
25
25
  members = connect_to_members
26
26
  initialize_pools(members)
27
27
  update_seed_list(members)
28
+ set_read_pool
29
+ set_tag_mappings
28
30
 
29
31
  @members = members
30
32
  @previously_connected = true
31
33
  end
32
34
 
33
- def healthy?
34
- if !@refresh_node || !refresh_node.set_config
35
- return false
35
+ # We're healthy if all members are pingable and if the view
36
+ # of the replica set returned by isMaster is equivalent
37
+ # to our view. If any of these isn't the case,
38
+ # set @refresh_require to true, and return.
39
+ def check_connection_health
40
+ begin
41
+ seed = get_valid_seed_node
42
+ rescue ConnectionFailure
43
+ @refresh_required = true
44
+ return
45
+ end
46
+
47
+ config = seed.set_config
48
+ if !config
49
+ @refresh_required = true
50
+ seed.close
51
+ return
52
+ end
53
+
54
+ if config['hosts'].length != @members.length
55
+ @refresh_required = true
56
+ seed.close
57
+ return
36
58
  end
37
59
 
38
- #if refresh_node.node_list
60
+ config['hosts'].each do |host|
61
+ member = @members.detect do |m|
62
+ m.address == host
63
+ end
64
+
65
+ if member && validate_existing_member(member)
66
+ next
67
+ else
68
+ @refresh_required = true
69
+ seed.close
70
+ return false
71
+ end
72
+ end
73
+
74
+ seed.close
75
+ end
76
+
77
+ # The replica set connection should initiate a full refresh.
78
+ def refresh_required?
79
+ @refresh_required
39
80
  end
40
81
 
41
- def close
82
+ def close(opts={})
42
83
  begin
43
84
  if @primary_pool
44
- @primary_pool.close
85
+ @primary_pool.close(opts)
45
86
  end
46
87
 
47
88
  if @secondary_pools
48
89
  @secondary_pools.each do |pool|
49
- pool.close
90
+ pool.close(opts)
50
91
  end
51
92
  end
52
93
 
@@ -62,6 +103,26 @@ module Mongo
62
103
 
63
104
  private
64
105
 
106
+ def validate_existing_member(member)
107
+ config = member.set_config
108
+ if !config
109
+ return false
110
+ else
111
+ if member.primary?
112
+ if member.last_state == :primary
113
+ return true
114
+ else # This node is now primary, but didn't used to be.
115
+ return false
116
+ end
117
+ elsif member.last_state == :secondary &&
118
+ member.secondary?
119
+ return true
120
+ else # This node isn't what it used to be.
121
+ return false
122
+ end
123
+ end
124
+ end
125
+
65
126
  def initialize_data
66
127
  @primary = nil
67
128
  @primary_pool = nil
@@ -72,6 +133,7 @@ module Mongo
72
133
  @hosts = Set.new
73
134
  @members = Set.new
74
135
  @tags_to_pools = {}
136
+ @tag_map = {}
75
137
  end
76
138
 
77
139
  # Connect to each member of the replica set
@@ -110,40 +172,46 @@ module Mongo
110
172
  @hosts << member.host_string
111
173
 
112
174
  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)
175
+ assign_primary(member)
119
176
  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)
177
+ assign_secondary(member)
127
178
  end
128
179
  end
129
180
 
130
-
131
181
  @max_bson_size = members.first.config['maxBsonObjectSize'] ||
132
182
  Mongo::DEFAULT_MAX_BSON_SIZE
133
183
  @arbiters = members.first.arbiters
184
+ end
134
185
 
135
- set_read_pool
136
- set_primary_tag_pools
186
+ def assign_primary(member)
187
+ member.last_state = :primary
188
+ @primary = member.host_port
189
+ @primary_pool = Pool.new(self.connection, member.host, member.port,
190
+ :size => self.connection.pool_size,
191
+ :timeout => self.connection.pool_timeout,
192
+ :node => member)
193
+ associate_tags_with_pool(member.tags, @primary_pool)
194
+ end
195
+
196
+ def assign_secondary(member)
197
+ member.last_state = :secondary
198
+ @secondaries << member.host_port
199
+ pool = Pool.new(self.connection, member.host, member.port,
200
+ :size => self.connection.pool_size,
201
+ :timeout => self.connection.pool_timeout,
202
+ :node => member)
203
+ @secondary_pools << pool
204
+ associate_tags_with_pool(member.tags, pool)
137
205
  end
138
206
 
139
207
  # If there's more than one pool associated with
140
208
  # 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|
209
+ def set_tag_mappings
210
+ @tags_to_pools.each do |key, pool_list|
143
211
  if pool_list.length == 1
144
- @tags_to_pools[k] = pool_list.first
212
+ @tag_map[key] = pool_list.first
145
213
  else
146
- @tags_to_pools[k] = nearby_pool_from_set(pool_list)
214
+ @tag_map[key] = nearby_pool_from_set(pool_list)
147
215
  end
148
216
  end
149
217
  end
@@ -189,7 +257,9 @@ module Mongo
189
257
  def get_valid_seed_node
190
258
  @seeds.each do |seed|
191
259
  node = Mongo::Node.new(self.connection, seed)
192
- if node.connect && node.set_config
260
+ if !node.connect
261
+ next
262
+ elsif node.set_config
193
263
  return node
194
264
  else
195
265
  node.close