mongo 1.6.0 → 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,8 +4,11 @@ module Mongo
4
4
  DEBUG_LEVEL = defined?(Logger) ? Logger::DEBUG : 0
5
5
 
6
6
  def write_logging_startup_message
7
- log(:debug, "Please note that logging negatively impacts client-side performance. " +
8
- "You should set your logging level no lower than :info in production.")
7
+ if @logger && (@logger.level == DEBUG_LEVEL)
8
+ log(:debug, "Logging level is currently :debug which could negatively impact " +
9
+ "client-side performance. You should set your logging level no lower than " +
10
+ ":info in production.")
11
+ end
9
12
  end
10
13
 
11
14
  # Log a message with the given level.
@@ -101,12 +101,12 @@ module Mongo
101
101
  rescue ConnectionFailure, OperationFailure, OperationTimeout, SocketError, SystemCallError, IOError => ex
102
102
  @connection.log(:warn, "Attempted connection to node #{host_string} raised " +
103
103
  "#{ex.class}: #{ex.message}")
104
-
104
+
105
105
  # Socket may already be nil from issuing command
106
106
  if @socket && !@socket.closed?
107
107
  @socket.close
108
108
  end
109
-
109
+
110
110
  return nil
111
111
  end
112
112
 
@@ -19,6 +19,7 @@ module Mongo
19
19
  class Pool
20
20
  PING_ATTEMPTS = 6
21
21
  MAX_PING_TIME = 1_000_000
22
+ PRUNE_INTERVAL = 10_000
22
23
 
23
24
  attr_accessor :host, :port, :address,
24
25
  :size, :timeout, :safe, :checked_out, :connection
@@ -36,8 +37,8 @@ module Mongo
36
37
  @address = "#{@host}:#{@port}"
37
38
 
38
39
  # Pool size and timeout.
39
- @size = opts[:size] || 10000
40
- @timeout = opts[:timeout] || 5.0
40
+ @size = opts.fetch(:size, 20)
41
+ @timeout = opts.fetch(:timeout, 30)
41
42
 
42
43
  # Mutex for synchronizing pool access
43
44
  @connection_mutex = Mutex.new
@@ -51,11 +52,11 @@ module Mongo
51
52
  @sockets = []
52
53
  @pids = {}
53
54
  @checked_out = []
54
- @threads = {}
55
55
  @ping_time = nil
56
56
  @last_ping = nil
57
57
  @closed = false
58
- @last_pruning = Time.now
58
+ @threads_to_sockets = {}
59
+ @checkout_counter = 0
59
60
  end
60
61
 
61
62
  # Close this pool.
@@ -64,22 +65,13 @@ module Mongo
64
65
  # close only those sockets that are not checked out.
65
66
  def close(opts={})
66
67
  @connection_mutex.synchronize do
67
- if opts[:soft]
68
- sockets_to_close = @sockets - @checked_out
68
+ if opts[:soft] && !@checked_out.empty?
69
+ @closing = true
70
+ close_sockets(@sockets - @checked_out)
69
71
  else
70
- sockets_to_close = @sockets
72
+ close_sockets(@sockets)
73
+ @closed = true
71
74
  end
72
- sockets_to_close.each do |sock|
73
- begin
74
- sock.close unless sock.closed?
75
- rescue IOError => ex
76
- warn "IOError when attempting to close socket connected to #{@host}:#{@port}: #{ex.inspect}"
77
- end
78
- end
79
- @sockets.clear
80
- @pids.clear
81
- @checked_out.clear
82
- @closed = true
83
75
  end
84
76
  end
85
77
 
@@ -166,6 +158,7 @@ module Mongo
166
158
  begin
167
159
  socket = self.connection.socket_class.new(@host, @port)
168
160
  socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
161
+ socket.pool = self
169
162
  rescue => ex
170
163
  socket.close if socket
171
164
  raise ConnectionFailure, "Failed to connect to host #{@host} and port #{@port}: #{ex}"
@@ -179,7 +172,7 @@ module Mongo
179
172
  @sockets << socket
180
173
  @pids[socket] = Process.pid
181
174
  @checked_out << socket
182
- @threads[socket] = Thread.current.object_id
175
+ @threads_to_sockets[Thread.current] = socket
183
176
  socket
184
177
  end
185
178
 
@@ -216,30 +209,33 @@ module Mongo
216
209
  #
217
210
  # This method is called exclusively from #checkout;
218
211
  # therefore, it runs within a mutex.
219
- def checkout_existing_socket
220
- socket = (@sockets - @checked_out).first
212
+ def checkout_existing_socket(socket=nil)
213
+ if !socket
214
+ socket = (@sockets - @checked_out).first
215
+ end
216
+
221
217
  if @pids[socket] != Process.pid
