mongo 1.3.1 → 1.4.0

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 (75) hide show
  1. data/README.md +9 -6
  2. data/Rakefile +3 -4
  3. data/docs/HISTORY.md +20 -2
  4. data/docs/READ_PREFERENCE.md +39 -0
  5. data/docs/RELEASES.md +1 -1
  6. data/docs/REPLICA_SETS.md +23 -2
  7. data/docs/TAILABLE_CURSORS.md +51 -0
  8. data/docs/TUTORIAL.md +4 -4
  9. data/docs/WRITE_CONCERN.md +5 -2
  10. data/lib/mongo.rb +7 -22
  11. data/lib/mongo/collection.rb +96 -29
  12. data/lib/mongo/connection.rb +107 -62
  13. data/lib/mongo/cursor.rb +136 -57
  14. data/lib/mongo/db.rb +26 -5
  15. data/lib/mongo/exceptions.rb +17 -1
  16. data/lib/mongo/gridfs/grid.rb +1 -1
  17. data/lib/mongo/repl_set_connection.rb +273 -156
  18. data/lib/mongo/util/logging.rb +42 -0
  19. data/lib/mongo/util/node.rb +183 -0
  20. data/lib/mongo/util/pool.rb +76 -13
  21. data/lib/mongo/util/pool_manager.rb +208 -0
  22. data/lib/mongo/util/ssl_socket.rb +38 -0
  23. data/lib/mongo/util/support.rb +9 -1
  24. data/lib/mongo/util/timeout.rb +42 -0
  25. data/lib/mongo/version.rb +3 -0
  26. data/mongo.gemspec +2 -2
  27. data/test/bson/binary_test.rb +1 -1
  28. data/test/bson/bson_string_test.rb +30 -0
  29. data/test/bson/bson_test.rb +6 -3
  30. data/test/bson/byte_buffer_test.rb +1 -1
  31. data/test/bson/hash_with_indifferent_access_test.rb +1 -1
  32. data/test/bson/json_test.rb +1 -1
  33. data/test/bson/object_id_test.rb +2 -18
  34. data/test/bson/ordered_hash_test.rb +38 -3
  35. data/test/bson/test_helper.rb +46 -0
  36. data/test/bson/timestamp_test.rb +32 -10
  37. data/test/collection_test.rb +89 -3
  38. data/test/connection_test.rb +35 -20
  39. data/test/cursor_test.rb +63 -2
  40. data/test/db_test.rb +12 -2
  41. data/test/pool_test.rb +21 -0
  42. data/test/replica_sets/connect_test.rb +26 -13
  43. data/test/replica_sets/connection_string_test.rb +1 -4
  44. data/test/replica_sets/count_test.rb +1 -0
  45. data/test/replica_sets/insert_test.rb +1 -0
  46. data/test/replica_sets/pooled_insert_test.rb +4 -1
  47. data/test/replica_sets/query_secondaries.rb +2 -1
  48. data/test/replica_sets/query_test.rb +2 -1
  49. data/test/replica_sets/read_preference_test.rb +43 -0
  50. data/test/replica_sets/refresh_test.rb +123 -0
  51. data/test/replica_sets/replication_ack_test.rb +9 -4
  52. data/test/replica_sets/rs_test_helper.rb +2 -2
  53. data/test/timeout_test.rb +14 -0
  54. data/test/tools/repl_set_manager.rb +134 -23
  55. data/test/unit/collection_test.rb +6 -8
  56. data/test/unit/connection_test.rb +4 -4
  57. data/test/unit/cursor_test.rb +23 -5
  58. data/test/unit/db_test.rb +2 -0
  59. data/test/unit/grid_test.rb +2 -0
  60. data/test/unit/node_test.rb +73 -0
  61. data/test/unit/pool_manager_test.rb +47 -0
  62. data/test/unit/read_test.rb +101 -0
  63. metadata +214 -138
  64. data/lib/mongo/test.rb +0 -20
  65. data/test/async/collection_test.rb +0 -224
  66. data/test/async/connection_test.rb +0 -24
  67. data/test/async/cursor_test.rb +0 -162
  68. data/test/async/worker_pool_test.rb +0 -99
  69. data/test/load/resque/load.rb +0 -21
  70. data/test/load/resque/processor.rb +0 -26
  71. data/test/load/unicorn/unicorn.rb +0 -29
  72. data/test/tools/load.rb +0 -58
  73. data/test/tools/sharding_manager.rb +0 -202
  74. data/test/tools/test.rb +0 -4
  75. data/test/unit/repl_set_connection_test.rb +0 -59
