andyjeffries-rubyrep 1.2.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.
Files changed (125) hide show
  1. data/History.txt +83 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +151 -0
  4. data/README.txt +37 -0
  5. data/bin/rubyrep +8 -0
  6. data/lib/rubyrep.rb +72 -0
  7. data/lib/rubyrep/base_runner.rb +195 -0
  8. data/lib/rubyrep/command_runner.rb +144 -0
  9. data/lib/rubyrep/committers/buffered_committer.rb +151 -0
  10. data/lib/rubyrep/committers/committers.rb +152 -0
  11. data/lib/rubyrep/configuration.rb +275 -0
  12. data/lib/rubyrep/connection_extenders/connection_extenders.rb +165 -0
  13. data/lib/rubyrep/connection_extenders/jdbc_extender.rb +65 -0
  14. data/lib/rubyrep/connection_extenders/mysql_extender.rb +59 -0
  15. data/lib/rubyrep/connection_extenders/postgresql_extender.rb +277 -0
  16. data/lib/rubyrep/database_proxy.rb +52 -0
  17. data/lib/rubyrep/direct_table_scan.rb +75 -0
  18. data/lib/rubyrep/generate_runner.rb +105 -0
  19. data/lib/rubyrep/initializer.rb +39 -0
  20. data/lib/rubyrep/log_helper.rb +30 -0
  21. data/lib/rubyrep/logged_change.rb +160 -0
  22. data/lib/rubyrep/logged_change_loader.rb +197 -0
  23. data/lib/rubyrep/noisy_connection.rb +80 -0
  24. data/lib/rubyrep/proxied_table_scan.rb +171 -0
  25. data/lib/rubyrep/proxy_block_cursor.rb +145 -0
  26. data/lib/rubyrep/proxy_connection.rb +431 -0
  27. data/lib/rubyrep/proxy_cursor.rb +44 -0
  28. data/lib/rubyrep/proxy_row_cursor.rb +43 -0
  29. data/lib/rubyrep/proxy_runner.rb +89 -0
  30. data/lib/rubyrep/replication_difference.rb +100 -0
  31. data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
  32. data/lib/rubyrep/replication_extenders/postgresql_replication.rb +236 -0
  33. data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
  34. data/lib/rubyrep/replication_helper.rb +142 -0
  35. data/lib/rubyrep/replication_initializer.rb +327 -0
  36. data/lib/rubyrep/replication_run.rb +142 -0
  37. data/lib/rubyrep/replication_runner.rb +166 -0
  38. data/lib/rubyrep/replicators/replicators.rb +42 -0
  39. data/lib/rubyrep/replicators/two_way_replicator.rb +361 -0
  40. data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
  41. data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
  42. data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
  43. data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
  44. data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
  45. data/lib/rubyrep/scan_runner.rb +25 -0
  46. data/lib/rubyrep/session.rb +230 -0
  47. data/lib/rubyrep/sync_helper.rb +121 -0
  48. data/lib/rubyrep/sync_runner.rb +31 -0
  49. data/lib/rubyrep/syncers/syncers.rb +112 -0
  50. data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
  51. data/lib/rubyrep/table_scan.rb +54 -0
  52. data/lib/rubyrep/table_scan_helper.rb +46 -0
  53. data/lib/rubyrep/table_sorter.rb +70 -0
  54. data/lib/rubyrep/table_spec_resolver.rb +142 -0
  55. data/lib/rubyrep/table_sync.rb +90 -0
  56. data/lib/rubyrep/task_sweeper.rb +77 -0
  57. data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
  58. data/lib/rubyrep/type_casting_cursor.rb +31 -0
  59. data/lib/rubyrep/uninstall_runner.rb +93 -0
  60. data/lib/rubyrep/version.rb +9 -0
  61. data/rubyrep +8 -0
  62. data/rubyrep.bat +4 -0
  63. data/setup.rb +1585 -0
  64. data/spec/base_runner_spec.rb +218 -0
  65. data/spec/buffered_committer_spec.rb +274 -0
  66. data/spec/command_runner_spec.rb +145 -0
  67. data/spec/committers_spec.rb +178 -0
  68. data/spec/configuration_spec.rb +203 -0
  69. data/spec/connection_extender_interface_spec.rb +141 -0
  70. data/spec/connection_extenders_registration_spec.rb +164 -0
  71. data/spec/database_proxy_spec.rb +48 -0
  72. data/spec/database_rake_spec.rb +40 -0
  73. data/spec/db_specific_connection_extenders_spec.rb +34 -0
  74. data/spec/db_specific_replication_extenders_spec.rb +38 -0
  75. data/spec/direct_table_scan_spec.rb +61 -0
  76. data/spec/dolphins.jpg +0 -0
  77. data/spec/generate_runner_spec.rb +84 -0
  78. data/spec/initializer_spec.rb +46 -0
  79. data/spec/log_helper_spec.rb +39 -0
  80. data/spec/logged_change_loader_spec.rb +68 -0
  81. data/spec/logged_change_spec.rb +470 -0
  82. data/spec/noisy_connection_spec.rb +78 -0
  83. data/spec/postgresql_replication_spec.rb +48 -0
  84. data/spec/postgresql_schema_support_spec.rb +212 -0
  85. data/spec/postgresql_support_spec.rb +63 -0
  86. data/spec/progress_bar_spec.rb +77 -0
  87. data/spec/proxied_table_scan_spec.rb +151 -0
  88. data/spec/proxy_block_cursor_spec.rb +197 -0
  89. data/spec/proxy_connection_spec.rb +423 -0
  90. data/spec/proxy_cursor_spec.rb +56 -0
  91. data/spec/proxy_row_cursor_spec.rb +66 -0
  92. data/spec/proxy_runner_spec.rb +70 -0
  93. data/spec/replication_difference_spec.rb +161 -0
  94. data/spec/replication_extender_interface_spec.rb +367 -0
  95. data/spec/replication_extenders_spec.rb +32 -0
  96. data/spec/replication_helper_spec.rb +178 -0
  97. data/spec/replication_initializer_spec.rb +509 -0
  98. data/spec/replication_run_spec.rb +443 -0
  99. data/spec/replication_runner_spec.rb +254 -0
  100. data/spec/replicators_spec.rb +36 -0
  101. data/spec/rubyrep_spec.rb +8 -0
  102. data/spec/scan_detail_reporter_spec.rb +119 -0
  103. data/spec/scan_progress_printers_spec.rb +68 -0
  104. data/spec/scan_report_printers_spec.rb +67 -0
  105. data/spec/scan_runner_spec.rb +50 -0
  106. data/spec/scan_summary_reporter_spec.rb +61 -0
  107. data/spec/session_spec.rb +253 -0
  108. data/spec/spec.opts +1 -0
  109. data/spec/spec_helper.rb +305 -0
  110. data/spec/strange_name_support_spec.rb +135 -0
  111. data/spec/sync_helper_spec.rb +169 -0
  112. data/spec/sync_runner_spec.rb +78 -0
  113. data/spec/syncers_spec.rb +171 -0
  114. data/spec/table_scan_helper_spec.rb +36 -0
  115. data/spec/table_scan_spec.rb +49 -0
  116. data/spec/table_sorter_spec.rb +30 -0
  117. data/spec/table_spec_resolver_spec.rb +111 -0
  118. data/spec/table_sync_spec.rb +140 -0
  119. data/spec/task_sweeper_spec.rb +47 -0
  120. data/spec/trigger_mode_switcher_spec.rb +83 -0
  121. data/spec/two_way_replicator_spec.rb +721 -0
  122. data/spec/two_way_syncer_spec.rb +256 -0
  123. data/spec/type_casting_cursor_spec.rb +50 -0
  124. data/spec/uninstall_runner_spec.rb +93 -0
  125. metadata +190 -0
