rubyrep 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. data/History.txt +4 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +137 -0
  4. data/README.txt +37 -0
  5. data/Rakefile +30 -0
  6. data/bin/rubyrep +8 -0
  7. data/config/hoe.rb +72 -0
  8. data/config/mysql_config.rb +25 -0
  9. data/config/postgres_config.rb +21 -0
  10. data/config/proxied_test_config.rb +14 -0
  11. data/config/redmine_config.rb +17 -0
  12. data/config/rep_config.rb +20 -0
  13. data/config/requirements.rb +32 -0
  14. data/config/test_config.rb +20 -0
  15. data/lib/rubyrep/base_runner.rb +195 -0
  16. data/lib/rubyrep/command_runner.rb +144 -0
  17. data/lib/rubyrep/committers/buffered_committer.rb +140 -0
  18. data/lib/rubyrep/committers/committers.rb +146 -0
  19. data/lib/rubyrep/configuration.rb +240 -0
  20. data/lib/rubyrep/connection_extenders/connection_extenders.rb +133 -0
  21. data/lib/rubyrep/connection_extenders/jdbc_extender.rb +284 -0
  22. data/lib/rubyrep/connection_extenders/mysql_extender.rb +168 -0
  23. data/lib/rubyrep/connection_extenders/postgresql_extender.rb +261 -0
  24. data/lib/rubyrep/database_proxy.rb +52 -0
  25. data/lib/rubyrep/direct_table_scan.rb +75 -0
  26. data/lib/rubyrep/generate_runner.rb +105 -0
  27. data/lib/rubyrep/initializer.rb +39 -0
  28. data/lib/rubyrep/logged_change.rb +326 -0
  29. data/lib/rubyrep/proxied_table_scan.rb +171 -0
  30. data/lib/rubyrep/proxy_block_cursor.rb +145 -0
  31. data/lib/rubyrep/proxy_connection.rb +318 -0
  32. data/lib/rubyrep/proxy_cursor.rb +44 -0
  33. data/lib/rubyrep/proxy_row_cursor.rb +43 -0
  34. data/lib/rubyrep/proxy_runner.rb +89 -0
  35. data/lib/rubyrep/replication_difference.rb +91 -0
  36. data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
  37. data/lib/rubyrep/replication_extenders/postgresql_replication.rb +204 -0
  38. data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
  39. data/lib/rubyrep/replication_helper.rb +104 -0
  40. data/lib/rubyrep/replication_initializer.rb +307 -0
  41. data/lib/rubyrep/replication_run.rb +48 -0
  42. data/lib/rubyrep/replication_runner.rb +138 -0
  43. data/lib/rubyrep/replicators/replicators.rb +37 -0
  44. data/lib/rubyrep/replicators/two_way_replicator.rb +334 -0
  45. data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
  46. data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
  47. data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
  48. data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
  49. data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
  50. data/lib/rubyrep/scan_runner.rb +25 -0
  51. data/lib/rubyrep/session.rb +177 -0
  52. data/lib/rubyrep/sync_helper.rb +111 -0
  53. data/lib/rubyrep/sync_runner.rb +31 -0
  54. data/lib/rubyrep/syncers/syncers.rb +112 -0
  55. data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
  56. data/lib/rubyrep/table_scan.rb +54 -0
  57. data/lib/rubyrep/table_scan_helper.rb +38 -0
  58. data/lib/rubyrep/table_sorter.rb +70 -0
  59. data/lib/rubyrep/table_spec_resolver.rb +136 -0
  60. data/lib/rubyrep/table_sync.rb +68 -0
  61. data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
  62. data/lib/rubyrep/type_casting_cursor.rb +31 -0
  63. data/lib/rubyrep/uninstall_runner.rb +92 -0
  64. data/lib/rubyrep/version.rb +9 -0
  65. data/lib/rubyrep.rb +68 -0
  66. data/script/destroy +14 -0
  67. data/script/generate +14 -0
  68. data/script/txt2html +74 -0
  69. data/setup.rb +1585 -0
  70. data/sims/performance/big_rep_spec.rb +100 -0
  71. data/sims/performance/big_scan_spec.rb +57 -0
  72. data/sims/performance/big_sync_spec.rb +141 -0
  73. data/sims/performance/performance.rake +228 -0
  74. data/sims/sim_helper.rb +24 -0
  75. data/spec/base_runner_spec.rb +218 -0
  76. data/spec/buffered_committer_spec.rb +271 -0
  77. data/spec/command_runner_spec.rb +145 -0
  78. data/spec/committers_spec.rb +174 -0
  79. data/spec/configuration_spec.rb +198 -0
  80. data/spec/connection_extender_interface_spec.rb +138 -0
  81. data/spec/connection_extenders_registration_spec.rb +129 -0
  82. data/spec/database_proxy_spec.rb +48 -0
  83. data/spec/database_rake_spec.rb +40 -0
  84. data/spec/db_specific_connection_extenders_spec.rb +34 -0
  85. data/spec/db_specific_replication_extenders_spec.rb +38 -0
  86. data/spec/direct_table_scan_spec.rb +61 -0
  87. data/spec/generate_runner_spec.rb +84 -0
  88. data/spec/initializer_spec.rb +46 -0
  89. data/spec/logged_change_spec.rb +480 -0
  90. data/spec/postgresql_replication_spec.rb +48 -0
  91. data/spec/postgresql_support_spec.rb +57 -0
  92. data/spec/progress_bar_spec.rb +77 -0
  93. data/spec/proxied_table_scan_spec.rb +151 -0
  94. data/spec/proxy_block_cursor_spec.rb +197 -0
  95. data/spec/proxy_connection_spec.rb +399 -0
  96. data/spec/proxy_cursor_spec.rb +56 -0
  97. data/spec/proxy_row_cursor_spec.rb +66 -0
  98. data/spec/proxy_runner_spec.rb +70 -0
  99. data/spec/replication_difference_spec.rb +160 -0
  100. data/spec/replication_extender_interface_spec.rb +365 -0
  101. data/spec/replication_extenders_spec.rb +32 -0
  102. data/spec/replication_helper_spec.rb +121 -0
  103. data/spec/replication_initializer_spec.rb +477 -0
  104. data/spec/replication_run_spec.rb +166 -0
  105. data/spec/replication_runner_spec.rb +213 -0
  106. data/spec/replicators_spec.rb +31 -0
  107. data/spec/rubyrep_spec.rb +8 -0
  108. data/spec/scan_detail_reporter_spec.rb +119 -0
  109. data/spec/scan_progress_printers_spec.rb +68 -0
  110. data/spec/scan_report_printers_spec.rb +67 -0
  111. data/spec/scan_runner_spec.rb +50 -0
  112. data/spec/scan_summary_reporter_spec.rb +61 -0
  113. data/spec/session_spec.rb +212 -0
  114. data/spec/spec.opts +1 -0
  115. data/spec/spec_helper.rb +295 -0
  116. data/spec/sync_helper_spec.rb +157 -0
  117. data/spec/sync_runner_spec.rb +78 -0
  118. data/spec/syncers_spec.rb +171 -0
  119. data/spec/table_scan_helper_spec.rb +29 -0
  120. data/spec/table_scan_spec.rb +49 -0
  121. data/spec/table_sorter_spec.rb +31 -0
  122. data/spec/table_spec_resolver_spec.rb +102 -0
  123. data/spec/table_sync_spec.rb +84 -0
  124. data/spec/trigger_mode_switcher_spec.rb +83 -0
  125. data/spec/two_way_replicator_spec.rb +551 -0
  126. data/spec/two_way_syncer_spec.rb +256 -0
  127. data/spec/type_casting_cursor_spec.rb +50 -0
  128. data/spec/uninstall_runner_spec.rb +86 -0
  129. data/tasks/database.rake +439 -0
  130. data/tasks/deployment.rake +29 -0
  131. data/tasks/environment.rake +9 -0
  132. data/tasks/java.rake +37 -0
  133. data/tasks/redmine_test.rake +47 -0
  134. data/tasks/rspec.rake +68 -0
  135. data/tasks/rubyrep.tailor +18 -0
  136. data/tasks/stats.rake +19 -0
  137. data/tasks/task_helper.rb +20 -0
  138. data.tar.gz.sig +0 -0
  139. metadata +243 -0
  140. metadata.gz.sig +0 -0