@@ -0,0 +1,123 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require './test/replica_sets/rs_test_helper'
3
+ require 'benchmark'
4
+
5
+ # on ports TEST_PORT, RS.ports[1], and TEST + 2.
6
+ class ReplicaSetRefreshTest < Test::Unit::TestCase
7
+ include Mongo
8
+
9
+ def setup
10
+ @conn = nil
11
+ end
12
+
13
+ def teardown
14
+ RS.restart_killed_nodes
15
+ @conn.close if @conn
16
+ end
17
+
18
+ def test_connect_speed
19
+ Benchmark.bm do |x|
20
+ x.report("Connect") do
21
+ 10.times do
22
+ ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
23
+ [RS.host, RS.ports[2]], :background_refresh => false)
24
+ end
25
+ end
26
+
27
+ @con = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
28
+ [RS.host, RS.ports[2]], :background_refresh => false)
29
+
30
+ x.report("manager") do
31
+ man = Mongo::PoolManager.new(@con, @con.seeds)
32
+ 10.times do
33
+ man.connect
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def test_connect_and_manual_refresh_with_secondaries_down
40
+ RS.kill_all_secondaries
41
+
42
+ rescue_connection_failure do
43
+ @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
44
+ [RS.host, RS.ports[2]], :background_refresh => false)
45
+ end
46
+
47
+ assert_equal [], @conn.secondaries
48
+ assert @conn.connected?
49
+ assert_equal @conn.read_pool, @conn.primary_pool
50
+
51
+ # Refresh with no change to set
52
+ @conn.refresh
53
+ assert_equal [], @conn.secondaries
54
+ assert @conn.connected?
55
+ assert_equal @conn.read_pool, @conn.primary_pool
56
+
57
+ RS.restart_killed_nodes
58
+ assert_equal [], @conn.secondaries
59
+ assert @conn.connected?
60
+ assert_equal @conn.read_pool, @conn.primary_pool
61
+
62
+ # Refresh with everything up
63
+ @conn.refresh
64
+ assert @conn.read_pool
65
+ assert @conn.secondaries.length > 0
66
+ end
67
+
68
+ def test_automated_refresh_with_secondaries_down
69
+ RS.kill_all_secondaries
70
+
71
+ rescue_connection_failure do
72
+ @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
73
+ [RS.host, RS.ports[2]], :refresh_interval => 2, :background_refresh => true)
74
+ end
75
+
76
+ assert_equal [], @conn.secondaries
77
+ assert @conn.connected?
78
+ assert_equal @conn.read_pool, @conn.primary_pool
79
+
80
+ RS.restart_killed_nodes
81
+
82
+ sleep(3)
83
+
84
+ assert @conn.read_pool != @conn.primary_pool, "Read pool and primary pool are identical."
85
+ assert @conn.secondaries.length > 0, "No secondaries have been added."
86
+ end
87
+
88
+ def test_automated_refresh_with_removed_node
89
+ @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
90
+ [RS.host, RS.ports[2]], :refresh_interval => 2, :background_refresh => true)
91
+
92
+ assert_equal 2, @conn.secondary_pools.length
93
+ assert_equal 2, @conn.secondaries.length
94
+
95
+ n = RS.remove_secondary_node
96
+ sleep(4)
97
+
98
+ assert_equal 1, @conn.secondaries.length
99
+ assert_equal 1, @conn.secondary_pools.length
100
+
101
+ RS.add_node(n)
102
+ end
103
+
104
+ def test_adding_and_removing_nodes
105
+ @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
106
+ [RS.host, RS.ports[2]], :refresh_interval => 2, :background_refresh => true)
107
+
108
+ RS.add_node
109
+ sleep(5)
110
+
111
+ @conn2 = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
112
+ [RS.host, RS.ports[2]], :refresh_interval => 2, :background_refresh => true)
113
+
114
+ assert @conn2.secondaries == @conn.secondaries
115
+ assert_equal 3, @conn.secondary_pools.length
116
+ assert_equal 3, @conn.secondaries.length
117
+
118
+ RS.remove_secondary_node
119
+ sleep(4)
120
+ assert_equal 2, @conn.secondary_pools.length
121
+ assert_equal 2, @conn.secondaries.length
122
+ end
123
+ end
@@ -20,6 +20,11 @@ class ReplicaSetAckTest < Test::Unit::TestCase
20
20
  @col = @db.collection("test-sets")
21
21
  end
22
22
 
23
+ def teardown
24
+ RS.restart_killed_nodes
25
+ @conn.close if @conn
26
+ end
27
+
23
28
  def test_safe_mode_with_w_failure
24
29
  assert_raise_error OperationFailure, "timeout" do
