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.
- data/History.txt +83 -0
- data/License.txt +20 -0
- data/Manifest.txt +151 -0
- data/README.txt +37 -0
- data/bin/rubyrep +8 -0
- data/lib/rubyrep.rb +72 -0
- data/lib/rubyrep/base_runner.rb +195 -0
- data/lib/rubyrep/command_runner.rb +144 -0
- data/lib/rubyrep/committers/buffered_committer.rb +151 -0
- data/lib/rubyrep/committers/committers.rb +152 -0
- data/lib/rubyrep/configuration.rb +275 -0
- data/lib/rubyrep/connection_extenders/connection_extenders.rb +165 -0
- data/lib/rubyrep/connection_extenders/jdbc_extender.rb +65 -0
- data/lib/rubyrep/connection_extenders/mysql_extender.rb +59 -0
- data/lib/rubyrep/connection_extenders/postgresql_extender.rb +277 -0
- data/lib/rubyrep/database_proxy.rb +52 -0
- data/lib/rubyrep/direct_table_scan.rb +75 -0
- data/lib/rubyrep/generate_runner.rb +105 -0
- data/lib/rubyrep/initializer.rb +39 -0
- data/lib/rubyrep/log_helper.rb +30 -0
- data/lib/rubyrep/logged_change.rb +160 -0
- data/lib/rubyrep/logged_change_loader.rb +197 -0
- data/lib/rubyrep/noisy_connection.rb +80 -0
- data/lib/rubyrep/proxied_table_scan.rb +171 -0
- data/lib/rubyrep/proxy_block_cursor.rb +145 -0
- data/lib/rubyrep/proxy_connection.rb +431 -0
- data/lib/rubyrep/proxy_cursor.rb +44 -0
- data/lib/rubyrep/proxy_row_cursor.rb +43 -0
- data/lib/rubyrep/proxy_runner.rb +89 -0
- data/lib/rubyrep/replication_difference.rb +100 -0
- data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
- data/lib/rubyrep/replication_extenders/postgresql_replication.rb +236 -0
- data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
- data/lib/rubyrep/replication_helper.rb +142 -0
- data/lib/rubyrep/replication_initializer.rb +327 -0
- data/lib/rubyrep/replication_run.rb +142 -0
- data/lib/rubyrep/replication_runner.rb +166 -0
- data/lib/rubyrep/replicators/replicators.rb +42 -0
- data/lib/rubyrep/replicators/two_way_replicator.rb +361 -0
- data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
- data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
- data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
- data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
- data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
- data/lib/rubyrep/scan_runner.rb +25 -0
- data/lib/rubyrep/session.rb +230 -0
- data/lib/rubyrep/sync_helper.rb +121 -0
- data/lib/rubyrep/sync_runner.rb +31 -0
- data/lib/rubyrep/syncers/syncers.rb +112 -0
- data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
- data/lib/rubyrep/table_scan.rb +54 -0
- data/lib/rubyrep/table_scan_helper.rb +46 -0
- data/lib/rubyrep/table_sorter.rb +70 -0
- data/lib/rubyrep/table_spec_resolver.rb +142 -0
- data/lib/rubyrep/table_sync.rb +90 -0
- data/lib/rubyrep/task_sweeper.rb +77 -0
- data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
- data/lib/rubyrep/type_casting_cursor.rb +31 -0
- data/lib/rubyrep/uninstall_runner.rb +93 -0
- data/lib/rubyrep/version.rb +9 -0
- data/rubyrep +8 -0
- data/rubyrep.bat +4 -0
- data/setup.rb +1585 -0
- data/spec/base_runner_spec.rb +218 -0
- data/spec/buffered_committer_spec.rb +274 -0
- data/spec/command_runner_spec.rb +145 -0
- data/spec/committers_spec.rb +178 -0
- data/spec/configuration_spec.rb +203 -0
- data/spec/connection_extender_interface_spec.rb +141 -0
- data/spec/connection_extenders_registration_spec.rb +164 -0
- data/spec/database_proxy_spec.rb +48 -0
- data/spec/database_rake_spec.rb +40 -0
- data/spec/db_specific_connection_extenders_spec.rb +34 -0
- data/spec/db_specific_replication_extenders_spec.rb +38 -0
- data/spec/direct_table_scan_spec.rb +61 -0
- data/spec/dolphins.jpg +0 -0
- data/spec/generate_runner_spec.rb +84 -0
- data/spec/initializer_spec.rb +46 -0
- data/spec/log_helper_spec.rb +39 -0
- data/spec/logged_change_loader_spec.rb +68 -0
- data/spec/logged_change_spec.rb +470 -0
- data/spec/noisy_connection_spec.rb +78 -0
- data/spec/postgresql_replication_spec.rb +48 -0
- data/spec/postgresql_schema_support_spec.rb +212 -0
- data/spec/postgresql_support_spec.rb +63 -0
- data/spec/progress_bar_spec.rb +77 -0
- data/spec/proxied_table_scan_spec.rb +151 -0
- data/spec/proxy_block_cursor_spec.rb +197 -0
- data/spec/proxy_connection_spec.rb +423 -0
- data/spec/proxy_cursor_spec.rb +56 -0
- data/spec/proxy_row_cursor_spec.rb +66 -0
- data/spec/proxy_runner_spec.rb +70 -0
- data/spec/replication_difference_spec.rb +161 -0
- data/spec/replication_extender_interface_spec.rb +367 -0
- data/spec/replication_extenders_spec.rb +32 -0
- data/spec/replication_helper_spec.rb +178 -0
- data/spec/replication_initializer_spec.rb +509 -0
- data/spec/replication_run_spec.rb +443 -0
- data/spec/replication_runner_spec.rb +254 -0
- data/spec/replicators_spec.rb +36 -0
- data/spec/rubyrep_spec.rb +8 -0
- data/spec/scan_detail_reporter_spec.rb +119 -0
- data/spec/scan_progress_printers_spec.rb +68 -0
- data/spec/scan_report_printers_spec.rb +67 -0
- data/spec/scan_runner_spec.rb +50 -0
- data/spec/scan_summary_reporter_spec.rb +61 -0
- data/spec/session_spec.rb +253 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +305 -0
- data/spec/strange_name_support_spec.rb +135 -0
- data/spec/sync_helper_spec.rb +169 -0
- data/spec/sync_runner_spec.rb +78 -0
- data/spec/syncers_spec.rb +171 -0
- data/spec/table_scan_helper_spec.rb +36 -0
- data/spec/table_scan_spec.rb +49 -0
- data/spec/table_sorter_spec.rb +30 -0
- data/spec/table_spec_resolver_spec.rb +111 -0
- data/spec/table_sync_spec.rb +140 -0
- data/spec/task_sweeper_spec.rb +47 -0
- data/spec/trigger_mode_switcher_spec.rb +83 -0
- data/spec/two_way_replicator_spec.rb +721 -0
- data/spec/two_way_syncer_spec.rb +256 -0
- data/spec/type_casting_cursor_spec.rb +50 -0
- data/spec/uninstall_runner_spec.rb +93 -0
- 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
|