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,144 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module RR
|
|
6
|
+
|
|
7
|
+
# This class implements the functionality to dispatch rubyrep commands.
|
|
8
|
+
class CommandRunner
|
|
9
|
+
|
|
10
|
+
# Returns a hash of all commands registered with #register.
|
|
11
|
+
def self.commands
|
|
12
|
+
@commands ||= {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Registers one or multiple commands.
|
|
16
|
+
# +commands+ is a hash with
|
|
17
|
+
# * key: name of the command
|
|
18
|
+
# * value: a command hash defining the command
|
|
19
|
+
#
|
|
20
|
+
# A command hash consists of
|
|
21
|
+
# * :+description+: short description of the command
|
|
22
|
+
# * :+command+: an object / class implementing the hash.
|
|
23
|
+
# Must have a method
|
|
24
|
+
#
|
|
25
|
+
# # runs a command
|
|
26
|
+
# # * +args+: array of command line parameters
|
|
27
|
+
# # note: will not contain the command name itself.
|
|
28
|
+
# def run(args)
|
|
29
|
+
def self.register(commands)
|
|
30
|
+
self.commands.merge!(commands)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Prints the version to stderr
|
|
34
|
+
def self.show_version
|
|
35
|
+
$stdout.puts "rubyrep version #{RR::VERSION::STRING}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Dispatches commands as per given command line parameters.
|
|
40
|
+
# * +args+: array of command line parameters
|
|
41
|
+
def self.run(args)
|
|
42
|
+
status = 0
|
|
43
|
+
options = {}
|
|
44
|
+
|
|
45
|
+
parser = OptionParser.new do |opts|
|
|
46
|
+
opts.banner = <<EOS
|
|
47
|
+
Usage: #{$0} [general options] command [parameters, ...]
|
|
48
|
+
|
|
49
|
+
Asynchronous master-master replication of relational databases.
|
|
50
|
+
EOS
|
|
51
|
+
opts.separator ""
|
|
52
|
+
opts.separator "Available options:"
|
|
53
|
+
|
|
54
|
+
opts.on("--verbose", "Show errors with full stack trace") do
|
|
55
|
+
options[:verbose] = true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
opts.on("-v", "--version", "Show version information.") do
|
|
59
|
+
show_version
|
|
60
|
+
options = nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
opts.on_tail("--help", "Show this message") do
|
|
64
|
+
$stderr.puts opts
|
|
65
|
+
|
|
66
|
+
$stderr.puts "\nAvailable commands:"
|
|
67
|
+
commands.sort.each do |command_name, command_hash|
|
|
68
|
+
$stderr.puts " #{command_name.ljust(15)} #{command_hash[:description]}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
options = nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
begin
|
|
76
|
+
|
|
77
|
+
# extract general options
|
|
78
|
+
general_args = []
|
|
79
|
+
until args.empty?
|
|
80
|
+
if args[0] =~ /^-/
|
|
81
|
+
general_args << args.shift
|
|
82
|
+
else
|
|
83
|
+
break
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# parse general options
|
|
88
|
+
parser.parse!(general_args)
|
|
89
|
+
|
|
90
|
+
# process commands
|
|
91
|
+
if options # this will be +nil+ if the --help or --version are specified
|
|
92
|
+
if args.empty?
|
|
93
|
+
$stderr.puts "No command specified.\n\n"
|
|
94
|
+
run(['--help'])
|
|
95
|
+
status = 1
|
|
96
|
+
else
|
|
97
|
+
command = args[0]
|
|
98
|
+
if command == 'help' and args.size == 1
|
|
99
|
+
run(['--help'])
|
|
100
|
+
status = 0
|
|
101
|
+
elsif commands.include? command
|
|
102
|
+
status = commands[command][:command].run(args.slice(1, 1_000_000))
|
|
103
|
+
else
|
|
104
|
+
$stderr.puts "Error: Unknown command specified.\n\n"
|
|
105
|
+
run(['--help'])
|
|
106
|
+
status = 1
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
rescue Exception => e
|
|
111
|
+
$stderr.puts "Exception caught: #{e}"
|
|
112
|
+
$stderr.puts e.backtrace if options && options[:verbose]
|
|
113
|
+
status = 1
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
return status
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Command runner to show help for other commands
|
|
121
|
+
class HelpRunner
|
|
122
|
+
CommandRunner.register 'help' => {
|
|
123
|
+
:command => self,
|
|
124
|
+
:description => "Shows detailed help for the specified command"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Runs the help command
|
|
128
|
+
# * +args+: array of command line parameters
|
|
129
|
+
def self.run(args)
|
|
130
|
+
if args[0] == 'help' or args[0] == '--help'
|
|
131
|
+
$stderr.puts(<<EOS)
|
|
132
|
+
Usage: #{$0} help [command]
|
|
133
|
+
|
|
134
|
+
Shows the help for the specified command.
|
|
135
|
+
EOS
|
|
136
|
+
0
|
|
137
|
+
else
|
|
138
|
+
CommandRunner.run([args[0], '--help'])
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
module Committers
|
|
3
|
+
|
|
4
|
+
# This committer periodically commits transactions. It can be used for
|
|
5
|
+
# pre-replication syncs as it
|
|
6
|
+
# * updates the activity marker table.
|
|
7
|
+
# * switches existing triggers to filter out rubyrep activity
|
|
8
|
+
class BufferedCommitter < DefaultCommitter
|
|
9
|
+
|
|
10
|
+
# Register the committer
|
|
11
|
+
Committers.register :buffered_commit => self
|
|
12
|
+
|
|
13
|
+
# Unless overwritten via configuration, transactions are commited after the
|
|
14
|
+
# given number of record changes
|
|
15
|
+
DEFAULT_COMMIT_FREQUENCY = 1000
|
|
16
|
+
|
|
17
|
+
# Switches the trigger mode of the specified +table+ in the specified
|
|
18
|
+
# +database+ to ignore rubyrep activity.
|
|
19
|
+
def exclude_rr_activity(database, table)
|
|
20
|
+
trigger_mode_switcher.exclude_rr_activity database, table
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns the trigger mode switcher (creates it if necessary)
|
|
24
|
+
def trigger_mode_switcher
|
|
25
|
+
@trigger_mode_switcher ||= TriggerModeSwitcher.new session
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the name of the activity marker table
|
|
29
|
+
def activity_marker_table
|
|
30
|
+
@activity_marker_table ||= "#{session.configuration.options[:rep_prefix]}_running_flags"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns +true+ if the activity marker table should be maintained.
|
|
34
|
+
def maintain_activity_status?
|
|
35
|
+
unless @activity_status_checked
|
|
36
|
+
@activity_status_checked = true
|
|
37
|
+
@maintain_activity_status = session.left.tables.include?(activity_marker_table)
|
|
38
|
+
end
|
|
39
|
+
@maintain_activity_status
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the number of changes, after which the open transactions should
|
|
43
|
+
# be committed and new transactions be started.
|
|
44
|
+
def commit_frequency
|
|
45
|
+
unless @commit_frequency
|
|
46
|
+
@commit_frequency = session.configuration.options[:commit_frequency]
|
|
47
|
+
@commit_frequency ||= DEFAULT_COMMIT_FREQUENCY
|
|
48
|
+
end
|
|
49
|
+
@commit_frequency
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Commits the open transactions in both databases. Before committing,
|
|
53
|
+
# clears the rubyrep activity marker.
|
|
54
|
+
def commit_db_transactions
|
|
55
|
+
[:left, :right].each do |database|
|
|
56
|
+
if maintain_activity_status?
|
|
57
|
+
session.send(database).execute("delete from #{activity_marker_table}")
|
|
58
|
+
end
|
|
59
|
+
session.send(database).commit_db_transaction
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Begins new transactions in both databases. After starting the transaction,
|
|
64
|
+
# marks the activity of rubyrep.
|
|
65
|
+
def begin_db_transactions
|
|
66
|
+
[:left, :right].each do |database|
|
|
67
|
+
session.send(database).begin_db_transaction
|
|
68
|
+
if maintain_activity_status?
|
|
69
|
+
session.send(database).execute("insert into #{activity_marker_table} values(1)")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Rolls back the open transactions in both databases.
|
|
75
|
+
def rollback_db_transactions
|
|
76
|
+
session.left.rollback_db_transaction
|
|
77
|
+
session.right.rollback_db_transaction
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Commits the open tranactions and starts new one if the #commit_frequency
|
|
81
|
+
# number of record changes have been executed.
|
|
82
|
+
def commit
|
|
83
|
+
@change_counter ||= 0
|
|
84
|
+
@change_counter += 1
|
|
85
|
+
if @change_counter == commit_frequency
|
|
86
|
+
@change_counter = 0
|
|
87
|
+
commit_db_transactions
|
|
88
|
+
begin_db_transactions
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A new committer is created for each table sync.
|
|
93
|
+
# * session: a Session object representing the current database session
|
|
94
|
+
def initialize(session)
|
|
95
|
+
super
|
|
96
|
+
begin_db_transactions
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Inserts the specified record in the specified +database+ (either :left or :right).
|
|
100
|
+
# +table+ is the name of the target table.
|
|
101
|
+
# +values+ is a hash of column_name => value pairs.
|
|
102
|
+
def insert_record(database, table, values)
|
|
103
|
+
exclude_rr_activity database, table
|
|
104
|
+
super
|
|
105
|
+
commit
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Updates the specified record in the specified +database+ (either :left or :right).
|
|
109
|
+
# +table+ is the name of the target table.
|
|
110
|
+
# +values+ is a hash of column_name => value pairs.
|
|
111
|
+
# +old_key+ is a column_name => value hash with the original primary key.
|
|
112
|
+
# If +old_key+ is +nil+, then the primary key must be contained in +values+.
|
|
113
|
+
def update_record(database, table, values, old_key = nil)
|
|
114
|
+
exclude_rr_activity database, table
|
|
115
|
+
super
|
|
116
|
+
commit
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Deletes the specified record in the specified +database+ (either :left or :right).
|
|
120
|
+
# +table+ is the name of the target table.
|
|
121
|
+
# +values+ is a hash of column_name => value pairs. (Only the primary key
|
|
122
|
+
# values will be used and must be included in the hash.)
|
|
123
|
+
def delete_record(database, table, values)
|
|
124
|
+
exclude_rr_activity database, table
|
|
125
|
+
super
|
|
126
|
+
commit
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Is called after the last insert / update / delete query.
|
|
130
|
+
# +success+ should be true if there were no problems, false otherwise.
|
|
131
|
+
def finalize(success = true)
|
|
132
|
+
if success
|
|
133
|
+
commit_db_transactions
|
|
134
|
+
else
|
|
135
|
+
rollback_db_transactions
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
# Committers are classes that implement transaction policies.
|
|
3
|
+
# This module provides functionality to register committers and access the
|
|
4
|
+
# list of registered committers.
|
|
5
|
+
# Every Committer needs to register itself with Committers#register.
|
|
6
|
+
# Each Committer must implement at the following methods:
|
|
7
|
+
#
|
|
8
|
+
# # Creates a new committer
|
|
9
|
+
# # * session: a Session object representing the current database session
|
|
10
|
+
# def initialize(session)
|
|
11
|
+
#
|
|
12
|
+
# # Inserts the specified record in the specified +database+ (either :left or :right).
|
|
13
|
+
# # +table+ is the name of the target table.
|
|
14
|
+
# # +values+ is a hash of column_name => value pairs.
|
|
15
|
+
# def insert_record(database, values)
|
|
16
|
+
#
|
|
17
|
+
# # Updates the specified record in the specified +database+ (either :left or :right).
|
|
18
|
+
# # +table+ is the name of the target table.
|
|
19
|
+
# # +values+ is a hash of column_name => value pairs.
|
|
20
|
+
# # +old_key+ is a column_name => value hash with the original primary key.
|
|
21
|
+
# # If +old_key+ is +nil+, then the primary key must be contained in +values+.
|
|
22
|
+
# def update_record(database, values, old_key)
|
|
23
|
+
#
|
|
24
|
+
# # Deletes the specified record in the specified +database+ (either :left or :right).
|
|
25
|
+
# # +table+ is the name of the target table.
|
|
26
|
+
# # +values+ is a hash of column_name => value pairs. (Only the primary key
|
|
27
|
+
# # values will be used and must be included in the hash.)
|
|
28
|
+
# def delete_record(database, values)
|
|
29
|
+
#
|
|
30
|
+
# # Is called after the last insert / update / delete query.
|
|
31
|
+
# def finalize
|
|
32
|
+
#
|
|
33
|
+
module Committers
|
|
34
|
+
# Returns a Hash of currently registered committers.
|
|
35
|
+
# (Empty Hash if no connection committers were defined.)
|
|
36
|
+
def self.committers
|
|
37
|
+
@committers ||= {}
|
|
38
|
+
@committers
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Registers one or multiple committers.
|
|
42
|
+
# committer_hash is a Hash with
|
|
43
|
+
# key:: The adapter symbol as used to reference the committer
|
|
44
|
+
# value:: The class implementing the committer
|
|
45
|
+
def self.register(committer_hash)
|
|
46
|
+
@committers ||= {}
|
|
47
|
+
@committers.merge! committer_hash
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# This committer does not do anything. This means that the default DBMS
|
|
51
|
+
# behaviour is used (for most DBMS: every DML statement (insert, update,
|
|
52
|
+
# delete) runs in it's own transaction.
|
|
53
|
+
class DefaultCommitter
|
|
54
|
+
|
|
55
|
+
# Register the committer
|
|
56
|
+
Committers.register :default => self
|
|
57
|
+
|
|
58
|
+
# The current Session object
|
|
59
|
+
attr_accessor :session
|
|
60
|
+
|
|
61
|
+
# A hash holding the proxy connections
|
|
62
|
+
# E. g. {:left => <left connection>, :right => <right connection>}
|
|
63
|
+
attr_accessor :connections
|
|
64
|
+
|
|
65
|
+
# A new committer is created for each table sync.
|
|
66
|
+
# * session: a Session object representing the current database session
|
|
67
|
+
def initialize(session)
|
|
68
|
+
self.session = session
|
|
69
|
+
self.connections = {:left => session.left, :right => session.right}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Inserts the specified record in the specified +database+ (either :left or :right).
|
|
73
|
+
# +table+ is the name of the target table.
|
|
74
|
+
# +values+ is a hash of column_name => value pairs.
|
|
75
|
+
def insert_record(database, table, values)
|
|
76
|
+
connections[database].insert_record(table, values)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Updates the specified record in the specified +database+ (either :left or :right).
|
|
80
|
+
# +table+ is the name of the target table.
|
|
81
|
+
# +values+ is a hash of column_name => value pairs.
|
|
82
|
+
# # +old_key+ is a column_name => value hash with the original primary key.
|
|
83
|
+
# If +old_key+ is +nil+, then the primary key must be contained in +values+.
|
|
84
|
+
def update_record(database, table, values, old_key = nil)
|
|
85
|
+
connections[database].update_record(table, values, old_key)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Deletes the specified record in the specified +database+ (either :left or :right).
|
|
89
|
+
# +table+ is the name of the target table.
|
|
90
|
+
# +values+ is a hash of column_name => value pairs. (Only the primary key
|
|
91
|
+
# values will be used and must be included in the hash.)
|
|
92
|
+
def delete_record(database, table, values)
|
|
93
|
+
connections[database].delete_record(table, values)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Is called after the last insert / update / delete query.
|
|
97
|
+
# +success+ should be true if there were no problems, false otherwise.
|
|
98
|
+
def finalize(success = true)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Starts a transaction but does never commit it.
|
|
103
|
+
# Useful during testing.
|
|
104
|
+
class NeverCommitter < DefaultCommitter
|
|
105
|
+
Committers.register :never_commit => self
|
|
106
|
+
@@current_session = nil
|
|
107
|
+
|
|
108
|
+
# Returns the last active data session
|
|
109
|
+
def self.current_session
|
|
110
|
+
@@current_session
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Saves the provided database session as class variable.
|
|
114
|
+
# Purpose: the last database session stays available after the
|
|
115
|
+
# NeverCommitter is destroyed so that also later the transaction rollback
|
|
116
|
+
# can still be executed.
|
|
117
|
+
def self.current_session=(session)
|
|
118
|
+
@@current_session = session
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Rolls back transactions of current session (if there is one).
|
|
122
|
+
# This would be called e. g. in rspec's after(:each) to ensure that
|
|
123
|
+
# the next test case finds the original test data.
|
|
124
|
+
def self.rollback_current_session
|
|
125
|
+
if self.current_session
|
|
126
|
+
self.current_session.left.rollback_db_transaction
|
|
127
|
+
self.current_session.right.rollback_db_transaction
|
|
128
|
+
self.current_session = nil
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Refer to DefaultCommitter#initialize for details.
|
|
133
|
+
# Starts new transactions on left and right database connectin of session.
|
|
134
|
+
# Additionally rolls back transactions started in previous
|
|
135
|
+
# +NeverCommitter+ instances.
|
|
136
|
+
def initialize(session)
|
|
137
|
+
super
|
|
138
|
+
self.class.rollback_current_session
|
|
139
|
+
self.class.current_session = session
|
|
140
|
+
session.left.begin_db_transaction
|
|
141
|
+
session.right.begin_db_transaction
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# The Configuration class holds the default configuration options for Rubyrep.
|
|
4
|
+
# Configuration values are changed with the Initializer::run method.
|
|
5
|
+
class Configuration
|
|
6
|
+
# Connection settings for the "left" database.
|
|
7
|
+
# See Configuration#right for details.
|
|
8
|
+
attr_accessor :left
|
|
9
|
+
|
|
10
|
+
# Connection settings for the "right" database.
|
|
11
|
+
# Takes a similar hash as ActiveRecord::Base.establish_connection.
|
|
12
|
+
# Additional settings in case a proxy is used:
|
|
13
|
+
# * +proxy_host+: name or IP address of where the proxy is running
|
|
14
|
+
# * +proxy_port+: port on which the proxy is listening
|
|
15
|
+
attr_accessor :right
|
|
16
|
+
|
|
17
|
+
# Returns true unless running on windows...
|
|
18
|
+
def self.true_if_running_in_a_terminal_and_not_under_windows
|
|
19
|
+
# Not using RUBY_PLATFORM as it should also work under JRuby
|
|
20
|
+
$stdout.tty? and not ENV['OS'] =~ /windows/i
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Default #options for a new Configuration object.
|
|
24
|
+
DEFAULT_OPTIONS = {
|
|
25
|
+
:proxy_block_size => 1000,
|
|
26
|
+
:row_buffer_size => 1000,
|
|
27
|
+
:replicator => :two_way,
|
|
28
|
+
:committer => :buffered_commit,
|
|
29
|
+
:commit_frequency => 1000,
|
|
30
|
+
:table_ordering => true,
|
|
31
|
+
:scan_progress_printer => :progress_bar,
|
|
32
|
+
:use_ansi => true_if_running_in_a_terminal_and_not_under_windows,
|
|
33
|
+
:adjust_sequences => true,
|
|
34
|
+
:sequence_adjustment_buffer => 0,
|
|
35
|
+
:sequence_increment => 2,
|
|
36
|
+
:left_sequence_offset => 0,
|
|
37
|
+
:right_sequence_offset => 1,
|
|
38
|
+
:replication_interval => 1,
|
|
39
|
+
:auto_key_limit => 0,
|
|
40
|
+
|
|
41
|
+
:rep_prefix => 'rr',
|
|
42
|
+
:key_sep => '|',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# General options.
|
|
46
|
+
# Possible settings:
|
|
47
|
+
# * :+proxy_block_size+: The proxy cursor will calculate the checksum for block_size number of records each.
|
|
48
|
+
# * :+row_buffer_size+:
|
|
49
|
+
# The number of rows that is read into memory at once.
|
|
50
|
+
# Only needed for database drivers that don't stream results one-by-one to the client.
|
|
51
|
+
# * :+committer+:
|
|
52
|
+
# A committer key as registered by Committers#register.
|
|
53
|
+
# Determines the transaction management to be used during the sync.
|
|
54
|
+
# * :+commit_frequency+:
|
|
55
|
+
# Used by BufferedCommitter. Number of changes after which the open
|
|
56
|
+
# transactions should be committed and new transactions be started.
|
|
57
|
+
# * :+table_ordering+:
|
|
58
|
+
# If true, sort tables before syncing as per foreign key dependencies.
|
|
59
|
+
# (Dependent tables are synced last to reduce risk of foreign key
|
|
60
|
+
# constraint violations.)
|
|
61
|
+
# * :+scan_progress_printer+:
|
|
62
|
+
# The progress printer key as registered by ScanProgressPrinters#register.
|
|
63
|
+
# Determines how the scan progress is visualized.
|
|
64
|
+
# * :+use_ansi+: Only use ANSI codes for text output if +true+.
|
|
65
|
+
# * :+auto_key_limit+:
|
|
66
|
+
# If a table has no primary keys and no primary keys have been specified
|
|
67
|
+
# manually using the :+primary_key_names+ option, then this option can be
|
|
68
|
+
# activated to simply use all columns of the table as a big combined key.
|
|
69
|
+
# This option specifies up to how many columns a table may have in order
|
|
70
|
+
# to use them as one big, combined primary key.
|
|
71
|
+
# Typical use case: the database has a lot of tables to map many-to-many
|
|
72
|
+
# relationshipts and no combined primary key is set up for them.
|
|
73
|
+
# Sync specific settings
|
|
74
|
+
# * :+before_table_sync+:
|
|
75
|
+
# A hook that is executed before a table sync.
|
|
76
|
+
# Can be either
|
|
77
|
+
# * a String: executed as SQL command on both databases.
|
|
78
|
+
# * a Proc:
|
|
79
|
+
# Called once before the table sync.
|
|
80
|
+
# The Proc is called with one parameter: the current SyncHelper instance.
|
|
81
|
+
# Through the sync helper there is access to the name of the synced table,
|
|
82
|
+
# the current session, etc
|
|
83
|
+
# Example:
|
|
84
|
+
# lambda {|helper| $stderr.puts "Hook called for #{helper.left_table}."}
|
|
85
|
+
# * :+after_table_sync+:
|
|
86
|
+
# Same as :+before_table_sync+ (but called after the sync is completed).
|
|
87
|
+
# * :+syncer+:
|
|
88
|
+
# A syncer key as registered by TableSync#register_syncer.
|
|
89
|
+
# Determines which sync algorithm is used.
|
|
90
|
+
# * further options as defined by each syncer
|
|
91
|
+
# Replication specific settings:
|
|
92
|
+
# * :+rep_prefix+: the prefix that is put in front of all created database objects
|
|
93
|
+
# * :+key_sep+: which string separates columns in the key column of the change log table
|
|
94
|
+
# * :+replicator+:
|
|
95
|
+
# Determines which replicator algorithm to use.
|
|
96
|
+
# For each replicator must also exist a corresponding +:syncer+. (It is
|
|
97
|
+
# used for the initial sync of a table.)
|
|
98
|
+
# If no +:syncer+ option is specified, than a syncer as named by this
|
|
99
|
+
# option is used.
|
|
100
|
+
# * :+adjust_sequences+:
|
|
101
|
+
# If true, adjust sequences to avoid number conflicts between left and
|
|
102
|
+
# right database during replication.
|
|
103
|
+
# * :+sequence_adjustement_buffer+:
|
|
104
|
+
# When updating a sequence, this is the additional gap to avoid sequence
|
|
105
|
+
# conflicts to appear due to concurrent record insertions.
|
|
106
|
+
# * :+sequence_increment+: new sequence value = last sequence value + this
|
|
107
|
+
# * :+left_sequence_offset+, +right_sequence_offset+:
|
|
108
|
+
# Default sequence offset for the table in the according data base.
|
|
109
|
+
# E. g. with a +sequence_increment+ of 2, an offset of 0 will produce even,
|
|
110
|
+
# an offset of 1 will produce odd numbers.
|
|
111
|
+
# * :+replication_interval+: time in seconds between replication runs
|
|
112
|
+
attr_reader :options
|
|
113
|
+
|
|
114
|
+
# Merges the specified +options+ hash into the existing options
|
|
115
|
+
def options=(options)
|
|
116
|
+
@options ||= {}
|
|
117
|
+
@options = @options.merge! options
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Array of table specifications for tables that should be processed
|
|
121
|
+
# Refer to #add_table_options for what constitutes a valid table specification.
|
|
122
|
+
def included_table_specs
|
|
123
|
+
@included_table_specs ||= []
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Array of table specifications for tables that should *not* be processed
|
|
127
|
+
# Refer to #add_table_options for what constitutes a valid table specification.
|
|
128
|
+
def excluded_table_specs
|
|
129
|
+
@excluded_table_specs ||= []
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Ensures that rubyrep infrastructure tables are excluded
|
|
133
|
+
def exclude_rubyrep_tables
|
|
134
|
+
exclude_tables Regexp.new("^#{options[:rep_prefix]}_.*")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# A list of tables having table specific options that should be considered
|
|
138
|
+
# during processing (scanned, synced, ...)
|
|
139
|
+
# +tables_with_options+ is a 2 element array with
|
|
140
|
+
# * first element: A +table_spec+ (either a table name or a regexp matching multiple tables)
|
|
141
|
+
# * second element: The +options+ hash (detailed format described in #add_tables
|
|
142
|
+
# Should only be accessed via #add_table_options and #options_for_table
|
|
143
|
+
def tables_with_options
|
|
144
|
+
@tables_with_options ||= []
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Adds the specified tables to the list of tables that should be processed.
|
|
148
|
+
# If options are provided, store them for future processing.
|
|
149
|
+
# Refer to #add_table_options for detailed description of parameters.
|
|
150
|
+
def include_tables(table_spec, options = nil)
|
|
151
|
+
included_table_specs << table_spec unless included_table_specs.include?(table_spec)
|
|
152
|
+
add_table_options(table_spec, options) if options
|
|
153
|
+
end
|
|
154
|
+
alias_method :include_table, :include_tables
|
|
155
|
+
|
|
156
|
+
# Excludes the specified table from the list of tables that should be
|
|
157
|
+
# processed.
|
|
158
|
+
# Refer to #add_table_options for detailed description of what constitutes a
|
|
159
|
+
# valid table specification.
|
|
160
|
+
def exclude_tables(table_spec)
|
|
161
|
+
excluded_table_specs << table_spec unless excluded_table_specs.include?(table_spec)
|
|
162
|
+
end
|
|
163
|
+
alias_method :exclude_table, :exclude_tables
|
|
164
|
+
|
|
165
|
+
# Adds the specified options for the provided +table_spec+.
|
|
166
|
+
# A +table_spec+ can be either
|
|
167
|
+
# * a table name or
|
|
168
|
+
# * a table pair (e. g. "my_left_table, my_right_table")
|
|
169
|
+
# * a regexp matching multiple tables.
|
|
170
|
+
# +options+ is hash with possible generic values as described under #options.
|
|
171
|
+
# Additional, exclusively table specific options:
|
|
172
|
+
# * :+primary_key_names+: array of primary key names
|
|
173
|
+
def add_table_options(table_spec, options)
|
|
174
|
+
i = nil
|
|
175
|
+
tables_with_options.each_with_index { |table_options, k|
|
|
176
|
+
i = k if table_options[0] == table_spec
|
|
177
|
+
}
|
|
178
|
+
if i
|
|
179
|
+
table_options = tables_with_options[i][1]
|
|
180
|
+
else
|
|
181
|
+
table_options = {}
|
|
182
|
+
tables_with_options << [table_spec, table_options]
|
|
183
|
+
end
|
|
184
|
+
table_options.merge! options
|
|
185
|
+
end
|
|
186
|
+
alias_method :add_table_option, :add_table_options
|
|
187
|
+
|
|
188
|
+
# Yields all table specs that have been set up with the given option
|
|
189
|
+
# * +key+: the option key
|
|
190
|
+
# Yields:
|
|
191
|
+
# * +table_spec+: the table specification of the matching option (or nil if non-table specific setting)
|
|
192
|
+
# * +option_value+: the option value for the specified +key+
|
|
193
|
+
def each_matching_option(key)
|
|
194
|
+
yield nil, options[key] if options.include?(key)
|
|
195
|
+
tables_with_options.each do |table_options|
|
|
196
|
+
yield table_options[0], table_options[1][key] if table_options[1].include? key
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Returns an option hash for the given table.
|
|
201
|
+
# Accumulates options for all matching table specs (most recently added options
|
|
202
|
+
# overwrite according options added before).
|
|
203
|
+
#
|
|
204
|
+
# Also includes the general options as returned by #options.
|
|
205
|
+
# (Table specific options overwrite the general options).
|
|
206
|
+
#
|
|
207
|
+
# Possible option values are described under #add_tables.
|
|
208
|
+
def options_for_table(table)
|
|
209
|
+
resulting_options = options.clone
|
|
210
|
+
tables_with_options.each do |table_options|
|
|
211
|
+
match = false
|
|
212
|
+
if table_options[0].kind_of? Regexp
|
|
213
|
+
match = (table_options[0] =~ table)
|
|
214
|
+
else
|
|
215
|
+
match = (table_options[0].sub(/(^.*),.*/,'\1').strip == table)
|
|
216
|
+
end
|
|
217
|
+
resulting_options.merge! table_options[1] if match
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Merge the default syncer options in (if syncer has some)
|
|
221
|
+
syncer_class = Syncers.configured_syncer(resulting_options)
|
|
222
|
+
if syncer_class.respond_to? :default_options
|
|
223
|
+
default_syncer_options = syncer_class.default_options.clone
|
|
224
|
+
else
|
|
225
|
+
default_syncer_options = {}
|
|
226
|
+
end
|
|
227
|
+
resulting_options = default_syncer_options.merge! resulting_options
|
|
228
|
+
|
|
229
|
+
resulting_options
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# initialize configuration settings
|
|
233
|
+
def initialize
|
|
234
|
+
self.left = {}
|
|
235
|
+
self.right = {}
|
|
236
|
+
self.options = DEFAULT_OPTIONS.clone
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
end
|
|
240
|
+
end
|