25
30
  @col.insert({:foo => 1}, :safe => {:w => 4, :wtimeout => 1, :fsync => true})
@@ -33,15 +38,15 @@ class ReplicaSetAckTest < Test::Unit::TestCase
33
38
  end
34
39
 
35
40
  def test_safe_mode_replication_ack
36
- @col.insert({:baz => "bar"}, :safe => {:w => 2, :wtimeout => 5000})
41
+ @col.insert({:baz => "bar"}, :safe => {:w => 3, :wtimeout => 5000})
37
42
 
38
- assert @col.insert({:foo => "0" * 5000}, :safe => {:w => 2, :wtimeout => 5000})
43
+ assert @col.insert({:foo => "0" * 5000}, :safe => {:w => 3, :wtimeout => 5000})
39
44
  assert_equal 2, @slave1[MONGO_TEST_DB]["test-sets"].count
40
45
 
41
- assert @col.update({:baz => "bar"}, {:baz => "foo"}, :safe => {:w => 2, :wtimeout => 5000})
46
+ assert @col.update({:baz => "bar"}, {:baz => "foo"}, :safe => {:w => 3, :wtimeout => 5000})
42
47
  assert @slave1[MONGO_TEST_DB]["test-sets"].find_one({:baz => "foo"})
43
48
 
44
- assert @col.remove({}, :safe => {:w => 2, :wtimeout => 5000})
49
+ assert @col.remove({}, :safe => {:w => 3, :wtimeout => 5000})
45
50
  assert_equal 0, @slave1[MONGO_TEST_DB]["test-sets"].count
46
51
  end
47
52
 
@@ -11,7 +11,7 @@ class Test::Unit::TestCase
11
11
 
12
12
  # Generic code for rescuing connection failures and retrying operations.
13
13
  # This could be combined with some timeout functionality.
14
- def rescue_connection_failure(max_retries=60)
14
+ def rescue_connection_failure(max_retries=30)
15
15
  retries = 0
16
16
  begin
17
17
  yield
@@ -19,7 +19,7 @@ class Test::Unit::TestCase
19
19
  puts "Rescue attempt #{retries}: from #{ex}"
20
20
  retries += 1
21
21
  raise ex if retries > max_retries
22
- sleep(1)
22
+ sleep(2)
23
23
  retry
24
24
  end
25
25
  end
@@ -0,0 +1,14 @@
1
+ require './test/test_helper'
2
+
3
+ class TestTimeout < Test::Unit::TestCase
4
+
5
+ def test_timeout
6
+ @conn = standard_connection(:op_timeout => 2)
7
+ assert @conn[MONGO_TEST_DB]['test'].save({:a => 1})
8
+ assert @conn[MONGO_TEST_DB]['test'].find.next
9
+ assert_raise OperationTimeout do
10
+ @conn[MONGO_TEST_DB]['test'].find({'$where' => 'function() { while(true) { this.a == 1 } }'}).next
11
+ end
12
+ end
13
+
14
+ end
@@ -10,17 +10,21 @@ end
10
10
 
11
11
  class ReplSetManager
12
12
 
13
- attr_accessor :host, :start_port, :ports, :name, :mongods
13
+ attr_accessor :host, :start_port, :ports, :name, :mongods, :tags, :version
14
14
 
15
15
  def initialize(opts={})
16
16
  @start_port = opts[:start_port] || 30000
17
17
  @ports = []
18
18
  @name = opts[:name] || 'replica-set-foo'
19
19
  @host = opts[:host] || 'localhost'
20
- @retries = opts[:retries] || 60
20
+ @retries = opts[:retries] || 30
21
21
  @config = {"_id" => @name, "members" => []}
22
22
  @durable = opts.fetch(:durable, false)
23
23
  @path = File.join(File.expand_path(File.dirname(__FILE__)), "data")
24
+ @oplog_size = opts.fetch(:oplog_size, 32)
25
+ @tags = [{"dc" => "ny", "rack" => "a", "db" => "main"},
26
+ {"dc" => "ny", "rack" => "b", "db" => "main"},
27
+ {"dc" => "sf", "rack" => "a", "db" => "main"}]
24
28
 
25
29
  @arbiter_count = opts[:arbiter_count] || 2
26
30
  @secondary_count = opts[:secondary_count] || 2
@@ -33,35 +37,59 @@ class ReplSetManager
33
37
  end
34
38
 
35
39
  @mongods = {}
40
+ version_string = `mongod --version`
41
+ version_string =~ /(\d\.\d\.\d)/
42
+ @version = $1.split(".").map {|d| d.to_i }
36
43
  end
37
44
 