222
- @pids[socket] = nil
223
- @sockets.delete(socket)
224
- socket.close if socket
225
- checkout_new_socket
218
+ @pids[socket] = nil
219
+ @sockets.delete(socket)
220
+ socket.close if socket
221
+ checkout_new_socket
226
222
  else
227
223
  @checked_out << socket
228
- @threads[socket] = Thread.current.object_id
224
+ @threads_to_sockets[Thread.current] = socket
229
225
  socket
230
226
  end
231
227
  end
232
228
 
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
- idle_sockets = @sockets - @checked_out
240
- idle_sockets.each do |socket|
241
- socket.close unless socket.closed?
242
- @sockets.delete(socket)
229
+ def prune_thread_socket_hash
230
+ map = {}
231
+ Thread.list.each do |t|
232
+ map[t] = 1
233
+ end
234
+
235
+ @threads_to_sockets.keys.each do |key|
236
+ if !map[key]
237
+ @threads_to_sockets.delete(key)
238
+ end
243
239
  end
244
240
  end
245
241
 
@@ -251,21 +247,33 @@ module Mongo
251
247
  start_time = Time.now
252
248
  loop do
253
249
  if (Time.now - start_time) > @timeout
254
- raise ConnectionTimeoutError, "could not obtain connection within " +
255
- "#{@timeout} seconds. The max pool size is currently #{@size}; " +
256
- "consider increasing the pool size or timeout."
250
+ raise ConnectionTimeoutError, "could not obtain connection within " +
251
+ "#{@timeout} seconds. The max pool size is currently #{@size}; " +
252
+ "consider increasing the pool size or timeout."
257
253
  end
258
254
 
259
255
  @connection_mutex.synchronize do
260
- if @sockets.size > @size * 1.5
261
- prune
256
+ if @checkout_counter > PRUNE_INTERVAL
257
+ @checkout_counter = 0
258
+ prune_thread_socket_hash
259
+ else
260
+ @checkout_counter += 1
262
261
  end
263
262
 
264
- socket = if @checked_out.size < @sockets.size
265
- checkout_existing_socket
266
- else
267
- checkout_new_socket
268
- end
263
+ if socket_for_thread = @threads_to_sockets[Thread.current]
264
+ if !@checked_out.include?(socket_for_thread)
265
+ socket = checkout_existing_socket(socket_for_thread)
266
+ end
267
+ else # First checkout for this thread
268
+ thread_length = @threads_to_sockets.keys.length
269
+ if (thread_length <= @sockets.size) && (@sockets.size < @size)
270
+ socket = checkout_new_socket
271
+ elsif @checked_out.size < @sockets.size
272
+ socket = checkout_existing_socket
273
+ elsif @sockets.size < @size
274
+ socket = checkout_new_socket
275
+ end
276
+ end
269
277
 
270
278
  if socket
271
279
  # This calls all procs, in order, scoped to existing sockets.
@@ -275,6 +283,18 @@ module Mongo
275
283
  op.call
276
284
  end
277
285
 
286
+ if socket.closed?
287
+ @checked_out.delete(socket)
288
+ @sockets.delete(socket)
289
+ @threads_to_sockets.each do |k,v|
290
+ if v == socket
291
+ @threads_to_sockets.delete(k)
292
+ end
293
+ end
294
+
295
+ socket = checkout_new_socket
296
+ end
297
+
278
298
  return socket
279
299
  else
280
300
  # Otherwise, wait
@@ -283,5 +303,19 @@ module Mongo
283
303
  end
284
304
  end
285
305
  end
306
+
307
+ private
308
+
309
+ def close_sockets(sockets)
310
+ sockets.each do |socket|
311
+ @sockets.delete(socket)
312
+ begin
313
+ socket.close unless socket.closed?
314
+ rescue IOError => ex
315
+ warn "IOError when attempting to close socket connected to #{@host}:#{@port}: #{ex.inspect}"
316
+ end
317
+ end
318
+ end
319
+
286
320
  end
287
321
  end
@@ -84,6 +84,10 @@ module Mongo
84
84
  @refresh_required
85
85
  end
86
86
 
87
+ def closed?
88
+ pools.all? { |pool| pool.closed? }
89
+ end
90
+
87
91
  def close(opts={})
88
92
  begin
89
93
  if @primary_pool
@@ -114,6 +118,10 @@ module Mongo
114
118
 
115
119
  private
116
120
 
121
+ def pools
122
+ [@primary_pool, *@secondary_pools]
123
+ end
124
+
117
125
  def validate_existing_member(member)
118
126
  config = member.set_config
119
127
  if !config
