andyjeffries-rubyrep 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +83 -0
- data/License.txt +20 -0
- data/Manifest.txt +151 -0
- data/README.txt +37 -0
- data/bin/rubyrep +8 -0
- data/lib/rubyrep.rb +72 -0
- data/lib/rubyrep/base_runner.rb +195 -0
- data/lib/rubyrep/command_runner.rb +144 -0
- data/lib/rubyrep/committers/buffered_committer.rb +151 -0
- data/lib/rubyrep/committers/committers.rb +152 -0
- data/lib/rubyrep/configuration.rb +275 -0
- data/lib/rubyrep/connection_extenders/connection_extenders.rb +165 -0
- data/lib/rubyrep/connection_extenders/jdbc_extender.rb +65 -0
- data/lib/rubyrep/connection_extenders/mysql_extender.rb +59 -0
- data/lib/rubyrep/connection_extenders/postgresql_extender.rb +277 -0
- data/lib/rubyrep/database_proxy.rb +52 -0
- data/lib/rubyrep/direct_table_scan.rb +75 -0
- data/lib/rubyrep/generate_runner.rb +105 -0
- data/lib/rubyrep/initializer.rb +39 -0
- data/lib/rubyrep/log_helper.rb +30 -0
- data/lib/rubyrep/logged_change.rb +160 -0
- data/lib/rubyrep/logged_change_loader.rb +197 -0
- data/lib/rubyrep/noisy_connection.rb +80 -0
- data/lib/rubyrep/proxied_table_scan.rb +171 -0
- data/lib/rubyrep/proxy_block_cursor.rb +145 -0
- data/lib/rubyrep/proxy_connection.rb +431 -0
- data/lib/rubyrep/proxy_cursor.rb +44 -0
- data/lib/rubyrep/proxy_row_cursor.rb +43 -0
- data/lib/rubyrep/proxy_runner.rb +89 -0
- data/lib/rubyrep/replication_difference.rb +100 -0
- data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
- data/lib/rubyrep/replication_extenders/postgresql_replication.rb +236 -0
- data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
- data/lib/rubyrep/replication_helper.rb +142 -0
- data/lib/rubyrep/replication_initializer.rb +327 -0
- data/lib/rubyrep/replication_run.rb +142 -0
- data/lib/rubyrep/replication_runner.rb +166 -0
- data/lib/rubyrep/replicators/replicators.rb +42 -0
- data/lib/rubyrep/replicators/two_way_replicator.rb +361 -0
- data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
- data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
- data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
- data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
- data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
- data/lib/rubyrep/scan_runner.rb +25 -0
- data/lib/rubyrep/session.rb +230 -0
- data/lib/rubyrep/sync_helper.rb +121 -0
- data/lib/rubyrep/sync_runner.rb +31 -0
- data/lib/rubyrep/syncers/syncers.rb +112 -0
- data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
- data/lib/rubyrep/table_scan.rb +54 -0
- data/lib/rubyrep/table_scan_helper.rb +46 -0
- data/lib/rubyrep/table_sorter.rb +70 -0
- data/lib/rubyrep/table_spec_resolver.rb +142 -0
- data/lib/rubyrep/table_sync.rb +90 -0
- data/lib/rubyrep/task_sweeper.rb +77 -0
- data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
- data/lib/rubyrep/type_casting_cursor.rb +31 -0
- data/lib/rubyrep/uninstall_runner.rb +93 -0
- data/lib/rubyrep/version.rb +9 -0
- data/rubyrep +8 -0
- data/rubyrep.bat +4 -0
- data/setup.rb +1585 -0
- data/spec/base_runner_spec.rb +218 -0
- data/spec/buffered_committer_spec.rb +274 -0
- data/spec/command_runner_spec.rb +145 -0
- data/spec/committers_spec.rb +178 -0
- data/spec/configuration_spec.rb +203 -0
- data/spec/connection_extender_interface_spec.rb +141 -0
- data/spec/connection_extenders_registration_spec.rb +164 -0
- data/spec/database_proxy_spec.rb +48 -0
- data/spec/database_rake_spec.rb +40 -0
- data/spec/db_specific_connection_extenders_spec.rb +34 -0
- data/spec/db_specific_replication_extenders_spec.rb +38 -0
- data/spec/direct_table_scan_spec.rb +61 -0
- data/spec/dolphins.jpg +0 -0
- data/spec/generate_runner_spec.rb +84 -0
- data/spec/initializer_spec.rb +46 -0
- data/spec/log_helper_spec.rb +39 -0
- data/spec/logged_change_loader_spec.rb +68 -0
- data/spec/logged_change_spec.rb +470 -0
- data/spec/noisy_connection_spec.rb +78 -0
- data/spec/postgresql_replication_spec.rb +48 -0
- data/spec/postgresql_schema_support_spec.rb +212 -0
- data/spec/postgresql_support_spec.rb +63 -0
- data/spec/progress_bar_spec.rb +77 -0
- data/spec/proxied_table_scan_spec.rb +151 -0
- data/spec/proxy_block_cursor_spec.rb +197 -0
- data/spec/proxy_connection_spec.rb +423 -0
- data/spec/proxy_cursor_spec.rb +56 -0
- data/spec/proxy_row_cursor_spec.rb +66 -0
- data/spec/proxy_runner_spec.rb +70 -0
- data/spec/replication_difference_spec.rb +161 -0
- data/spec/replication_extender_interface_spec.rb +367 -0
- data/spec/replication_extenders_spec.rb +32 -0
- data/spec/replication_helper_spec.rb +178 -0
- data/spec/replication_initializer_spec.rb +509 -0
- data/spec/replication_run_spec.rb +443 -0
- data/spec/replication_runner_spec.rb +254 -0
- data/spec/replicators_spec.rb +36 -0
- data/spec/rubyrep_spec.rb +8 -0
- data/spec/scan_detail_reporter_spec.rb +119 -0
- data/spec/scan_progress_printers_spec.rb +68 -0
- data/spec/scan_report_printers_spec.rb +67 -0
- data/spec/scan_runner_spec.rb +50 -0
- data/spec/scan_summary_reporter_spec.rb +61 -0
- data/spec/session_spec.rb +253 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +305 -0
- data/spec/strange_name_support_spec.rb +135 -0
- data/spec/sync_helper_spec.rb +169 -0
- data/spec/sync_runner_spec.rb +78 -0
- data/spec/syncers_spec.rb +171 -0
- data/spec/table_scan_helper_spec.rb +36 -0
- data/spec/table_scan_spec.rb +49 -0
- data/spec/table_sorter_spec.rb +30 -0
- data/spec/table_spec_resolver_spec.rb +111 -0
- data/spec/table_sync_spec.rb +140 -0
- data/spec/task_sweeper_spec.rb +47 -0
- data/spec/trigger_mode_switcher_spec.rb +83 -0
- data/spec/two_way_replicator_spec.rb +721 -0
- data/spec/two_way_syncer_spec.rb +256 -0
- data/spec/type_casting_cursor_spec.rb +50 -0
- data/spec/uninstall_runner_spec.rb +93 -0
- metadata +190 -0
|
@@ -0,0 +1,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
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module RR::ScanReportPrinters
|
|
2
|
+
# A ScanReportPrinter producing a summary (number of differences) only.
|
|
3
|
+
class ScanSummaryReporter
|
|
4
|
+
|
|
5
|
+
# Register ScanSummaryReporter with the given command line options.
|
|
6
|
+
# (Command line format as specified by OptionParser#on.)
|
|
7
|
+
RR::ScanReportPrinters.register self, "-s", "--summary[=detailed]",
|
|
8
|
+
"Print the number of differences of each table. Either totals only, e. g.",
|
|
9
|
+
" left_table / right_table [differences]",
|
|
10
|
+
"or a detailed split by type, e. g.",
|
|
11
|
+
" left_table / right_table [conflicts] [left_only records] [right_only records]"
|
|
12
|
+
|
|
13
|
+
# Set to true if only the total number of differences should be reported
|
|
14
|
+
attr_accessor :only_totals
|
|
15
|
+
|
|
16
|
+
# Name of the left table of the current scan
|
|
17
|
+
attr_accessor :left_table
|
|
18
|
+
|
|
19
|
+
# Name of the right table of the current scan
|
|
20
|
+
attr_accessor :right_table
|
|
21
|
+
|
|
22
|
+
# Hold the result of the current scan. A hash with a running count of
|
|
23
|
+
# +:conflict+, +:left+ (only) or +:right+ (only) records.
|
|
24
|
+
attr_accessor :scan_result
|
|
25
|
+
|
|
26
|
+
# A scan run is to be started using this scan result printer.
|
|
27
|
+
# +arg+ is the command line argument as yielded by OptionParser#on.
|
|
28
|
+
def initialize(_, arg)
|
|
29
|
+
self.only_totals = (arg != 'detailed')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A scan of the given 'left' table and corresponding 'right' table is executed.
|
|
33
|
+
# Needs to yield so that the actual scan can be executed.
|
|
34
|
+
def scan(left_table, right_table)
|
|
35
|
+
self.left_table = left_table
|
|
36
|
+
self.right_table = right_table
|
|
37
|
+
self.scan_result = {:conflict => 0, :left => 0, :right => 0}
|
|
38
|
+
|
|
39
|
+
header = left_table.clone
|
|
40
|
+
header << " / " << right_table if left_table != right_table
|
|
41
|
+
$stdout.write "#{header.rjust(36)} "
|
|
42
|
+
|
|
43
|
+
yield # Give control back so that the actual table scan can be done.
|
|
44
|
+
|
|
45
|
+
if only_totals
|
|
46
|
+
$stdout.write \
|
|
47
|
+
"#{rjust_value(scan_result[:conflict] + scan_result[:left] + scan_result[:right])}"
|
|
48
|
+
else
|
|
49
|
+
$stdout.write \
|
|
50
|
+
"#{rjust_value(scan_result[:conflict])} " +
|
|
51
|
+
"#{rjust_value(scan_result[:left])} " +
|
|
52
|
+
"#{rjust_value(scan_result[:right])}"
|
|
53
|
+
end
|
|
54
|
+
$stdout.puts
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Right adjusts the given number and returns according string.
|
|
58
|
+
def rjust_value(value)
|
|
59
|
+
value.to_s.rjust(3)
|
|
60
|
+
end
|
|
61
|
+
private :rjust_value
|
|
62
|
+
|
|
63
|
+
# Each difference is handed to the printer as described in the format
|
|
64
|
+
# as described e. g. in DirectTableScan#run
|
|
65
|
+
def report_difference(type, row)
|
|
66
|
+
scan_result[type] += 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Optional method. If a scan report printer has it, it is called after the
|
|
70
|
+
# last table scan is executed.
|
|
71
|
+
# (A good place to print a final summary.)
|
|
72
|
+
def scanning_finished
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
|
2
|
+
|
|
3
|
+
module RR
|
|
4
|
+
# This class implements the functionality of the rrscan.rb command.
|
|
5
|
+
class ScanRunner < BaseRunner
|
|
6
|
+
|
|
7
|
+
CommandRunner.register 'scan' => {
|
|
8
|
+
:command => self,
|
|
9
|
+
:description => 'Scans for differing records between databases'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Returns summary description string for the scan command.
|
|
13
|
+
def summary_description
|
|
14
|
+
"Scans for differences of the specified tables between both databases."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Creates the correct scan class.
|
|
18
|
+
# Parameters as defined under BaseRunner#create_processor
|
|
19
|
+
def create_processor(left_table, right_table)
|
|
20
|
+
TableScanHelper.scan_class(session).new session, left_table, right_table
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
require 'drb'
|
|
2
|
+
|
|
3
|
+
module RR
|
|
4
|
+
|
|
5
|
+
# This class represents a rubyrep session.
|
|
6
|
+
# Creates and holds expensive objects like e. g. database connections.
|
|
7
|
+
class Session
|
|
8
|
+
|
|
9
|
+
# The Configuration object provided to the initializer
|
|
10
|
+
attr_accessor :configuration
|
|
11
|
+
|
|
12
|
+
# Returns the "left" ActiveRecord / proxy database connection
|
|
13
|
+
def left
|
|
14
|
+
@connections[:left]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Stores the "left" ActiveRecord /proxy database connection
|
|
18
|
+
def left=(connection)
|
|
19
|
+
@connections[:left] = connection
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns the "right" ActiveRecord / proxy database connection
|
|
23
|
+
def right
|
|
24
|
+
@connections[:right]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Stores the "right" ActiveRecord / proxy database connection
|
|
28
|
+
def right=(connection)
|
|
29
|
+
@connections[:right] = connection
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Hash to hold under either :left or :right the according Drb / direct DatabaseProxy
|
|
33
|
+
attr_accessor :proxies
|
|
34
|
+
|
|
35
|
+
# Creates a hash of manual primary key names as can be specified with the
|
|
36
|
+
# Configuration options :+primary_key_names+ or :+auto_key_limit+.
|
|
37
|
+
# * +db_arm: should be either :left or :right
|
|
38
|
+
#
|
|
39
|
+
# Returns the identified manual primary keys. This is a hash with
|
|
40
|
+
# * key: table_name
|
|
41
|
+
# * value: array of primary key names
|
|
42
|
+
def manual_primary_keys(db_arm)
|
|
43
|
+
manual_primary_keys = {}
|
|
44
|
+
resolver = TableSpecResolver.new self
|
|
45
|
+
table_pairs = resolver.resolve configuration.included_table_specs, [], false
|
|
46
|
+
table_pairs.each do |table_pair|
|
|
47
|
+
options = configuration.options_for_table(table_pair[:left])
|
|
48
|
+
key_names = options[:key]
|
|
49
|
+
if key_names == nil and options[:auto_key_limit] > 0
|
|
50
|
+
if left.primary_key_names(table_pair[:left], :raw => true).empty?
|
|
51
|
+
column_names = left.column_names(table_pair[:left])
|
|
52
|
+
if column_names.size <= options[:auto_key_limit]
|
|
53
|
+
key_names = column_names
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
if key_names
|
|
58
|
+
table_name = table_pair[db_arm]
|
|
59
|
+
manual_primary_keys[table_name] = [key_names].flatten
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
manual_primary_keys
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns the corresponding table in the other database.
|
|
66
|
+
# * +db_arm+: database of the given table (either :+left+ or :+right+)
|
|
67
|
+
# * +table+: name of the table
|
|
68
|
+
#
|
|
69
|
+
# If no corresponding table can be found, return the given table.
|
|
70
|
+
# Rationale:
|
|
71
|
+
# Support the case where a table was dropped from the configuration but
|
|
72
|
+
# there were still some unreplicated changes left.
|
|
73
|
+
def corresponding_table(db_arm, table)
|
|
74
|
+
unless @table_map
|
|
75
|
+
@table_map = {:left => {}, :right => {}}
|
|
76
|
+
resolver = TableSpecResolver.new self
|
|
77
|
+
table_pairs = resolver.resolve configuration.included_table_specs, [], false
|
|
78
|
+
table_pairs.each do |table_pair|
|
|
79
|
+
@table_map[:left][table_pair[:left]] = table_pair[:right]
|
|
80
|
+
@table_map[:right][table_pair[:right]] = table_pair[:left]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
@table_map[db_arm][table] || table
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns +true+ if proxy connections are used
|
|
87
|
+
def proxied?
|
|
88
|
+
[configuration.left, configuration.right].any? \
|
|
89
|
+
{|arm_config| arm_config.include? :proxy_host}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns an array of table pairs of the configured tables.
|
|
93
|
+
# Refer to TableSpecResolver#resolve for a detailed description of the
|
|
94
|
+
# return value.
|
|
95
|
+
# If +included_table_specs+ is provided (that is: not an empty array), it
|
|
96
|
+
# will be used instead of the configured table specs.
|
|
97
|
+
def configured_table_pairs(included_table_specs = [])
|
|
98
|
+
resolver = TableSpecResolver.new self
|
|
99
|
+
included_table_specs = configuration.included_table_specs if included_table_specs.empty?
|
|
100
|
+
resolver.resolve included_table_specs, configuration.excluded_table_specs
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Orders the array of table pairs as per primary key / foreign key relations
|
|
104
|
+
# of the tables. Returns the result.
|
|
105
|
+
# Only sorts if the configuration has set option :+table_ordering+.
|
|
106
|
+
# Refer to TableSpecResolver#resolve for a detailed description of the
|
|
107
|
+
# parameter and return value.
|
|
108
|
+
def sort_table_pairs(table_pairs)
|
|
109
|
+
if configuration.options[:table_ordering]
|
|
110
|
+
left_tables = table_pairs.map {|table_pair| table_pair[:left]}
|
|
111
|
+
sorted_left_tables = TableSorter.new(self, left_tables).sort
|
|
112
|
+
sorted_left_tables.map do |left_table|
|
|
113
|
+
table_pairs.find do |table_pair|
|
|
114
|
+
table_pair[:left] == left_table
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
table_pairs
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Returns +true+ if the specified database connection is not alive.
|
|
123
|
+
# * +database+: target database (either +:left+ or :+right+)
|
|
124
|
+
def database_unreachable?(database)
|
|
125
|
+
unreachable = true
|
|
126
|
+
Thread.new do
|
|
127
|
+
begin
|
|
128
|
+
if send(database) && send(database).select_one("select 1+1 as x")['x'].to_i == 2
|
|
129
|
+
unreachable = false # database is actually reachable
|
|
130
|
+
end
|
|
131
|
+
end rescue nil
|
|
132
|
+
end.join configuration.options[:database_connection_timeout]
|
|
133
|
+
unreachable
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Disconnects both database connections
|
|
137
|
+
def disconnect_databases
|
|
138
|
+
[:left, :right].each do |database|
|
|
139
|
+
disconnect_database(database)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Disconnnects the specified database
|
|
144
|
+
# * +database+: the target database (either :+left+ or :+right+)
|
|
145
|
+
def disconnect_database(database)
|
|
146
|
+
proxy, connection = @proxies[database], @connections[database]
|
|
147
|
+
@proxies[database] = nil
|
|
148
|
+
@connections[database] = nil
|
|
149
|
+
if proxy
|
|
150
|
+
proxy.destroy_session(connection)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Refreshes both database connections
|
|
155
|
+
# * +options+: A options hash with the following settings
|
|
156
|
+
# * :+forced+: if +true+, always establish a new database connection
|
|
157
|
+
def refresh(options = {})
|
|
158
|
+
[:left, :right].each {|database| refresh_database_connection database, options}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Refreshes the specified database connection.
|
|
162
|
+
# (I. e. reestablish if not active anymore.)
|
|
163
|
+
# * +database+: target database (either :+left+ or :+right+)
|
|
164
|
+
# * +options+: A options hash with the following settings
|
|
165
|
+
# * :+forced+: if +true+, always establish a new database connection
|
|
166
|
+
def refresh_database_connection(database, options)
|
|
167
|
+
if options[:forced] or database_unreachable?(database)
|
|
168
|
+
# step 1: disconnect both database connection (if still possible)
|
|
169
|
+
begin
|
|
170
|
+
Thread.new do
|
|
171
|
+
disconnect_database database rescue nil
|
|
172
|
+
end.join configuration.options[:database_connection_timeout]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
connect_exception = nil
|
|
176
|
+
# step 2: try to reconnect the database
|
|
177
|
+
Thread.new do
|
|
178
|
+
begin
|
|
179
|
+
connect_database database
|
|
180
|
+
rescue Exception => e
|
|
181
|
+
# save exception so it can be rethrown outside of the thread
|
|
182
|
+
connect_exception = e
|
|
183
|
+
end
|
|
184
|
+
end.join configuration.options[:database_connection_timeout]
|
|
185
|
+
raise connect_exception if connect_exception
|
|
186
|
+
|
|
187
|
+
# step 3: verify if database connections actually work (to detect silent connection failures)
|
|
188
|
+
if database_unreachable?(database)
|
|
189
|
+
raise "no connection to '#{database}' database"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Set up the (proxied or direct) database connections to the specified
|
|
195
|
+
# database.
|
|
196
|
+
# * +database+: the target database (either :+left+ or :+right+)
|
|
197
|
+
def connect_database(database)
|
|
198
|
+
if configuration.left == configuration.right and database == :right
|
|
199
|
+
# If both database configurations point to the same database
|
|
200
|
+
# then don't create the database connection twice.
|
|
201
|
+
# Assumes that the left database is always connected before the right one.
|
|
202
|
+
self.right = self.left
|
|
203
|
+
else
|
|
204
|
+
# Connect the database / proxy
|
|
205
|
+
arm_config = configuration.send database
|
|
206
|
+
if arm_config.include? :proxy_host
|
|
207
|
+
drb_url = "druby://#{arm_config[:proxy_host]}:#{arm_config[:proxy_port]}"
|
|
208
|
+
@proxies[database] = DRbObject.new nil, drb_url
|
|
209
|
+
else
|
|
210
|
+
# Create fake proxy
|
|
211
|
+
@proxies[database] = DatabaseProxy.new
|
|
212
|
+
end
|
|
213
|
+
@connections[database] = @proxies[database].create_session arm_config
|
|
214
|
+
|
|
215
|
+
send(database).manual_primary_keys = manual_primary_keys(database)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Creates a new rubyrep session with the provided Configuration
|
|
220
|
+
def initialize(config = Initializer::configuration)
|
|
221
|
+
@connections = {:left => nil, :right => nil}
|
|
222
|
+
@proxies = {:left => nil, :right => nil}
|
|
223
|
+
|
|
224
|
+
# Keep the database configuration for future reference
|
|
225
|
+
self.configuration = config
|
|
226
|
+
|
|
227
|
+
refresh
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|