@@ -0,0 +1,334 @@
1
+ module RR
2
+ module Replicators
3
+ # This replicator implements a two way replication.
4
+ # Options:
5
+ # * :+left_change_handling+, :+right_change_handling+:
6
+ # Handling of records that were changed only in the named database.
7
+ # Can be any of the following:
8
+ # * :+ignore+: No action.
9
+ # * :+replicate+: Updates other database accordingly. *Default* *Setting*
10
+ # * +Proc+ object:
11
+ # If a Proc object is given, it is responsible for dealing with the
12
+ # record. Called with the following parameters:
13
+ # * replication_helper: The current ReplicationHelper instance.
14
+ # * difference: A ReplicationDifference instance describing the change
15
+ # * :+replication_conflict_handling+:
16
+ # Handling of conflicting record changes. Can be any of the following:
17
+ # * :+ignore+: No action. *Default* *Setting*
18
+ # * :+left_wins+: The right database is updated accordingly.
19
+ # * :+right_wins+: The left database is updated accordingly.
20
+ # * :+later_wins+:
21
+ # The more recent change is replicated.
22
+ # (If both changes have same age: left change is replicated)
23
+ # * :+earlier_wins+:
24
+ # The less recent change is replicated.
25
+ # (If both records have same age: left change is replicated)
26
+ # * +Proc+ object:
27
+ # If a Proc object is given, it is responsible for dealing with the
28
+ # record. Called with the following parameters:
29
+ # * replication_helper: The current ReplicationHelper instance.
30
+ # * difference: A ReplicationDifference instance describing the changes
31
+ # * :+logged_replication_events+:
32
+ # Specifies which types of replications are logged.
33
+ # Is either a single value or an array of multiple ones.
34
+ # Default: [:ignored_conflicts]
35
+ # Possible values:
36
+ # * :+ignored_changes+: log ignored (but not replicated) non-conflict changes
37
+ # * :+all_changes+: log all non-conflict changes
38
+ # * :+ignored_conflicts+: log ignored (but not replicated) conflicts
39
+ # * :+all_conflicts+: log all conflicts
40
+ #
41
+ # Example of using a Proc object for custom behaviour:
42
+ # lambda do |rep_helper, diff|
43
+ # # if specified as replication_conflict_handling option, logs all
44
+ # # conflicts to a text file
45
+ # File.open('/var/log/rubyrep_conflict_log', 'a') do |f|
46
+ # f.puts <<-end_str
47
+ # #{Time.now}: conflict
48
+ # * in table #{diff.changes[:left].table}
49
+ # * for record '#{diff.changes[:left].key}'
50
+ # * change type in left db: '#{diff.changes[:left].type}'
51
+ # * change type in right db: '#{diff.changes[:right].type}'
52
+ # end_str
53
+ # end
54
+ # end
55
+ class TwoWayReplicator
56
+
57
+ # Register the syncer
58
+ Replicators.register :two_way => self
59
+
60
+ # The current ReplicationHelper object
61
+ attr_accessor :rep_helper
62
+
63
+ # Default TwoWayReplicator options.
64
+ DEFAULT_OPTIONS = {
65
+ :left_change_handling => :replicate,
66
+ :right_change_handling => :replicate,
67
+ :replication_conflict_handling => :ignore,
68
+ :logged_replication_events => [:ignored_conflicts],
69
+ }
70
+
71
+ # Checks if an option is configured correctly. Raises an ArgumentError if not.
72
+ # * +table_spec+: the table specification to which the option belongs. May be +nil+.
73
+ # * +valid_option_values+: array of valid option values
74
+ # * +option_key+: the key of the option that is to be checked
75
+ # * +option_value+: the value of the option that is to be checked
76
+ def verify_option(table_spec, valid_option_values, option_key, option_value)
77
+ unless valid_option_values.include? option_value
78
+ message = ""
79
+ message << "#{table_spec.inspect}: " if table_spec
80
+ message << "#{option_value.inspect} not a valid #{option_key.inspect} option"
81
+ raise ArgumentError.new(message)
82
+ end
83
+ end
84
+
85
+ # Verifies if the :+left_change_handling+ / :+right_change_handling+
86
+ # options are valid.
87
+ # Raises an ArgumentError if an option is invalid
88
+ def validate_change_handling_options
89
+ [:left_change_handling, :right_change_handling].each do |key|
90
+ rep_helper.session.configuration.each_matching_option(key) do |table_spec, value|
91
+ unless value.respond_to? :call
92
+ verify_option table_spec, [:ignore, :replicate], key, value
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ # Verifies if the given :+replication_conflict_handling+ options are valid.
99
+ # Raises an ArgumentError if an option is invalid.
100
+ def validate_conflict_handling_options
101
+ rep_helper.session.configuration.each_matching_option(:replication_conflict_handling) do |table_spec, value|
102
+ unless value.respond_to? :call
103
+ verify_option table_spec,
104
+ [:ignore, :left_wins, :right_wins, :later_wins, :earlier_wins],
105
+ :replication_conflict_handling, value
106
+ end
107
+ end
108
+ end
109
+
110
+ # Verifies if the given :+replication_logging+ option /options is / are valid.
111
+ # Raises an ArgumentError if invalid
112
+ def validate_logging_options
113
+ rep_helper.session.configuration.each_matching_option(:logged_replication_events) do |table_spec, values|
114
+ values = [values].flatten # ensure that I have an array
115
+ values.each do |value|
116
+ verify_option table_spec,
117
+ [:ignored_changes, :all_changes, :ignored_conflicts, :all_conflicts],
118
+ :logged_replication_events, value
119
+ end
120
+ end
121
+ end
122
+
123
+ # Initializes the TwoWayReplicator
124
+ # Raises an ArgumentError if any of the replication options is invalid.
125
+ #
126
+ # Parameters:
127
+ # * rep_helper:
128
+ # The ReplicationHelper object providing information and utility functions.
129
+ def initialize(rep_helper)
130
+ self.rep_helper = rep_helper
131
+
132
+ validate_change_handling_options
133
+ validate_conflict_handling_options
134
+ validate_logging_options
135
+ end
136
+
137
+ # Shortcut to calculate the "other" database.
138
+ OTHER_SIDE = {
139
+ :left => :right,
140
+ :right => :left
141
+ }
142
+
143
+ # Specifies how to clear conflicts.
144
+ # The outer hash keys describe the type of the winning change.
145
+ # The inner hash keys describe the type of the loosing change.
146
+ # The inser hash values describe the action to take on the loosing side.
147
+ CONFLICT_STATE_MATRIX = {
148
+ :insert => {:insert => :update, :update => :update, :delete => :insert},
149
+ :update => {:insert => :update, :update => :update, :delete => :insert},
150
+ :delete => {:insert => :delete, :update => :delete, :delete => :delete}
151
+ }
152
+
153
+ # Helper function that clears a conflict by taking the change from the
154
+ # specified winning database and updating the other database accordingly.
155
+ # * +source_db+: the winning database (either :+left+ or :+right+)
156
+ # * +diff+: the ReplicationDifference instance
157
+ # * +remaining_attempts+: the number of remaining replication attempts for this difference
158
+ def clear_conflict(source_db, diff, remaining_attempts)
159
+ source_change = diff.changes[source_db]
160
+ target_db = OTHER_SIDE[source_db]
161
+ target_change = diff.changes[target_db]
162
+
163
+ target_action = CONFLICT_STATE_MATRIX[source_change.type][target_change.type]
164
+ source_key = source_change.type == :update ? source_change.new_key : source_change.key
165
+ target_key = target_change.type == :update ? target_change.new_key : target_change.key
166
+ case target_action
167
+ when :insert
168
+ attempt_insert source_db, diff, remaining_attempts, source_key
169
+ when :update
170
+ attempt_update source_db, diff, remaining_attempts, source_key, target_key
171
+ when :delete
172
+ attempt_delete source_db, diff, target_key
173
+ end
174
+ end
175
+
176
+ # Returns the options for the specified table name.
177
+ # * +table+: name of the table (left database version)
178
+ def options_for_table(table)
179
+ @options_for_table ||= {}
180
+ unless @options_for_table.include? table
181
+ @options_for_table[table] = DEFAULT_OPTIONS.merge(
182
+ rep_helper.session.configuration.options_for_table(table))
183
+ end
184
+ @options_for_table[table]
185
+ end
186
+
187
+ # Logs replication of the specified difference as per configured
188
+ # :+replication_conflict_logging+ / :+left_change_logging+ / :+right_change_logging+ options.
189
+ # * +winner+: Either the winner database (:+left+ or :+right+) or :+ignore+
190
+ # * +diff+: the ReplicationDifference instance
191
+ def log_replication_outcome(winner, diff)
192
+ options = options_for_table(diff.changes[:left].table)
193
+ option_values = [options[:logged_replication_events]].flatten # make sure I have an array
194
+ if diff.type == :conflict
195
+ return unless option_values.include?(:all_conflicts) or option_values.include?(:ignored_conflicts)
196
+ return if winner != :ignore and not option_values.include?(:all_conflicts)
197
+ outcome = {:left => 'left_won', :right => 'right_won', :ignore => 'ignored'}[winner]
198
+ else
199
+ return unless option_values.include?(:all_changes) or option_values.include?(:ignored_changes)
200
+ return if winner != :ignore and not option_values.include?(:all_changes)
201
+ outcome = winner == :ignore ? 'ignored' : 'replicated'
202
+ end
203
+ rep_helper.log_replication_outcome diff, outcome
204
+ end
205
+
206
+ # How often a replication will be attempted (in case it fails because the
207
+ # record in question was removed from the source or inserted into the
208
+ # target database _after_ the ReplicationDifference was loaded
209
+ MAX_REPLICATION_ATTEMPTS = 2
210
+
211
+ # Attempts to read the specified record from the source database and insert
212
+ # it into the target database.
213
+ # Retries if insert fails due to missing source or suddenly existing target
214
+ # record.
215
+ # * +source_db+: either :+left+ or :+right+ - source database of replication
216
+ # * +diff+: the current ReplicationDifference instance
217
+ # * +remaining_attempts+: the number of remaining replication attempts for this difference
218
+ # * +source_key+: a column_name => value hash identifying the source record
219
+ def attempt_insert(source_db, diff, remaining_attempts, source_key)
220
+ source_change = diff.changes[source_db]
221
+ source_table = source_change.table
222
+ target_db = OTHER_SIDE[source_db]
223
+ target_table = rep_helper.corresponding_table(source_db, source_table)
224
+
225
+ values = rep_helper.load_record source_db, source_table, source_key
226
+ if values == nil
227
+ diff.amend
228
+ replicate_difference diff, remaining_attempts - 1
229
+ else
230
+ begin
231
+ # note: savepoints have to be used for postgresql (as a failed SQL
232
+ # statement will otherwise invalidate the complete transaction.)
233
+ rep_helper.session.send(target_db).execute "savepoint rr_insert"
234
+ log_replication_outcome source_db, diff
235
+ rep_helper.insert_record target_db, target_table, values
236
+ rescue Exception => e
237
+ rep_helper.session.send(target_db).execute "rollback to savepoint rr_insert"
238
+ row = rep_helper.load_record target_db, target_table, source_key
239
+ raise unless row # problem is not the existence of the record in the target db
240
+ diff.amend
241
+ replicate_difference diff, remaining_attempts - 1
242
+ end
243
+ end
244
+ end
245
+
246
+ # Attempts to read the specified record from the source database and update
247
+ # the specified record in the target database.
248
+ # Retries if update fails due to missing source
249
+ # * +source_db+: either :+left+ or :+right+ - source database of replication
250
+ # * +diff+: the current ReplicationDifference instance
251
+ # * +remaining_attempts+: the number of remaining replication attempts for this difference
252
+ # * +source_key+: a column_name => value hash identifying the source record
253
+ # * +target_key+: a column_name => value hash identifying the source record
254
+ def attempt_update(source_db, diff, remaining_attempts, source_key, target_key)
255
+ source_change = diff.changes[source_db]
256
+ source_table = source_change.table
257
+ target_db = OTHER_SIDE[source_db]
258
+ target_table = rep_helper.corresponding_table(source_db, source_table)
259
+
260
+ values = rep_helper.load_record source_db, source_table, source_key
261
+ if values == nil
262
+ diff.amend
263
+ replicate_difference diff, remaining_attempts - 1
264
+ else
265
+ log_replication_outcome source_db, diff
266
+ rep_helper.update_record target_db, target_table, values, target_key
267
+ end
268
+ end
269
+
270
+ # Attempts delete the source record from the target database.
271
+ # E. g. if +source_db is :+left+, then the record is deleted in database
272
+ # :+right+.
273
+ # * +source_db+: either :+left+ or :+right+ - source database of replication
274
+ # * +diff+: the current ReplicationDifference instance
275
+ # * +target_key+: a column_name => value hash identifying the source record
276
+ def attempt_delete(source_db, diff, target_key)
277
+ change = diff.changes[source_db]
278
+ target_db = OTHER_SIDE[source_db]
279
+ target_table = rep_helper.corresponding_table(source_db, change.table)
280
+ log_replication_outcome source_db, diff
281
+ rep_helper.delete_record target_db, target_table, target_key
282
+ end
283
+
284
+ # Called to replicate the specified difference.
285
+ # * :+diff+: ReplicationDifference instance
286
+ # * :+remaining_attempts+: how many more times a replication will be attempted
287
+ def replicate_difference(diff, remaining_attempts = MAX_REPLICATION_ATTEMPTS)
288
+ raise Exception, "max replication attempts exceeded" if remaining_attempts == 0
289
+ options = options_for_table(diff.changes[:left].table)
290
+ if diff.type == :left or diff.type == :right
291
+ key = diff.type == :left ? :left_change_handling : :right_change_handling
292
+ option = options[key]
293
+
294
+ if option == :ignore
295
+ log_replication_outcome :ignore, diff
296
+ elsif option == :replicate
297
+ source_db = diff.type
298
+
299
+ change = diff.changes[source_db]
300
+
301
+ case change.type
302
+ when :insert
303
+ attempt_insert source_db, diff, remaining_attempts, change.key
304
+ when :update
305
+ attempt_update source_db, diff, remaining_attempts, change.new_key, change.key
306
+ when :delete
307
+ attempt_delete source_db, diff, change.key
308
+ end
309
+ else # option must be a Proc
310
+ option.call rep_helper, diff
311
+ end
312
+ elsif diff.type == :conflict
313
+ option = options[:replication_conflict_handling]
314
+ if option == :ignore
315
+ log_replication_outcome :ignore, diff
316
+ elsif option == :left_wins
317
+ clear_conflict :left, diff, remaining_attempts
318
+ elsif option == :right_wins
319
+ clear_conflict :right, diff, remaining_attempts
320
+ elsif option == :later_wins
321
+ winner_db = diff.changes[:left].last_changed_at >= diff.changes[:right].last_changed_at ? :left : :right
322
+ clear_conflict winner_db, diff, remaining_attempts
323
+ elsif option == :earlier_wins
324
+ winner_db = diff.changes[:left].last_changed_at <= diff.changes[:right].last_changed_at ? :left : :right
325
+ clear_conflict winner_db, diff, remaining_attempts
326
+ else # option must be a Proc
327
+ option.call rep_helper, diff
328
+ end
329
+ end
330
+ end
331
+
332
+ end
333
+ end
334
+ end
@@ -0,0 +1,65 @@
1
+ module RR
2
+ module ScanProgressPrinters
3
+
4
+ # A helper class to print a text progress bar.
5
+ class ProgressBar
6
+
7
+ MAX_MARKERS = 25 #length of the progress bar (in characters)
8
+
9
+ # Register ProgressBar with the given command line options.
10
+ # (Command line format as specified by OptionParser#on.)
11
+ # First argument is the key through which the printer can be refered in
12
+ # the configuration file
13
+ RR::ScanProgressPrinters.register :progress_bar, self,
14
+ "-b", "--progress-bar[=length]",
15
+ "Show the progress of the table scanning process as progress bar."
16
+
17
+ # Receives the command line argument
18
+ cattr_accessor :arg
19
+
20
+ # Returns the length (in characters) of the progress bar.
21
+ def max_markers
22
+ @max_markers ||= arg ? arg.to_i : MAX_MARKERS
23
+ end
24
+
25
+ # Creates a new progress bar.
26
+ # * +max_steps+: number of steps at completion
27
+ # * +session+: the current Session
28
+ # * +left_table+: name of the left database table
29
+ # * +right_table+: name of the right database table
30
+ def initialize(max_steps, session, left_table, right_table)
31
+ @use_ansi = session.configuration.options_for_table(left_table)[:use_ansi]
32
+ @max_steps, @current_steps = max_steps, 0
33
+ @steps_per_marker = @max_steps.to_f / max_markers
34
+ @current_markers, @current_percentage = 0, 0
35
+ end
36
+
37
+ # Increases progress by +step_increment+ steps.
38
+ def step(step_increment = 1)
39
+ @current_steps+= step_increment
40
+ new_markers = @max_steps != 0 ? (@current_steps / @steps_per_marker).to_i : max_markers
41
+
42
+ new_percentage = @max_steps != 0 ? @current_steps * 100 / @max_steps : 100
43
+ if @use_ansi and new_percentage != @current_percentage
44
+ # This part uses ANSI escape sequences to show a running percentage
45
+ # to the left of the progress bar
46
+ print "\e[1D" * (@current_markers + 5) if @current_percentage != 0 # go left
47
+ print "#{new_percentage}%".rjust(4) << " "
48
+ print "\e[1C" * @current_markers if @current_markers != 0 # go back right
49
+ $stdout.flush
50
+ @current_percentage = new_percentage
51
+ end
52
+
53
+ if new_markers > @current_markers
54
+ print '.' * (new_markers - @current_markers)
55
+ @current_markers = new_markers
56
+ $stdout.flush
57
+ end
58
+ if @current_steps == @max_steps
59
+ print '.' * (max_markers - @current_markers) + ' '
60
+ $stdout.flush
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,65 @@
1
+ module RR
2
+ # Manages scan progress printers. Scan progress printers implement functionality
3
+ # to report the progress of a table scan.
4
+ # *Each* table scan is handled by a *separate* printer instance.
5
+ #
6
+ # Scan progress printers need to register themselves and their command line options
7
+ # with #register.
8
+ #
9
+ # A scan progress printer needs to implement at the minimum the following
10
+ # functionality:
11
+ #
12
+ # # Receives the command line argument as yielded by OptionParser#on.
13
+ # def self.arg=(arg)
14
+ #
15
+ # # Creation of a new ScanProgressPrinter.
16
+ # # * +max_steps+: number of steps at completion
17
+ # # * +session+: the current Session
18
+ # # * +left_table+: name of the left database table
19
+ # # * +right_table+: name of the right database table
20
+ # def initialize(max_steps, left_table, right_table)
21
+ #
22
+ # # Progress is advanced by +progress+ number of steps.
23
+ # def step(progress)
24
+ #
25
+ module ScanProgressPrinters
26
+
27
+ # Hash of registered ScanProgressPrinters.
28
+ # Each entry is a hash with the following key and related value:
29
+ # * key: Identifier of the progress printer
30
+ # * value: A hash with payload information. Possible values:
31
+ # * :+printer_class+: The ScanProgressPrinter class.
32
+ # * :+opts+: An array defining the command line options (handed to OptionParter#on).
33
+ def self.printers
34
+ @@progress_printers ||= {}
35
+ end
36
+
37
+ # Needs to be called by ScanProgressPrinters to register themselves (+printer+)
38
+ # and their command line options.
39
+ # * :+printer_id+ is the symbol through which the printer can be referenced.
40
+ # * :+printer_class+ is the ScanProgressPrinter class,
41
+ # * :+opts+ is an array defining the command line options (handed to OptionParter#on).
42
+ def self.register(printer_id, printer_class, *opts)
43
+ printers[printer_id] = {
44
+ :printer_class => printer_class,
45
+ :opts => opts
46
+ }
47
+ end
48
+
49
+ # Registers all report printer command line options into the given
50
+ # OptionParser.
51
+ # Once the command line is parsed with OptionParser#parse! it will
52
+ # yield the correct printer class.
53
+ #
54
+ # Note:
55
+ # If multiple printers are specified in the command line, all are yielded.
56
+ def self.on_printer_selection(opts)
57
+ printers.each_value do |printer|
58
+ opts.on(*printer[:opts]) do |arg|
59
+ printer[:printer_class].arg = arg
60
+ yield printer[:printer_class]
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,111 @@
1
+ require 'fileutils'
2
+ require 'tmpdir'
3
+ require 'tempfile'
4
+ require 'yaml'
5
+
6
+ module RR::ScanReportPrinters
7
+ # A ScanReportPrinter producing a summary (number of differences) only.
8
+ class ScanDetailReporter < ScanSummaryReporter
9
+
10
+ # Register ScanSummaryReporter with the given command line options.
11
+ # (Command line format as specified by OptionParser#on.)
12
+ RR::ScanReportPrinters.register self, "-d", "--detailed[=mode]",
13
+ "Print the number of differences of each table. E. g.",
14
+ " left_table / right_table [differences]",
15
+ "followed by a full dump of the differences in YAML format.",
16
+ "The 'mode' argument determines how the row differences are printed:",
17
+ " * full shows the full records",
18
+ " * keys shows the primary key columns only",
19
+ " * diff shows the primary key and differing columsn only"
20
+
21
+ # The current Session object
22
+ attr_accessor :session
23
+
24
+ # The temporary File receiving the differences
25
+ attr_accessor :tmpfile
26
+
27
+ # Mode of reporting. Should be either
28
+ # * :+full+
29
+ # * :+keys+ or
30
+ # * :+diff+
31
+ attr_accessor :report_mode
32
+
33
+ # Array of names of the primary key columns of the table currently being
34
+ # scanned.
35
+ attr_accessor :primary_key_names
36
+
37
+ # A scan run is to be started using this scan result printer.
38
+ # +arg+ is the command line argument as yielded by OptionParser#on.
39
+ def initialize(session, arg)
40
+ super session, ""
41
+ self.session = session
42
+
43
+ self.report_mode = case arg
44
+ when 'diff'
45
+ :diff
46
+ when 'keys'
47
+ :keys
48
+ else
49
+ :full
50
+ end
51
+ end
52
+
53
+ # A scan of the given 'left' table and corresponding 'right' table is executed.
54
+ # Needs to yield so that the actual scan can be executed.
55
+ def scan(left_table, right_table)
56
+
57
+ super left_table, right_table
58
+
59
+ ensure
60
+ self.primary_key_names = nil
61
+ if self.tmpfile
62
+ self.tmpfile.close
63
+ self.tmpfile.open
64
+ self.tmpfile.each_line {|line| puts line}
65
+ self.tmpfile.close!
66
+ self.tmpfile = nil
67
+ end
68
+ end
69
+
70
+ # Returns a cleaned row as per current +report_mode+.
71
+ # +row+ is either a column_name => value hash or an array of 2 such rows.
72
+ def clear_columns(row)
73
+ case report_mode
74
+ when :full
75
+ row
76
+ when :keys
77
+ row = row[0] if row.kind_of?(Array)
78
+ self.primary_key_names ||= session.left.primary_key_names(self.left_table)
79
+ row.reject {|column, value| !self.primary_key_names.include?(column)}
80
+ when :diff
81
+ self.primary_key_names ||= session.left.primary_key_names(self.left_table)
82
+ if row.kind_of?(Array)
83
+ new_row_array = [{}, {}]
84
+ row[0].each do |column, value|
85
+ if self.primary_key_names.include?(column) or value != row[1][column]
86
+ new_row_array[0][column] = row[0][column]
87
+ new_row_array[1][column] = row[1][column]
88
+ end
89
+ end
90
+ new_row_array
91
+ else
92
+ row
93
+ end
94
+ end
95
+ end
96
+
97
+ # Each difference is handed to the printer as described in the format
98
+ # as described e. g. in DirectTableScan#run
99
+ def report_difference(type, row)
100
+ self.tmpfile ||= Tempfile.new 'rubyrep_scan_details'
101
+ tmpfile.puts({type => clear_columns(row)}.to_yaml)
102
+ super type, row
103
+ end
104
+
105
+ # Optional method. If a scan report printer has it, it is called after the
106
+ # last table scan is executed.
107
+ # (A good place to print a final summary.)
108
+ def scanning_finished
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,67 @@
1
+ module RR
2
+ # Manages scan report printers. Scan report printers implement functionality
3
+ # to report the row differences identified during a scan.
4
+ # *All* table scans are processed by the *same* printer instance.
5
+ #
6
+ # Scan report printers need to register themselves and their command line options
7
+ # with #register.
8
+ #
9
+ # A scan report printer needs to implement at the minimum the following
10
+ # functionality:
11
+ #
12
+ # # Creation of a new ScanReportPrinter.
13
+ # # * +session+: the current Session object
14
+ # # * +arg+: command line argument as yielded by OptionParser#on.
15
+ # def initialize(arg)
16
+ #
17
+ # # A scan of the given 'left' table and corresponding 'right' table is executed.
18
+ # # Needs to yield so that the actual scan can be executed.
19
+ # def scan(left_table, right_table)
20
+ #
21
+ # # Each difference is handed to the printer as described in the format
22
+ # # as described e. g. in DirectTableScan#run
23
+ # def report_difference(type, row)
24
+ #
25
+ # # Optional method. If a scan report printer has it, it is called after the
26
+ # # last table scan is executed.
27
+ # # (A good place to print a final summary.)
28
+ # def scanning_finished
29
+ #
30
+ module ScanReportPrinters
31
+
32
+ # Array of registered ScanReportPrinters.
33
+ # Each entry is a hash with the following keys and related values:
34
+ # * :+printer_class+: The ScanReportPrinter class.
35
+ # * :+opts+: An array defining the command line options (handed to OptionParter#on).
36
+ def self.printers
37
+ @@report_printers ||= []
38
+ end
39
+
40
+ # Needs to be called by ScanReportPrinters to register themselves (+printer+)
41
+ # and their command line options.
42
+ # * :+printer_class+ is the ScanReportPrinter class,
43
+ # * :+opts+ is an array defining the command line options (handed to OptionParter#on).
44
+ def self.register(printer_class, *opts)
45
+ printers << {
46
+ :printer_class => printer_class,
47
+ :opts => opts
48
+ }
49
+ end
50
+
51
+ # Registers all report printer command line options into the given
52
+ # OptionParser.
53
+ # Once the command line is parsed with OptionParser#parse! it will
54
+ # yield the printer class and the optional command line parameter.
55
+ #
56
+ # Note:
57
+ # If multiple printers are specified in the command line, all are created
58
+ # and yielded.
59
+ def self.on_printer_selection(opts)
60
+ printers.each do |printer|
61
+ opts.on(*printer[:opts]) do |arg|
62
+ yield printer[:printer_class], arg
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end