38
45
  def start_set
39
- puts "** Starting a replica set with #{@count} nodes"
46
+ begin
47
+ con = Mongo::Connection.new(@host, @start_port)
48
+ rescue Mongo::ConnectionFailure
49
+ end
40
50
 
41
- system("killall mongod")
51
+ if con && ensure_up(1, con)
52
+ should_start = false
53
+ puts "** Replica set already started."
54
+ else
55
+ should_start = true
56
+ system("killall mongod")
57
+ puts "** Starting a replica set with #{@count} nodes"
58
+ end
42
59
 
43
60
  n = 0
44
61
  (@primary_count + @secondary_count).times do
45
- init_node(n)
62
+ init_node(n, should_start) do |attrs|
63
+ if @version[0] >= 2
64
+ attrs['tags'] = @tags[n % @tags.size]
65
+ end
66
+ end
46
67
  n += 1
47
68
  end
48
69
 
49
70
  @passive_count.times do
50
- init_node(n) do |attrs|
71
+ init_node(n, should_start) do |attrs|
51
72
  attrs['priority'] = 0
52
73
  end
53
74
  n += 1
54
75
  end
55
76
 
56
77
  @arbiter_count.times do
57
- init_node(n) do |attrs|
78
+ init_node(n, should_start) do |attrs|
58
79
  attrs['arbiterOnly'] = true
59
80
  end
60
81
  n += 1
61
82
  end
62
83
 
63
- initiate
64
- ensure_up
84
+ if con && ensure_up(1, con)
85
+ @mongods.each do |k, v|
86
+ v['up'] = true
87
+ v['pid'] = File.open(File.join(v['db_path'], 'mongod.lock')).read.strip
88
+ end
89
+ else
90
+ initiate
91
+ ensure_up
92
+ end
65
93
  end
66
94
 
67
95
  def cleanup_set
@@ -71,18 +99,20 @@ class ReplSetManager
71
99
  end
72
100
  end
73
101
 
74
- def init_node(n)
102
+ def init_node(n, should_start=true)
75
103
  @mongods[n] ||= {}
76
104
  port = @start_port + n
77
105
  @ports << port
78
106
  @mongods[n]['port'] = port
79
107
  @mongods[n]['db_path'] = get_path("rs-#{port}")
80
108
  @mongods[n]['log_path'] = get_path("log-#{port}")
81
- system("rm -rf #{@mongods[n]['db_path']}")
82
- system("mkdir -p #{@mongods[n]['db_path']}")
83
-
84
109
  @mongods[n]['start'] = start_cmd(n)
85
- start(n)
110
+
111
+ if should_start
112
+ system("rm -rf #{@mongods[n]['db_path']}")
113
+ system("mkdir -p #{@mongods[n]['db_path']}")
114
+ start(n)
115
+ end
86
116
 
87
117
  member = {'_id' => n, 'host' => "#{@host}:#{@mongods[n]['port']}"}
88
118
 
@@ -96,13 +126,64 @@ class ReplSetManager
96
126
  @config['members'] << member
97
127
  end
98
128
 
129
+ def journal_switch
130
+ if @version[0] >= 2
131
+ if @durable
132
+ "--journal"
133
+ else
134
+ "--nojournal"
135
+ end
136
+ elsif @durable
137
+ "--journal"
138
+ end
139
+ end
140
+
99
141
  def start_cmd(n)
100
142
  @mongods[n]['start'] = "mongod --replSet #{@name} --logpath '#{@mongods[n]['log_path']}' " +
101
- " --dbpath #{@mongods[n]['db_path']} --port #{@mongods[n]['port']} --fork"
143
+ "--oplogSize #{@oplog_size} #{journal_switch} --dbpath #{@mongods[n]['db_path']} --port #{@mongods[n]['port']} --fork"
102
144
  @mongods[n]['start'] += " --dur" if @durable
103
145
  @mongods[n]['start']
104
146
  end
105
147
 
148
+ def remove_secondary_node
149
+ primary = get_node_with_state(1)
150
+ con = get_connection(primary)
151
+ config = con['local']['system.replset'].find_one
152
+ secondary = get_node_with_state(2)
153
+ host_port = "#{@host}:#{@mongods[secondary]['port']}"
154
+ kill(secondary)
155
+ @mongods.delete(secondary)
156
+ @config['members'].reject! {|m| m['host'] == host_port}
157
+ @config['version'] = config['version'] + 1
158
+
159
+ begin
160
+ con['admin'].command({'replSetReconfig' => @config})
161
+ rescue Mongo::ConnectionFailure
162
+ end
163
+
164
+ con.close
165
+
166
+ return secondary
167
+ end
168
+
169
+ def add_node(n=nil)
170
+ primary = get_node_with_state(1)
171
+ con = get_connection(primary)
172
+ init_node(n || @mongods.length)
173
+
174
+ config = con['local']['system.replset'].find_one
175
+ @config['version'] = config['version'] + 1
176
+
177
+ # We expect a connection failure on reconfigure here.
178
+ begin
179
+ con['admin'].command({'replSetReconfig' => @config})
180
+ rescue Mongo::ConnectionFailure
181
+ end
182
+
183
+ con.close
184
+ ensure_up
185
+ end
186
+
106
187
  def kill(node, signal=2)
