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