mongo 1.6.0 → 1.6.1

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.
@@ -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"