mongo 1.6.0 → 1.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +5 -8
- data/Rakefile +84 -2
- data/docs/HISTORY.md +8 -0
- data/docs/REPLICA_SETS.md +9 -1
- data/lib/mongo.rb +1 -0
- data/lib/mongo/connection.rb +32 -17
- data/lib/mongo/cursor.rb +1 -1
- data/lib/mongo/gridfs/grid.rb +1 -1
- data/lib/mongo/repl_set_connection.rb +174 -136
- data/lib/mongo/util/logging.rb +5 -2
- data/lib/mongo/util/node.rb +2 -2
- data/lib/mongo/util/pool.rb +80 -46
- data/lib/mongo/util/pool_manager.rb +8 -0
- data/lib/mongo/util/ssl_socket.rb +2 -1
- data/lib/mongo/util/tcp_socket.rb +6 -0
- data/lib/mongo/version.rb +1 -1
- data/mongo.gemspec +1 -1
- data/test/connection_test.rb +2 -2
- data/test/pool_test.rb +57 -0
- data/test/replica_sets/pooled_insert_test.rb +1 -1
- data/test/replica_sets/read_preference_test.rb +2 -2
- data/test/replica_sets/refresh_with_threads_test.rb +10 -2
- data/test/unit/connection_test.rb +31 -3
- data/test/unit/read_test.rb +5 -3
- metadata +10 -7
data/lib/mongo/util/logging.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
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.
|
data/lib/mongo/util/node.rb
CHANGED
@@ -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
|
|
data/lib/mongo/util/pool.rb
CHANGED
@@ -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
|
40
|
-
@timeout
|
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
|
-
@
|
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
|
-
|
68
|
+
if opts[:soft] && !@checked_out.empty?
|
69
|
+
@closing = true
|
70
|
+
close_sockets(@sockets - @checked_out)
|
69
71
|
else
|
70
|
-
|
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
|
-
@
|
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
|
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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
-
@
|
224
|
+
@threads_to_sockets[Thread.current] = socket
|
229
225
|
socket
|
230
226
|
end
|
231
227
|
end
|
232
228
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
-
|
255
|
-
|
256
|
-
|
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 @
|
261
|
-
|
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
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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
|
data/lib/mongo/version.rb
CHANGED
data/mongo.gemspec
CHANGED
data/test/connection_test.rb
CHANGED
@@ -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 =
|
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 =
|
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)
|
data/test/pool_test.rb
ADDED
@@ -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 =>
|
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.
|
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
|
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
|
-
|
49
|
-
|
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"
|