107
188
  pid = @mongods[node]['pid']
108
189
  puts "** Killing node with pid #{pid} at port #{@mongods[node]['port']}"
@@ -127,6 +208,7 @@ class ReplSetManager
127
208
  con['admin'].command({'replSetStepDown' => 90})
128
209
  rescue Mongo::ConnectionFailure
129
210
  end
211
+ con.close
130
212
  end
131
213
 
132
214
  def kill_secondary
@@ -135,6 +217,15 @@ class ReplSetManager
135
217
  return node
136
218
  end
137
219
 
220
+ def kill_all_secondaries
221
+ nodes = get_all_nodes_with_state(2)
222
+ if nodes
223
+ nodes.each do |n|
224
+ kill(n)
225
+ end
226
+ end
227
+ end
228
+
138
229
  def restart_killed_nodes
139
230
  nodes = @mongods.keys.select do |key|
140
231
  @mongods[key]['up'] == false
@@ -159,21 +250,26 @@ class ReplSetManager
159
250
  end
160
251
  alias :restart :start
161
252
 
162
- def ensure_up
253
+ def ensure_up(n=nil, connection=nil)
163
254
  print "** Ensuring members are up..."
164
255
 
165
- attempt do
166
- con = get_connection
256
+ attempt(n) do
257
+ con = connection || get_connection
167
258
  status = con['admin'].command({'replSetGetStatus' => 1})
168
259
  print "."
169
- if status['members'].all? { |m| m['health'] == 1 && [1, 2, 7].include?(m['state']) } &&
260
+ if status['members'].all? { |m| m['health'] == 1 &&
261
+ [1, 2, 7].include?(m['state']) } &&
170
262
  status['members'].any? { |m| m['state'] == 1 }
171
263
  print "all members up!\n\n"
264
+ con.close
172
265
  return status
173
266
  else
267
+ con.close
174
268
  raise Mongo::OperationFailure
175
269
  end
176
270
  end
271
+
272
+ return false
177
273
  end
178
274
 
179
275
  def primary
@@ -207,6 +303,20 @@ class ReplSetManager
207
303
  attempt do
208
304
  con['admin'].command({'replSetInitiate' => @config})
209
305
  end
306
+
307
+ con.close
308
+ end
309
+
310
+ def get_all_nodes_with_state(state)
311
+ status = ensure_up
312
+ nodes = status['members'].select {|m| m['state'] == state}
313
+ nodes = nodes.map do |node|
314
+ host_port = node['name'].split(':')
315
+ port = host_port[1] ? host_port[1].to_i : 27017
316
+ @mongods.keys.detect {|key| @mongods[key]['port'] == port}
317
+ end
318
+
319
+ nodes == [] ? false : nodes
210
320
  end
211
321
 
212
322
  def get_node_with_state(state)
@@ -215,7 +325,7 @@ class ReplSetManager
215
325
  if node
216
326
  host_port = node['name'].split(':')
217
327
  port = host_port[1] ? host_port[1].to_i : 27017
218
- key = @mongods.keys.detect {|key| @mongods[key]['port'] == port}
328
+ key = @mongods.keys.detect {|n| @mongods[n]['port'] == port}
219
329
  return key
220
330
  else
221
331
  return false
@@ -247,19 +357,20 @@ class ReplSetManager
247
357
  File.join(@path, name)
248
358
  end
249
359
 
250
- def attempt
360
+ def attempt(retries=nil)
251
361
  raise "No block given!" unless block_given?
252
362
  count = 0
253
363
 
254
- while count < @retries do
364
+ while count < (retries || @retries) do
255
365
  begin
256
366
  return yield
257
367
  rescue Mongo::OperationFailure, Mongo::ConnectionFailure => ex
258
- sleep(1)
368
+ sleep(2)
259
369
  count += 1
260
370
  end
261
371
  end
262
372
 
373
+ puts "NO MORE ATTEMPTS"
263
374
  raise ex
264
375
  end
265
376