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,46 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
|
|
2
|
+
|
|
3
|
+
require 'rubyrep'
|
|
4
|
+
|
|
5
|
+
module RR
|
|
6
|
+
|
|
7
|
+
# Some helper functions that are of use to all TableScan classes
|
|
8
|
+
module TableScanHelper
|
|
9
|
+
# Compares the primary keys of left_row and right_row to determine their rank.
|
|
10
|
+
# Assumes there is a function primary_key_names returning the array of primary keys
|
|
11
|
+
# that are relevant for this comparison
|
|
12
|
+
#
|
|
13
|
+
# Assumes that at least one of left_row and right_row is not nil
|
|
14
|
+
# A nil row counts as infinite.
|
|
15
|
+
# E. g. left_row is something and right_row is nil ==> left_row is smaller ==> return -1
|
|
16
|
+
def rank_rows(left_row, right_row)
|
|
17
|
+
raise "At least one of left_row and right_row must not be nil!" unless left_row or right_row
|
|
18
|
+
return -1 unless right_row
|
|
19
|
+
return 1 unless left_row
|
|
20
|
+
rank = 0
|
|
21
|
+
primary_key_names.any? do |key|
|
|
22
|
+
if left_row[key].kind_of?(String)
|
|
23
|
+
# When databases order strings, then 'a' < 'A' while for Ruby 'A' < 'a'
|
|
24
|
+
# ==> Use a combination of case sensitive and case insensitive comparing to
|
|
25
|
+
# reproduce the database behaviour.
|
|
26
|
+
rank = left_row[key].casecmp(right_row[key]) # deal with 'a' to 'B' comparisons
|
|
27
|
+
rank = -(left_row[key] <=> right_row[key]) if rank == 0 # deal with 'a' to 'A' comparisons
|
|
28
|
+
else
|
|
29
|
+
rank = left_row[key] <=> right_row[key]
|
|
30
|
+
end
|
|
31
|
+
rank != 0
|
|
32
|
+
end
|
|
33
|
+
rank
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the correct class for the table scan based on the type of the
|
|
37
|
+
# session (proxied or direct).
|
|
38
|
+
def self.scan_class(session)
|
|
39
|
+
if session.proxied?
|
|
40
|
+
ProxiedTableScan
|
|
41
|
+
else
|
|
42
|
+
DirectTableScan
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
|
|
2
|
+
|
|
3
|
+
require 'tsort'
|
|
4
|
+
require 'rubyrep'
|
|
5
|
+
|
|
6
|
+
module RR
|
|
7
|
+
# This class sorts a given list of tables so that tables referencing other
|
|
8
|
+
# tables via foreign keys are placed behind those referenced tables.
|
|
9
|
+
#
|
|
10
|
+
# Rationale:
|
|
11
|
+
# If tables are sorted in that sequence, the risk of foreign key violations is
|
|
12
|
+
# smaller.
|
|
13
|
+
class TableSorter
|
|
14
|
+
include TSort
|
|
15
|
+
|
|
16
|
+
# The active +Session+
|
|
17
|
+
attr_accessor :session
|
|
18
|
+
|
|
19
|
+
# The list of table names to be ordered
|
|
20
|
+
attr_accessor :tables
|
|
21
|
+
|
|
22
|
+
# The table dependencies.
|
|
23
|
+
# Format as described e. g. here: PostgreSQLExtender#referenced_tables
|
|
24
|
+
def referenced_tables
|
|
25
|
+
unless @referenced_tables
|
|
26
|
+
@referenced_tables = session.left.referenced_tables(tables)
|
|
27
|
+
|
|
28
|
+
# Strip away all unrelated tables
|
|
29
|
+
@referenced_tables.each_pair do |table, references|
|
|
30
|
+
references.delete_if do |reference|
|
|
31
|
+
not tables.include? reference
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
@referenced_tables
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Yields each table.
|
|
39
|
+
# For details see standard library: TSort#sort_each_node.
|
|
40
|
+
def tsort_each_node
|
|
41
|
+
referenced_tables.each_key do |table|
|
|
42
|
+
yield table
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Yields all tables that are references by +table+.
|
|
47
|
+
def tsort_each_child(table)
|
|
48
|
+
referenced_tables[table].each do |reference|
|
|
49
|
+
yield reference
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sort
|
|
54
|
+
# Note:
|
|
55
|
+
# We should not use TSort#tsort as this one throws an exception if
|
|
56
|
+
# there are cyclic redundancies.
|
|
57
|
+
# (Our goal is to just get the best ordering that is possible and then
|
|
58
|
+
# take our chances.)
|
|
59
|
+
strongly_connected_components.flatten
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Initializes the TableSorter
|
|
63
|
+
# * session: The active +Session+ instance
|
|
64
|
+
# * tables: an array of table names
|
|
65
|
+
def initialize(session, tables)
|
|
66
|
+
self.session = session
|
|
67
|
+
self.tables = tables
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# Resolves table specifications as provided e. g. in the command line of rrscan
|
|
4
|
+
class TableSpecResolver
|
|
5
|
+
|
|
6
|
+
# The +Session+ instance from which the table specifications are resolved.
|
|
7
|
+
attr_accessor :session
|
|
8
|
+
|
|
9
|
+
# Returns the array of tables of the specified database. Caches the table array.
|
|
10
|
+
# * database: either :+left+ or :+right+
|
|
11
|
+
def tables(database)
|
|
12
|
+
@table_cache ||= {}
|
|
13
|
+
unless @table_cache[database]
|
|
14
|
+
@table_cache[database] = session.send(database).tables
|
|
15
|
+
end
|
|
16
|
+
@table_cache[database]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Creates a resolver that works based on the given +Session+ instance.
|
|
20
|
+
def initialize(session)
|
|
21
|
+
self.session = session
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns all those tables from the given table_pairs that do not exist.
|
|
25
|
+
# * +table_pairs+: same as described at #table_pairs_without_excluded
|
|
26
|
+
#
|
|
27
|
+
# Returns:
|
|
28
|
+
# A hash with keys :+left+ and +:right+, with the value for each key being
|
|
29
|
+
# an array of non-existing tables for the according database.
|
|
30
|
+
# The keys only exist if there are according missing tables.
|
|
31
|
+
def non_existing_tables(table_pairs)
|
|
32
|
+
[:left, :right].inject({}) do |memo, database|
|
|
33
|
+
found_tables = table_pairs.inject([]) do |phantom_tables, table_pair|
|
|
34
|
+
phantom_tables << table_pair[database] unless tables(database).include?(table_pair[database])
|
|
35
|
+
phantom_tables
|
|
36
|
+
end
|
|
37
|
+
memo[database] = found_tables unless found_tables.empty?
|
|
38
|
+
memo
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Resolves the given array of table specificifications.
|
|
43
|
+
# Table specifications are either
|
|
44
|
+
# * strings as produced by BaseRunner#get_options or
|
|
45
|
+
# * actual regular expressions
|
|
46
|
+
# If +excluded_table_specs+ is provided, removes all tables that match it
|
|
47
|
+
# (even if otherwise matching +included_table_specs+).
|
|
48
|
+
#
|
|
49
|
+
# If +verify+ is +true+, raises an exception if any non-existing tables are
|
|
50
|
+
# specified.
|
|
51
|
+
#
|
|
52
|
+
# Returns an array of table name pairs in Hash form.
|
|
53
|
+
# For example something like
|
|
54
|
+
# [{:left => 'my_table', :right => 'my_table_backup'}]
|
|
55
|
+
#
|
|
56
|
+
# Takes care that a table is only returned once.
|
|
57
|
+
def resolve(included_table_specs, excluded_table_specs = [], verify = true)
|
|
58
|
+
table_pairs = expand_table_specs(included_table_specs, verify)
|
|
59
|
+
table_pairs = table_pairs_without_duplicates(table_pairs)
|
|
60
|
+
table_pairs = table_pairs_without_excluded(table_pairs, excluded_table_specs)
|
|
61
|
+
|
|
62
|
+
if verify
|
|
63
|
+
non_existing_tables = non_existing_tables(table_pairs)
|
|
64
|
+
unless non_existing_tables.empty?
|
|
65
|
+
raise "non-existing tables specified: #{non_existing_tables.inspect}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
table_pairs
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Helper for #resolve
|
|
73
|
+
# Expands table specifications into table pairs.
|
|
74
|
+
# Parameters:
|
|
75
|
+
# * +table_specs+:
|
|
76
|
+
# An array of table specifications as described under #resolve.
|
|
77
|
+
# * +verify+:
|
|
78
|
+
# If +true+, table specs in regexp format only resolve if the table exists
|
|
79
|
+
# in left and right database.
|
|
80
|
+
# Return value: refer to #resolve for a detailed description
|
|
81
|
+
def expand_table_specs(table_specs, verify)
|
|
82
|
+
table_pairs = []
|
|
83
|
+
table_specs.each do |table_spec|
|
|
84
|
+
|
|
85
|
+
# If it is a regexp, convert it in an according string
|
|
86
|
+
table_spec = table_spec.inspect if table_spec.kind_of? Regexp
|
|
87
|
+
|
|
88
|
+
case table_spec
|
|
89
|
+
when /^\/.*\/$/ # matches e. g. '/^user/'
|
|
90
|
+
table_spec = table_spec.sub(/^\/(.*)\/$/,'\1') # remove leading and trailing slash
|
|
91
|
+
matching_tables = tables(:left).grep(Regexp.new(table_spec, Regexp::IGNORECASE))
|
|
92
|
+
matching_tables.each do |table|
|
|
93
|
+
if !verify or tables(:right).include? table
|
|
94
|
+
table_pairs << {:left => table, :right => table}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
when /.+,.+/ # matches e. g. 'users,users_backup'
|
|
98
|
+
pair = table_spec.match(/(.*),(.*)/)[1..2].map { |str| str.strip }
|
|
99
|
+
table_pairs << {:left => pair[0], :right => pair[1]}
|
|
100
|
+
else # everything else: just a normal table
|
|
101
|
+
table_pairs << {:left => table_spec.strip, :right => table_spec.strip}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
table_pairs
|
|
105
|
+
end
|
|
106
|
+
private :expand_table_specs
|
|
107
|
+
|
|
108
|
+
# Helper for #resolve
|
|
109
|
+
# Takes given table_pairs and removes all tables that are excluded.
|
|
110
|
+
# Returns the result.
|
|
111
|
+
# Both the given and the returned table_pairs is an array of hashes with
|
|
112
|
+
# * :+left+: name of the left table
|
|
113
|
+
# * :+right+: name of the corresponding right table
|
|
114
|
+
# +excluded_table_specs+ is the array of table specifications to be excluded.
|
|
115
|
+
def table_pairs_without_excluded(table_pairs, excluded_table_specs)
|
|
116
|
+
excluded_tables = expand_table_specs(excluded_table_specs, false).map do |table_pair|
|
|
117
|
+
table_pair[:left]
|
|
118
|
+
end
|
|
119
|
+
table_pairs.select {|table_pair| not excluded_tables.include? table_pair[:left]}
|
|
120
|
+
end
|
|
121
|
+
private :table_pairs_without_excluded
|
|
122
|
+
|
|
123
|
+
# Helper for #resolve
|
|
124
|
+
# Takes given table_pairs and removes all duplicates.
|
|
125
|
+
# Returns the result.
|
|
126
|
+
# Both the given and the returned table_pairs is an array of hashes with
|
|
127
|
+
# * :+left+: name of the left table
|
|
128
|
+
# * :+right+: name of the corresponding right table
|
|
129
|
+
def table_pairs_without_duplicates(table_pairs)
|
|
130
|
+
processed_left_tables = {}
|
|
131
|
+
resulting_table_pairs = []
|
|
132
|
+
table_pairs.each do |table_pair|
|
|
133
|
+
unless processed_left_tables.include? table_pair[:left]
|
|
134
|
+
resulting_table_pairs << table_pair
|
|
135
|
+
processed_left_tables[table_pair[:left]] = true
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
resulting_table_pairs
|
|
139
|
+
end
|
|
140
|
+
private :table_pairs_without_duplicates
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# Synchronizes the data of two tables.
|
|
4
|
+
class TableSync < TableScan
|
|
5
|
+
|
|
6
|
+
# Instance of SyncHelper
|
|
7
|
+
attr_accessor :helper
|
|
8
|
+
|
|
9
|
+
# Returns a hash of sync options for this table sync.
|
|
10
|
+
def sync_options
|
|
11
|
+
@sync_options ||= session.configuration.options_for_table(left_table)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Creates a new TableSync instance
|
|
15
|
+
# * session: a Session object representing the current database session
|
|
16
|
+
# * left_table: name of the table in the left database
|
|
17
|
+
# * right_table: name of the table in the right database. If not given, same like left_table
|
|
18
|
+
def initialize(session, left_table, right_table = nil)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Executes the specified sync hook
|
|
23
|
+
# * +hook_id+: either :+before_table_sync+ or :+after_table_sync+
|
|
24
|
+
def execute_sync_hook(hook_id)
|
|
25
|
+
hook = sync_options[hook_id]
|
|
26
|
+
if hook
|
|
27
|
+
if hook.respond_to?(:call)
|
|
28
|
+
hook.call(helper)
|
|
29
|
+
else
|
|
30
|
+
[:left, :right].each do |database|
|
|
31
|
+
session.send(database).execute hook
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Calls the event filter for the give table difference.
|
|
38
|
+
# * +type+: type of difference
|
|
39
|
+
# * +row+: the differing row
|
|
40
|
+
# Refer to DirectTableScan#run for full description of +type+ and +row+.
|
|
41
|
+
# Returns +true+ if syncing of the difference should *not* proceed.
|
|
42
|
+
def event_filtered?(type, row)
|
|
43
|
+
event_filter = sync_options[:event_filter]
|
|
44
|
+
if event_filter && event_filter.respond_to?(:before_sync)
|
|
45
|
+
not event_filter.before_sync(
|
|
46
|
+
helper.left_table,
|
|
47
|
+
helper.extract_key([row].flatten.first),
|
|
48
|
+
helper,
|
|
49
|
+
type,
|
|
50
|
+
row
|
|
51
|
+
)
|
|
52
|
+
else
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Executes the table sync. If a block is given, yields each difference with
|
|
58
|
+
# the following 2 parameters
|
|
59
|
+
# * +type+
|
|
60
|
+
# * +row+
|
|
61
|
+
# Purpose: enable display of progress information.
|
|
62
|
+
# See DirectTableScan#run for full description of yielded parameters.
|
|
63
|
+
def run
|
|
64
|
+
success = false
|
|
65
|
+
|
|
66
|
+
scan_class = TableScanHelper.scan_class(session)
|
|
67
|
+
scan = scan_class.new(session, left_table, right_table)
|
|
68
|
+
scan.progress_printer = progress_printer
|
|
69
|
+
|
|
70
|
+
self.helper = SyncHelper.new(self)
|
|
71
|
+
syncer = Syncers.configured_syncer(sync_options).new(helper)
|
|
72
|
+
|
|
73
|
+
execute_sync_hook :before_table_sync
|
|
74
|
+
|
|
75
|
+
scan.run do |type, row|
|
|
76
|
+
yield type, row if block_given? # To enable progress reporting
|
|
77
|
+
unless event_filtered?(type, row)
|
|
78
|
+
syncer.sync_difference type, row
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
execute_sync_hook :after_table_sync
|
|
83
|
+
|
|
84
|
+
success = true # considered to be successful if we get till here
|
|
85
|
+
ensure
|
|
86
|
+
helper.finalize success if helper
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# Monitors and cancels stalled tasks
|
|
4
|
+
class TaskSweeper
|
|
5
|
+
|
|
6
|
+
# Executes the give block in a separate Thread.
|
|
7
|
+
# Returns if block is finished or stalled.
|
|
8
|
+
# The block must call regular #ping to announce it is not stalled.
|
|
9
|
+
# * +timeout_period+:
|
|
10
|
+
# Maximum time (in seonds) without ping, after which a task is considered stalled.
|
|
11
|
+
# Returns the created sweeper (allows checking if task was terminated).
|
|
12
|
+
def self.timeout(timeout_period)
|
|
13
|
+
sweeper = TaskSweeper.new(timeout_period)
|
|
14
|
+
sweeper.send(:timeout) {yield sweeper}
|
|
15
|
+
sweeper
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Time in seconds after which a task is considered stalled. Timer is reset
|
|
19
|
+
# by calling #ping.
|
|
20
|
+
attr_accessor :timeout_period
|
|
21
|
+
|
|
22
|
+
# Must be called by the executed task to announce it is still alive
|
|
23
|
+
def ping
|
|
24
|
+
self.last_ping = Time.now
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns +true+ if the task was timed out.
|
|
28
|
+
# The terminated task is expected to free all resources and exit.
|
|
29
|
+
def terminated?
|
|
30
|
+
terminated
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Waits without timeout till the task executing thread is finished
|
|
34
|
+
def join
|
|
35
|
+
thread && thread.join
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Creates a new TaskSweeper
|
|
39
|
+
# * +timeout_period+: timeout value in seconds
|
|
40
|
+
def initialize(timeout_period)
|
|
41
|
+
self.timeout_period = timeout_period
|
|
42
|
+
self.terminated = false
|
|
43
|
+
self.last_ping = Time.now
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
protected
|
|
47
|
+
|
|
48
|
+
# Time of last ping
|
|
49
|
+
attr_accessor :last_ping
|
|
50
|
+
|
|
51
|
+
# Set to +true+ if the executed task has timed out
|
|
52
|
+
attr_accessor :terminated
|
|
53
|
+
|
|
54
|
+
# The task executing thread
|
|
55
|
+
attr_accessor :thread
|
|
56
|
+
|
|
57
|
+
# Executes the given block and times it out if stalled.
|
|
58
|
+
def timeout
|
|
59
|
+
exception = nil
|
|
60
|
+
self.thread = Thread.new do
|
|
61
|
+
begin
|
|
62
|
+
yield
|
|
63
|
+
rescue Exception => e
|
|
64
|
+
# save exception so it can be rethrown outside of the thread
|
|
65
|
+
exception = e
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
while self.thread.join(self.timeout_period) == nil do
|
|
69
|
+
if self.last_ping < Time.now - self.timeout_period
|
|
70
|
+
self.terminated = true
|
|
71
|
+
break
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
raise exception if exception
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
module RR
|
|
4
|
+
|
|
5
|
+
# Switches rubyrep triggers between "exclude rubyrep activity" modes.
|
|
6
|
+
class TriggerModeSwitcher
|
|
7
|
+
|
|
8
|
+
# Keeps track of all the triggers.
|
|
9
|
+
# This is a hash with 2 keys: :+left+ and :+right+.
|
|
10
|
+
# Each of these entries is a Set containing table names.
|
|
11
|
+
def triggers
|
|
12
|
+
@triggers ||= {
|
|
13
|
+
:left => Set.new,
|
|
14
|
+
:right => Set.new
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The active Session
|
|
19
|
+
attr_accessor :session
|
|
20
|
+
|
|
21
|
+
def initialize(session)
|
|
22
|
+
self.session = session
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Does the actual switching of the trigger mode.
|
|
26
|
+
# * +database+: either :+left+ or :+right+
|
|
27
|
+
# * +table+: name of the table
|
|
28
|
+
# * +exclude_rr_activity+: the new trigger mode (either +true+ or +false+)
|
|
29
|
+
def switch_trigger_mode(database, table, exclude_rr_activity)
|
|
30
|
+
options = session.configuration.options
|
|
31
|
+
if session.send(database).replication_trigger_exists? "#{options[:rep_prefix]}_#{table}", table
|
|
32
|
+
params = {
|
|
33
|
+
:trigger_name => "#{options[:rep_prefix]}_#{table}",
|
|
34
|
+
:table => table,
|
|
35
|
+
:keys => session.send(database).primary_key_names(table),
|
|
36
|
+
:log_table => "#{options[:rep_prefix]}_pending_changes",
|
|
37
|
+
:activity_table => "#{options[:rep_prefix]}_running_flags",
|
|
38
|
+
:key_sep => options[:key_sep],
|
|
39
|
+
:exclude_rr_activity => exclude_rr_activity,
|
|
40
|
+
}
|
|
41
|
+
session.send(database).create_or_replace_replication_trigger_function(params)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Switches the trigger of the named table to "exclude rubyrep activity" mode.
|
|
46
|
+
# Only switches if it didn't do so already for the table.
|
|
47
|
+
# * +database+: either :+left+ or :+right+
|
|
48
|
+
# * +table+: name of the table
|
|
49
|
+
def exclude_rr_activity(database, table)
|
|
50
|
+
switch_trigger_mode(database, table, true) if triggers[database].add? table
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Restores all switched triggers to not exclude rubyrep activity
|
|
54
|
+
def restore_triggers
|
|
55
|
+
[:left, :right].each do |database|
|
|
56
|
+
triggers[database].each do |table|
|
|
57
|
+
switch_trigger_mode database, table, false
|
|
58
|
+
end
|
|
59
|
+
triggers[database].clear
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|