@@ -7,6 +7,8 @@ module Mongo
7
7
  # mirroring Ruby's TCPSocket, vis., TCPSocket#send and TCPSocket#read.
8
8
  class SSLSocket
9
9
 
10
+ attr_accessor :pool
11
+
10
12
  def initialize(host, port)
11
13
  @socket = ::TCPSocket.new(host, port)
12
14
  @ssl = OpenSSL::SSL::SSLSocket.new(@socket)
@@ -33,6 +35,5 @@ module Mongo
33
35
  def close
34
36
  @ssl.close
35
37
  end
36
-
37
38
  end
38
39
  end
@@ -0,0 +1,6 @@
1
+ module Mongo
2
+ class TCPSocket < ::TCPSocket
3
+ attr_accessor :pool
4
+
5
+ end
6
+ end
@@ -1,3 +1,3 @@
1
1
  module Mongo
2
- VERSION = "1.6.0"
2
+ VERSION = "1.6.1"
3
3
  end
@@ -30,5 +30,5 @@ Gem::Specification.new do |s|
30
30
  s.email = 'mongodb-dev@googlegroups.com'
31
31
  s.homepage = 'http://www.mongodb.org'
32
32
 
33
- s.add_dependency('bson', Mongo::VERSION)
33
+ s.add_dependency('bson', "~> #{Mongo::VERSION}")
34
34
  end
@@ -238,7 +238,7 @@ class TestConnection < Test::Unit::TestCase
238
238
  assert !conn.active?
239
239
 
240
240
  # Simulate a dropped connection.
241
- dropped_socket = Mocha::Mock.new
241
+ dropped_socket = mock('dropped_socket')
242
242
  dropped_socket.stubs(:read).raises(Errno::ECONNRESET)
243
243
  dropped_socket.stubs(:send).raises(Errno::ECONNRESET)
244
244
  dropped_socket.stub_everything
@@ -357,7 +357,7 @@ class TestConnection < Test::Unit::TestCase
357
357
  end
358
358
 
359
359
  should "show a proper exception message if an IOError is raised while closing a socket" do
360
- fake_socket = Mocha::Mock.new
360
+ fake_socket = mock('fake_socket')
361
361
  fake_socket.stubs(:close).raises(IOError.new)
362
362
  fake_socket.stub_everything
363
363
  TCPSocket.stubs(:new).returns(fake_socket)
@@ -0,0 +1,57 @@
1
+ require './test/test_helper'
2
+ require 'thread'
3
+
4
+ class PoolTest < Test::Unit::TestCase
5
+ include Mongo
6
+
7
+ def setup
8
+ @connection = standard_connection
9
+ end
10
+
11
+ def test_pool_affinity
12
+ @pool = Pool.new(@connection, TEST_HOST, TEST_PORT, :size => 5)
13
+
14
+ @threads = []
15
+ @sockets = []
16
+
17
+ 10.times do
18
+ @threads << Thread.new do
19
+ original_socket = @pool.checkout
20
+ @sockets << original_socket
21
+ @pool.checkin(original_socket)
22
+ 5000.times do
23
+ socket = @pool.checkout
24
+ assert_equal original_socket, socket
25
+ @pool.checkin(socket)
26
+ end
27
+ end
28
+ end
29
+
30
+ @threads.each { |t| t.join }
31
+ end
32
+
33
+ def test_pool_thread_pruning
34
+ @pool = Pool.new(@connection, TEST_HOST, TEST_PORT, :size => 5)
35
+
36
+ @threads = []
37
+
38
+ 10.times do
39
+ @threads << Thread.new do
40
+ 50.times do
41
+ socket = @pool.checkout
42
+ @pool.checkin(socket)
43
+ end
44
+ end
45
+ end
46
+
47
+ @threads.each { |t| t.join }
48
+ assert_equal 10, @pool.instance_variable_get(:@threads_to_sockets).size
49
+
50
+ # Thread-socket pool
51
+ 10000.times do
52
+ @pool.checkin(@pool.checkout)
53
+ end
54
+
55
+ assert_equal 1, @pool.instance_variable_get(:@threads_to_sockets).size
56
+ end
57
+ end
@@ -7,7 +7,7 @@ class ReplicaSetPooledInsertTest < Test::Unit::TestCase
7
7
 
8
8
  def setup
9
9
  ensure_rs
10
- @conn = ReplSetConnection.new(build_seeds(3), :pool_size => 5, :timeout => 5, :refresh_mode => false)
10
+ @conn = ReplSetConnection.new(build_seeds(3), :pool_size => 10, :timeout => 5, :refresh_mode => false)
11
11
  @db = @conn.db(MONGO_TEST_DB)
