rubyrep 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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