@@ -0,0 +1,443 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ include RR
4
+
5
+ describe ReplicationRun do
6
+ before(:each) do
7
+ Initializer.configuration = standard_config
8
+ end
9
+
10
+ it "initialize should store the provided session" do
11
+ session = Session.new
12
+ sweeper = TaskSweeper.new 1
13
+ run = ReplicationRun.new session, sweeper
14
+ run.session.should == session
15
+ end
16
+
17
+ it "install_sweeper should install a task sweeper into the database connections" do
18
+ session = Session.new
19
+ sweeper = TaskSweeper.new 1
20
+ run = ReplicationRun.new session, sweeper
21
+ run.install_sweeper
22
+
23
+ session.left.sweeper.should == sweeper
24
+ session.right.sweeper.should == sweeper
25
+ end
26
+
27
+ it "helper should return the correctly initialized replication helper" do
28
+ run = ReplicationRun.new Session.new, TaskSweeper.new(1)
29
+ run.helper.should be_an_instance_of(ReplicationHelper)
30
+ run.helper.replication_run.should == run
31
+ run.helper.should == run.helper # ensure the helper is created only once
32
+ end
33
+
34
+ it "replicator should return the configured replicator" do
35
+ session = Session.new
36
+ run = ReplicationRun.new session, TaskSweeper.new(1)
37
+ run.replicator.
38
+ should be_an_instance_of(Replicators.replicators[session.configuration.options[:replicator]])
39
+ run.replicator.should == run.replicator # should only create the replicator once
40
+ run.replicator.rep_helper.should == run.helper
41
+ end
42
+
43
+ it "event_filtered? should behave correctly" do
44
+ begin
45
+ config = deep_copy(standard_config)
46
+ config.options[:committer] = :never_commit
47
+ session = Session.new(config)
48
+
49
+ session.left.insert_record 'extender_no_record', {
50
+ 'id' => '1',
51
+ 'name' => 'bla'
52
+ }
53
+ session.left.insert_record 'rr_pending_changes', {
54
+ 'change_table' => 'extender_no_record',
55
+ 'change_key' => 'id|1',
56
+ 'change_type' => 'I',
57
+ 'change_time' => Time.now
58
+ }
59
+
60
+ loaders = LoggedChangeLoaders.new(session)
61
+ loaders.update
62
+ diff = ReplicationDifference.new loaders
63
+ diff.load
64
+
65
+ # No event filter at all
66
+ run = ReplicationRun.new session, TaskSweeper.new(1)
67
+ run.event_filtered?(diff).should be_false
68
+
69
+ # Event filter that does not handle replication events
70
+ session.configuration.options[:event_filter] = Object.new
71
+ run = ReplicationRun.new session, TaskSweeper.new(1)
72
+ run.event_filtered?(diff).should be_false
73
+
74
+ # event_filtered? should signal filtering (i. e. return true) if filter returns false.
75
+ filter = Object.new
76
+ def filter.before_replicate(table, key, helper, diff)
77
+ false
78
+ end
79
+ session.configuration.options[:event_filter] = filter
80
+ run = ReplicationRun.new session, TaskSweeper.new(1)
81
+ run.event_filtered?(diff).should be_true
82
+
83
+ # event_filtered? should return false if filter returns true.
84
+ filter = {}
85
+ def filter.before_replicate(table, key, helper, diff)
86
+ self[:args] = [table, key, helper, diff]
87
+ true
88
+ end
89
+ session.configuration.options[:event_filter] = filter
90
+ run = ReplicationRun.new session, TaskSweeper.new(1)
91
+ run.event_filtered?(diff).should be_false
92
+ filter[:args].should == ['extender_no_record', {'id' => 1}, run.helper, diff]
93
+ ensure
94
+ Committers::NeverCommitter.rollback_current_session
95
+ if session
96
+ session.left.execute "delete from extender_no_record"
97
+ session.right.execute "delete from extender_no_record"
98
+ session.left.execute "delete from rr_pending_changes"
99
+ end
100
+ end
101
+ end
102
+
103
+ it "run should replicate all logged changes" do
104
+ begin
105
+ config = deep_copy(standard_config)
106
+ config.options[:committer] = :never_commit
107
+
108
+ session = Session.new(config)
109
+
110
+ session.left.insert_record 'extender_no_record', {
111
+ 'id' => '1',
112
+ 'name' => 'bla'
113
+ }
114
+ session.left.insert_record 'rr_pending_changes', {
115
+ 'change_table' => 'extender_no_record',
116
+ 'change_key' => 'id|1',
117
+ 'change_type' => 'I',
118
+ 'change_time' => Time.now
119
+ }
120
+
121
+ run = ReplicationRun.new session, TaskSweeper.new(1)
122
+ run.run
123
+
124
+ session.right.select_record(:table => "extender_no_record").should == {
125
+ 'id' => 1,
126
+ 'name' => 'bla'
127
+ }
128
+ ensure
129
+ Committers::NeverCommitter.rollback_current_session
130
+ if session
131
+ session.left.execute "delete from extender_no_record"
132
+ session.right.execute "delete from extender_no_record"
133
+ session.left.execute "delete from rr_pending_changes"
134
+ end
135
+ end
136
+ end
137
+
138
+ it "run should replication records with foreign key constraints" do
139
+ begin
140
+ config = deep_copy(standard_config)
141
+ config.options[:committer] = :never_commit
142
+
143
+ session = Session.new(config)
144
+
145
+ session.left.insert_record 'referencing_table', {
146
+ 'id' => '5',
147
+ }
148
+ session.left.insert_record 'rr_pending_changes', {
149
+ 'change_table' => 'referencing_table',
150
+ 'change_key' => 'id|5',
151
+ 'change_type' => 'I',
152
+ 'change_time' => Time.now
153
+ }
154
+
155
+ session.left.insert_record 'referenced_table2', {
156
+ 'id' => '6',
157
+ }
158
+ session.left.insert_record 'rr_pending_changes', {
159
+ 'change_table' => 'referenced_table2',
160
+ 'change_key' => 'id|6',
161
+ 'change_type' => 'I',
162
+ 'change_time' => Time.now
163
+ }
164
+
165
+ session.left.update_record 'referencing_table', {
166
+ 'id' => 5,
167
+ 'third_fk' => '6'
168
+ }
169
+ session.left.insert_record 'rr_pending_changes', {
170
+ 'change_table' => 'referencing_table',
171
+ 'change_key' => 'id|5',
172
+ 'change_new_key' => 'id|5',
173
+ 'change_type' => 'U',
174
+ 'change_time' => Time.now
175
+ }
176
+
177
+ run = ReplicationRun.new session, TaskSweeper.new(1)
178
+ run.run
179
+
180
+ session.right.select_record(:table => "referencing_table", :from => {'id' => 5}).should == {
181
+ 'id' => 5,
182
+ 'first_fk' => nil,
183
+ 'second_fk' => nil,
184
+ 'third_fk' => 6
185
+ }
186
+ ensure
187
+ Committers::NeverCommitter.rollback_current_session
188
+ if session
189
+ session.left.execute "delete from referencing_table where id = 5"
190
+ session.left.execute "delete from referenced_table2 where id = 6"
191
+
192
+ session.right.execute "delete from referencing_table where id = 5"
193
+ session.right.execute "delete from referenced_table2 where id = 6"
194
+
195
+ session.left.execute "delete from rr_pending_changes"
196
+ end
197
+ end
198
+ end
199
+
200
+ it "run should not replicate filtered changes" do
201
+ begin
202
+ config = deep_copy(standard_config)
203
+ config.options[:committer] = :never_commit
204
+
205
+ filter = Object.new
206
+ def filter.before_replicate(table, key, helper, diff)
207
+ key['id'] != 1
208
+ end
209
+ config.options[:event_filter] = filter
210
+
211
+ session = Session.new(config)
212
+
213
+ session.left.insert_record 'extender_no_record', {
214
+ 'id' => '1',
215
+ 'name' => 'bla'
216
+ }
217
+ session.left.insert_record 'extender_no_record', {
218
+ 'id' => '2',
219
+ 'name' => 'blub'
220
+ }
221
+ session.left.insert_record 'rr_pending_changes', {
222
+ 'change_table' => 'extender_no_record',
223
+ 'change_key' => 'id|1',
224
+ 'change_type' => 'I',
225
+ 'change_time' => Time.now
226
+ }
227
+ session.left.insert_record 'rr_pending_changes', {
228
+ 'change_table' => 'extender_no_record',
229
+ 'change_key' => 'id|2',
230
+ 'change_type' => 'I',
231
+ 'change_time' => Time.now
232
+ }
233
+
234
+ run = ReplicationRun.new session, TaskSweeper.new(1)
235
+ run.run
236
+
237
+ session.right.select_records(:table => "extender_no_record").should == [{
238
+ 'id' => 2,
239
+ 'name' => 'blub'
240
+ }]
241
+ ensure
242
+ Committers::NeverCommitter.rollback_current_session
243
+ if session
244
+ session.left.execute "delete from extender_no_record"
245
+ session.right.execute "delete from extender_no_record"
246
+ session.left.execute "delete from rr_pending_changes"
247
+ end
248
+ end
249
+ end
250
+
251
+ it "run should not create the replicator if there are no pending changes" do
252
+ session = Session.new
253
+ run = ReplicationRun.new session, TaskSweeper.new(1)
254
+ run.should_not_receive(:replicator)
255
+ run.run
256
+ end
257
+
258
+ it "run should only replicate real differences" do
259
+ session = Session.new
260
+ session.left.begin_db_transaction
261
+ session.right.begin_db_transaction
262
+ begin
263
+
264
+ session.left.insert_record 'rr_pending_changes', {
265
+ 'change_table' => 'extender_no_record',
266
+ 'change_key' => 'id|1',
267
+ 'change_type' => 'D',
268
+ 'change_time' => Time.now
269
+ }
270
+ session.right.insert_record 'rr_pending_changes', {
271
+ 'change_table' => 'extender_no_record',
272
+ 'change_key' => 'id|1',
273
+ 'change_type' => 'D',
274
+ 'change_time' => Time.now
275
+ }
276
+
277
+ run = ReplicationRun.new session, TaskSweeper.new(1)
278
+ run.replicator.should_not_receive(:replicate)
279
+ run.run
280
+
281
+ ensure
282
+ session.left.rollback_db_transaction
283
+ session.right.rollback_db_transaction
284
+ end
285
+ end
286
+
287
+ it "run should log raised exceptions" do
288
+ session = Session.new
289
+ session.left.begin_db_transaction
290
+ session.right.begin_db_transaction
291
+ begin
292
+ session.left.execute "delete from rr_pending_changes"
293
+ session.left.execute "delete from rr_logged_events"
294
+ session.left.insert_record 'rr_pending_changes', {
295
+ 'change_table' => 'extender_no_record',
296
+ 'change_key' => 'id|1',
297
+ 'change_type' => 'D',
298
+ 'change_time' => Time.now
299
+ }
300
+ run = ReplicationRun.new session, TaskSweeper.new(1)
301
+ run.replicator.stub!(:replicate_difference).and_return {raise Exception, 'dummy message'}
302
+ run.run
303
+
304
+ row = session.left.select_one("select * from rr_logged_events")
305
+ row['description'].should == 'dummy message'
306
+ row['long_description'].should =~ /Exception/
307
+ ensure
308
+ session.left.rollback_db_transaction
309
+ session.right.rollback_db_transaction
310
+ end
311
+ end
312
+
313
+ it "run should re-raise original exception if logging to database fails" do
314
+ session = Session.new
315
+ session.left.begin_db_transaction
316
+ session.right.begin_db_transaction
317
+ begin
318
+ session.left.execute "delete from rr_pending_changes"
319
+ session.left.execute "delete from rr_logged_events"
320
+ session.left.insert_record 'rr_pending_changes', {
321
+ 'change_table' => 'extender_no_record',
322
+ 'change_key' => 'id|1',
323
+ 'change_type' => 'D',
324
+ 'change_time' => Time.now
325
+ }
326
+ run = ReplicationRun.new session, TaskSweeper.new(1)
327
+ run.replicator.stub!(:replicate_difference).and_return {raise Exception, 'dummy message'}
328
+ run.helper.stub!(:log_replication_outcome).and_return {raise Exception, 'blub'}
329
+ lambda {run.run}.should raise_error(Exception, 'dummy message')
330
+ ensure
331
+ session.left.rollback_db_transaction
332
+ session.right.rollback_db_transaction
333
+ end
334
+ end
335
+
336
+ it "run should return silently if timed out before work actually started" do
337
+ session = Session.new
338
+ session.left.begin_db_transaction
339
+ session.right.begin_db_transaction
340
+ begin
341
+ session.left.execute "delete from rr_pending_changes"
342
+ session.left.insert_record 'rr_pending_changes', {
343
+ 'change_table' => 'extender_no_record',
344
+ 'change_key' => 'id|1',
345
+ 'change_type' => 'D',
346
+ 'change_time' => Time.now
347
+ }
348
+ sweeper = TaskSweeper.new(1)
349
+ sweeper.stub!(:terminated?).and_return(true)
350
+ run = ReplicationRun.new session, sweeper
351
+ LoggedChangeLoaders.should_not_receive(:new)
352
+ run.run
353
+ ensure
354
+ session.left.rollback_db_transaction
355
+ session.right.rollback_db_transaction
356
+ end
357
+ end
358
+
359
+ it "run should rollback if timed out" do
360
+ session = Session.new
361
+ session.left.begin_db_transaction
362
+ session.right.begin_db_transaction
363
+ begin
364
+ session.left.execute "delete from rr_pending_changes"
365
+ session.left.execute "delete from rr_logged_events"
366
+ session.left.insert_record 'rr_pending_changes', {
367
+ 'change_table' => 'extender_no_record',
368
+ 'change_key' => 'id|1',
369
+ 'change_type' => 'D',
370
+ 'change_time' => Time.now
371
+ }
372
+ sweeper = TaskSweeper.new(1)
373
+ sweeper.should_receive(:terminated?).any_number_of_times.and_return(false, true)
374
+ run = ReplicationRun.new session, sweeper
375
+ run.helper.should_receive(:finalize).with(false)
376
+ run.run
377
+ ensure
378
+ session.left.rollback_db_transaction if session.left
379
+ session.right.rollback_db_transaction if session.right
380
+ end
381
+ end
382
+
383
+ it "run should not catch exceptions raised during replicator initialization" do
384
+ config = deep_copy(standard_config)
385
+ config.options[:logged_replication_events] = [:invalid_option]
386
+ session = Session.new config
387
+ session.left.begin_db_transaction
388
+ begin
389
+
390
+ session.left.insert_record 'rr_pending_changes', {
391
+ 'change_table' => 'extender_no_record',
392
+ 'change_key' => 'id|1',
393
+ 'change_type' => 'D',
394
+ 'change_time' => Time.now
395
+ }
396
+
397
+ run = ReplicationRun.new session, TaskSweeper.new(1)
398
+ lambda {run.run}.should raise_error(ArgumentError)
399
+ ensure
400
+ session.left.rollback_db_transaction
401
+ end
402
+ end
403
+
404
+ it "run should process trigger created change log records" do
405
+ begin
406
+ config = deep_copy(standard_config)
407
+ config.options[:committer] = :never_commit
408
+ config.options[:logged_replication_events] = [:all_changes]
409
+
410
+ session = Session.new(config)
411
+ session.left.execute "delete from rr_logged_events"
412
+ initializer = ReplicationInitializer.new(session)
413
+ initializer.create_trigger :left, 'extender_no_record'
414
+
415
+ session.left.insert_record 'extender_no_record', {
416
+ 'id' => '1',
417
+ 'name' => 'bla'
418
+ }
419
+
420
+ run = ReplicationRun.new session, TaskSweeper.new(1)
421
+ run.run
422
+
423
+ session.right.select_record(:table => "extender_no_record").should == {
424
+ 'id' => 1,
425
+ 'name' => 'bla'
426
+ }
427
+
428
+ # also verify that event was logged
429
+ row = session.left.select_one("select * from rr_logged_events")
430
+ row['diff_type'].should == 'left'
431
+ row['change_key'].should == '1'
432
+ row['description'].should == 'replicated'
433
+ ensure
434
+ Committers::NeverCommitter.rollback_current_session
435
+ if session
436
+ session.left.execute "delete from extender_no_record"
437
+ session.right.execute "delete from extender_no_record"
438
+ session.left.execute "delete from rr_pending_changes"
439
+ end
440
+ initializer.drop_trigger :left, 'extender_no_record' if initializer
441
+ end
442
+ end
443
+ end
@@ -0,0 +1,254 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ include RR
4
+
5
+ describe ReplicationRunner do
6
+ before(:each) do
7
+ Initializer.configuration = standard_config
8
+ end
9
+
10
+ it "should register itself with CommandRunner" do
11
+ CommandRunner.commands['replicate'][:command].should == ReplicationRunner
12
+ CommandRunner.commands['replicate'][:description].should be_an_instance_of(String)
13
+ end
14
+
15
+ it "process_options should make options as nil and teturn status as 1 if command line parameters are unknown" do
16
+ # also verify that an error message is printed
17
+ $stderr.should_receive(:puts).any_number_of_times
18
+ runner = ReplicationRunner.new
19
+ status = runner.process_options ["--nonsense"]
20
+ runner.options.should == nil
21
+ status.should == 1
22
+ end
23
+
24
+ it "process_options should make options as nil and return status as 1 if config option is not given" do
25
+ # also verify that an error message is printed
26
+ $stderr.should_receive(:puts).any_number_of_times
27
+ runner = ReplicationRunner.new
28
+ status = runner.process_options []
29
+ runner.options.should == nil
30
+ status.should == 1
31
+ end
32
+
33
+ it "process_options should make options as nil and return status as 0 if command line includes '--help'" do
34
+ # also verify that the help message is printed
35
+ $stderr.should_receive(:puts)
36
+ runner = ReplicationRunner.new
37
+ status = runner.process_options ["--help"]
38
+ runner.options.should == nil
39
+ status.should == 0
40
+ end
41
+
42
+ it "process_options should set the correct options" do
43
+ runner = ReplicationRunner.new
44
+ runner.process_options ["-c", "config_path"]
45
+ runner.options[:config_file].should == 'config_path'
46
+ end
47
+
48
+ it "run should not start a replication if the command line is invalid" do
49
+ $stderr.should_receive(:puts).any_number_of_times
50
+ ReplicationRunner.any_instance_should_not_receive(:execute) {
51
+ ReplicationRunner.run(["--nonsense"])
52
+ }
53
+ end
54
+
55
+ it "run should start a replication if the command line is correct" do
56
+ ReplicationRunner.any_instance_should_receive(:execute) {
57
+ ReplicationRunner.run(["--config=path"])
58
+ }
59
+ end
60
+
61
+ it "session should create and return the session" do
62
+ runner = ReplicationRunner.new
63
+ runner.options = {:config_file => "config/test_config.rb"}
64
+ runner.session.should be_an_instance_of(Session)
65
+ runner.session.should == runner.session # should only be created one time
66
+ end
67
+
68
+ it "pause_replication should not pause if next replication is already overdue" do
69
+ runner = ReplicationRunner.new
70
+ runner.stub!(:session).and_return(Session.new(standard_config))
71
+ waiter_thread = mock('waiter_thread')
72
+ waiter_thread.should_not_receive(:join)
73
+ runner.instance_variable_set(:@waiter_thread, waiter_thread)
74
+
75
+ runner.pause_replication # verify no wait during first run
76
+ runner.instance_variable_set(:@last_run, 1.hour.ago)
77
+ runner.pause_replication # verify no wait if overdue
78
+ end
79
+
80
+ it "pause_replication should pause for correct time frame" do
81
+ runner = ReplicationRunner.new
82
+ runner.stub!(:session).and_return(Session.new(deep_copy(standard_config)))
83
+ runner.session.configuration.stub!(:options).and_return(:replication_interval => 2)
84
+ waiter_thread = mock('waiter_thread')
85
+ runner.instance_variable_set(:@waiter_thread, waiter_thread)
86
+
87
+ now = Time.now
88
+ Time.stub!(:now).and_return(now)
89
+ runner.instance_variable_set(:@last_run, now - 1.seconds)
90
+ waiter_thread.should_receive(:join).and_return {|time| time.to_f.should be_close(1.0, 0.01); 0}
91
+
92
+ runner.pause_replication
93
+ end
94
+
95
+ it "init_waiter should setup correct signal processing" do
96
+ org_stdout = $stdout
97
+ $stdout = StringIO.new
98
+ begin
99
+ runner = ReplicationRunner.new
100
+ runner.stub!(:session).and_return(Session.new(standard_config))
101
+
102
+ # simulate sending the TERM signal
103
+ Signal.should_receive(:trap).with('TERM').and_yield
104
+
105
+ # also verify that the INT signal is trapped
106
+ Signal.should_receive(:trap).with('INT')
107
+
108
+ runner.init_waiter
109
+
110
+ # verify the that any pause would have been prematurely finished and
111
+ # termination signal been set
112
+ runner.termination_requested.should be_true
113
+ runner.instance_variable_get(:@waiter_thread).should_not be_alive
114
+ $stdout.string.should =~ /TERM.*shutdown/
115
+ ensure
116
+ $stdout = org_stdout
117
+ end
118
+ end
119
+
120
+ it "prepare_replication should call ReplicationInitializer#prepare_replication" do
121
+ runner = ReplicationRunner.new
122
+ runner.stub!(:session).and_return(:dummy_session)
123
+ initializer = mock('replication_initializer')
124
+ initializer.should_receive(:prepare_replication)
125
+ ReplicationInitializer.should_receive(:new).with(:dummy_session).and_return(initializer)
126
+ runner.prepare_replication
127
+ end
128
+
129
+ # Checks a specified number of times with specified waiting period between
130
+ # attempts if a given SQL query returns records.
131
+ # Returns +true+ if a record was found
132
+ # * +session+: an active Session object
133
+ # * +database+: either :+left+ or :+right+
134
+ # * +query+: sql query to execute
135
+ # * +max_attempts+: number of attempts to find the record
136
+ # * +interval+: waiting time in seconds between attempts
137
+ def check_for_record(session, database, query, max_attempts, interval)
138
+ found = false
139
+
140
+ max_attempts.times do
141
+ found = !!session.send(database).select_one(query)
142
+ break if found
143
+ sleep interval
144
+ end
145
+
146
+ found
147
+ end
148
+
149
+ it "execute should catch and print exceptions" do
150
+ org_stderr = $stderr
151
+ $stderr = StringIO.new
152
+ begin
153
+ session = Session.new
154
+ runner = ReplicationRunner.new
155
+ runner.stub!(:session).and_return(session)
156
+ runner.stub!(:init_waiter)
157
+ runner.stub!(:prepare_replication)
158
+ runner.stub!(:pause_replication)
159
+ runner.should_receive(:termination_requested).twice.and_return(false, true)
160
+
161
+ session.should_receive(:refresh).and_return {raise "refresh failed"}
162
+
163
+ runner.execute
164
+
165
+ $stderr.string.should =~ /Exception caught.*refresh failed/
166
+ $stderr.string.should =~ /replication_runner.rb:[0-9]+:in/
167
+ ensure
168
+ $stderr = org_stderr
169
+ end
170
+ end
171
+
172
+ it "execute_once should not clean up if successful" do
173
+ runner = ReplicationRunner.new
174
+ session = Session.new
175
+ runner.instance_variable_set(:@session, session)
176
+
177
+ runner.execute_once
178
+ runner.instance_variable_get(:@session).should == session
179
+ end
180
+
181
+ it "execute_once should clean up after failed replication runs" do
182
+ runner = ReplicationRunner.new
183
+ session = Session.new
184
+ runner.instance_variable_set(:@session, session)
185
+
186
+ session.should_receive(:refresh).and_raise('bla')
187
+ lambda {runner.execute_once}.should raise_error('bla')
188
+ runner.instance_variable_get(:@session).should be_nil
189
+ end
190
+
191
+ it "execute_once should raise exception if replication run times out" do
192
+ session = Session.new
193
+ runner = ReplicationRunner.new
194
+ runner.stub!(:session).and_return(session)
195
+ terminated = mock("terminated")
196
+ terminated.stub!(:terminated?).and_return(true)
197
+ TaskSweeper.stub!(:timeout).and_return(terminated)
198
+
199
+ lambda {runner.execute_once}.should raise_error(/timed out/)
200
+ end
201
+
202
+ it "execute should start the replication" do
203
+ config = deep_copy(standard_config)
204
+ config.options[:committer] = :buffered_commit
205
+ config.options[:replication_interval] = 0.01
206
+
207
+ # reset table selection
208
+ config.included_table_specs.replace ['scanner_left_records_only']
209
+ config.tables_with_options.clear
210
+
211
+ session = Session.new config
212
+ org_stdout = $stdout
213
+ begin
214
+ $stdout = StringIO.new
215
+ runner = ReplicationRunner.new
216
+ runner.process_options ["-c", "./config/test_config.rb"]
217
+ runner.stub!(:session).and_return(session)
218
+
219
+ t = Thread.new {runner.execute}
220
+
221
+ # verify that the initial sync is done
222
+ found = check_for_record(
223
+ session, :right,
224
+ "select * from scanner_left_records_only where id = 1",
225
+ 100, 0.01
226
+ )
227
+ t.should be_alive # verify that the replication thread didn't die
228
+ found.should be_true
229
+
230
+ session.left.execute "insert into scanner_left_records_only(name) values('bla')"
231
+
232
+ # verify that the replication works
233
+ check_for_record(
234
+ session, :right,
235
+ "select * from scanner_left_records_only where name = 'bla'",
236
+ 100, 0.01
237
+ ).should be_true
238
+
239
+ runner.instance_variable_set(:@termination_requested, true)
240
+ t.join
241
+ ensure
242
+ $stdout = org_stdout
243
+ initializer = ReplicationInitializer.new session
244
+ [:left, :right].each do |database|
245
+ initializer.clear_sequence_setup database, 'scanner_left_records_only'
246
+ if initializer.trigger_exists?(database, 'scanner_left_records_only')
247
+ initializer.drop_trigger database, 'scanner_left_records_only'
248
+ end
249
+ session.send(database).execute "delete from scanner_left_records_only where name = 'bla'"
250
+ end
251
+ session.right.execute "delete from scanner_left_records_only"
252
+ end
253
+ end
254
+ end