12
12
  @db.drop_collection("test-sets")
13
13
  @coll = @db.collection("test-sets")
@@ -50,7 +50,7 @@ class ReadPreferenceTest < Test::Unit::TestCase
50
50
  @coll.save({:a => 20}, :safe => {:w => 2})
51
51
 
52
52
  # Test that reads are going to secondary on ReplSetConnection
53
- @secondary = Connection.new(@rs.host, @conn.read_pool.port, :slave_ok => true)
53
+ @secondary = Connection.new(@rs.host, @conn.secondary_pool.port, :slave_ok => true)
54
54
  queries_before = @secondary['admin'].command({:serverStatus => 1})['opcounters']['query']
55
55
  @coll.find_one
56
56
  queries_after = @secondary['admin'].command({:serverStatus => 1})['opcounters']['query']
@@ -60,7 +60,7 @@ class ReadPreferenceTest < Test::Unit::TestCase
60
60
  @conn.refresh
61
61
 
62
62
  # Test that reads are only allowed from secondaries
63
- assert_raise ConnectionFailure.new("Could not connect to a secondary for reading.") do
63
+ assert_raise ConnectionFailure.new("Could not checkout a socket.") do
64
64
  @coll.find_one
65
65
  end
66
66
 
@@ -45,8 +45,16 @@ class ReplicaSetRefreshWithThreadsTest < Test::Unit::TestCase
45
45
  end
46
46
  end
47
47
 
48
- @rs.add_node
49
- threads.each {|t| t.join }
48
+ # MongoDB < 2.0 will disconnect clients on rs.reconfig()
49
+ if @rs.version.first < 2
50
+ assert_raise Mongo::ConnectionFailure do
51
+ @rs.add_node
52
+ threads.each {|t| t.join }
53
+ end
54
+ else
55
+ @rs.add_node
56
+ threads.each {|t| t.join }
57
+ end
50
58
 
51
59
  config = @conn['admin'].command({:ismaster => 1})
52
60
 
@@ -26,6 +26,34 @@ class ConnectionTest < Test::Unit::TestCase
26
26
  should "default slave_ok to false" do
27
27
  assert !@conn.slave_ok?
28
28
  end
29
+
30
+ should "warn if invalid options are specified" do
31
+ conn = Connection.allocate
32
+ opts = {:connect => false}
33
+
34
+ ReplSetConnection::REPL_SET_OPTS.each do |opt|
35
+ conn.expects(:warn).with("#{opt} is not a valid option for #{conn.class}")
36
+ opts[opt] = true
37
+ end
38
+
39
+ args = ['localhost', 27017, opts]
40
+ conn.send(:initialize, *args)
41
+ end
42
+
43
+ context "given a replica set" do
44
+ should "warn if invalid options are specified" do
45
+ conn = ReplSetConnection.allocate
46
+ opts = {:connect => false}
47
+
48
+ Connection::CONNECTION_OPTS.each do |opt|
49
+ conn.expects(:warn).with("#{opt} is not a valid option for #{conn.class}")
50
+ opts[opt] = true
51
+ end
52
+
53
+ args = [['localhost:27017'], opts]
54
+ conn.send(:initialize, *args)
55
+ end
56
+ end
29
57
  end
30
58
 
31
59
  context "initializing with a mongodb uri" do
@@ -45,21 +73,21 @@ class ConnectionTest < Test::Unit::TestCase
45
73
  @conn = Connection.from_uri("mongodb://#{host_name}/foo", :connect => false)
46
74
  assert_equal [host_name, 27017], @conn.host_to_try
47
75
  end
48
-
76
+
49
77
  should "set safe options on connection" do
50
78
  host_name = "localhost"
51
79
  opts = "safe=true&w=2&wtimeoutMS=1000&fsync=true&journal=true"
52
80
  @conn = Connection.from_uri("mongodb://#{host_name}/foo?#{opts}", :connect => false)
53
81
  assert_equal({:w => 2, :wtimeout => 1000, :fsync => true, :j => true}, @conn.safe)
54
82
  end
55
-
83
+
56
84
  should "have wtimeoutMS take precidence over the depricated wtimeout" do
57
85
  host_name = "localhost"
58
86
  opts = "safe=true&wtimeout=100&wtimeoutMS=500"
59
87
  @conn = Connection.from_uri("mongodb://#{host_name}/foo?#{opts}", :connect => false)
60
88
  assert_equal({:wtimeout => 500}, @conn.safe)
61
89
  end
62
-
90
+
63
91
  should "set timeout options on connection" do
64
92
  host_name = "localhost"
65
93
  opts = "connectTimeoutMS=1000&socketTimeoutMS=5000"