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.
- data/History.txt +4 -0
- data/License.txt +20 -0
- data/Manifest.txt +137 -0
- data/README.txt +37 -0
- data/Rakefile +30 -0
- data/bin/rubyrep +8 -0
- data/config/hoe.rb +72 -0
- data/config/mysql_config.rb +25 -0
- data/config/postgres_config.rb +21 -0
- data/config/proxied_test_config.rb +14 -0
- data/config/redmine_config.rb +17 -0
- data/config/rep_config.rb +20 -0
- data/config/requirements.rb +32 -0
- data/config/test_config.rb +20 -0
- data/lib/rubyrep/base_runner.rb +195 -0
- data/lib/rubyrep/command_runner.rb +144 -0
- data/lib/rubyrep/committers/buffered_committer.rb +140 -0
- data/lib/rubyrep/committers/committers.rb +146 -0
- data/lib/rubyrep/configuration.rb +240 -0
- data/lib/rubyrep/connection_extenders/connection_extenders.rb +133 -0
- data/lib/rubyrep/connection_extenders/jdbc_extender.rb +284 -0
- data/lib/rubyrep/connection_extenders/mysql_extender.rb +168 -0
- data/lib/rubyrep/connection_extenders/postgresql_extender.rb +261 -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/logged_change.rb +326 -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 +318 -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 +91 -0
- data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
- data/lib/rubyrep/replication_extenders/postgresql_replication.rb +204 -0
- data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
- data/lib/rubyrep/replication_helper.rb +104 -0
- data/lib/rubyrep/replication_initializer.rb +307 -0
- data/lib/rubyrep/replication_run.rb +48 -0
- data/lib/rubyrep/replication_runner.rb +138 -0
- data/lib/rubyrep/replicators/replicators.rb +37 -0
- data/lib/rubyrep/replicators/two_way_replicator.rb +334 -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 +177 -0
- data/lib/rubyrep/sync_helper.rb +111 -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 +38 -0
- data/lib/rubyrep/table_sorter.rb +70 -0
- data/lib/rubyrep/table_spec_resolver.rb +136 -0
- data/lib/rubyrep/table_sync.rb +68 -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 +92 -0
- data/lib/rubyrep/version.rb +9 -0
- data/lib/rubyrep.rb +68 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/txt2html +74 -0
- data/setup.rb +1585 -0
- data/sims/performance/big_rep_spec.rb +100 -0
- data/sims/performance/big_scan_spec.rb +57 -0
- data/sims/performance/big_sync_spec.rb +141 -0
- data/sims/performance/performance.rake +228 -0
- data/sims/sim_helper.rb +24 -0
- data/spec/base_runner_spec.rb +218 -0
- data/spec/buffered_committer_spec.rb +271 -0
- data/spec/command_runner_spec.rb +145 -0
- data/spec/committers_spec.rb +174 -0
- data/spec/configuration_spec.rb +198 -0
- data/spec/connection_extender_interface_spec.rb +138 -0
- data/spec/connection_extenders_registration_spec.rb +129 -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/generate_runner_spec.rb +84 -0
- data/spec/initializer_spec.rb +46 -0
- data/spec/logged_change_spec.rb +480 -0
- data/spec/postgresql_replication_spec.rb +48 -0
- data/spec/postgresql_support_spec.rb +57 -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 +399 -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 +160 -0
- data/spec/replication_extender_interface_spec.rb +365 -0
- data/spec/replication_extenders_spec.rb +32 -0
- data/spec/replication_helper_spec.rb +121 -0
- data/spec/replication_initializer_spec.rb +477 -0
- data/spec/replication_run_spec.rb +166 -0
- data/spec/replication_runner_spec.rb +213 -0
- data/spec/replicators_spec.rb +31 -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 +212 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +295 -0
- data/spec/sync_helper_spec.rb +157 -0
- data/spec/sync_runner_spec.rb +78 -0
- data/spec/syncers_spec.rb +171 -0
- data/spec/table_scan_helper_spec.rb +29 -0
- data/spec/table_scan_spec.rb +49 -0
- data/spec/table_sorter_spec.rb +31 -0
- data/spec/table_spec_resolver_spec.rb +102 -0
- data/spec/table_sync_spec.rb +84 -0
- data/spec/trigger_mode_switcher_spec.rb +83 -0
- data/spec/two_way_replicator_spec.rb +551 -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 +86 -0
- data/tasks/database.rake +439 -0
- data/tasks/deployment.rake +29 -0
- data/tasks/environment.rake +9 -0
- data/tasks/java.rake +37 -0
- data/tasks/redmine_test.rake +47 -0
- data/tasks/rspec.rake +68 -0
- data/tasks/rubyrep.tailor +18 -0
- data/tasks/stats.rake +19 -0
- data/tasks/task_helper.rb +20 -0
- data.tar.gz.sig +0 -0
- metadata +243 -0
- 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
|