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.
Files changed (125) hide show
  1. data/History.txt +83 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +151 -0
  4. data/README.txt +37 -0
  5. data/bin/rubyrep +8 -0
  6. data/lib/rubyrep.rb +72 -0
  7. data/lib/rubyrep/base_runner.rb +195 -0
  8. data/lib/rubyrep/command_runner.rb +144 -0
  9. data/lib/rubyrep/committers/buffered_committer.rb +151 -0
  10. data/lib/rubyrep/committers/committers.rb +152 -0
  11. data/lib/rubyrep/configuration.rb +275 -0
  12. data/lib/rubyrep/connection_extenders/connection_extenders.rb +165 -0
  13. data/lib/rubyrep/connection_extenders/jdbc_extender.rb +65 -0
  14. data/lib/rubyrep/connection_extenders/mysql_extender.rb +59 -0
  15. data/lib/rubyrep/connection_extenders/postgresql_extender.rb +277 -0
  16. data/lib/rubyrep/database_proxy.rb +52 -0
  17. data/lib/rubyrep/direct_table_scan.rb +75 -0
  18. data/lib/rubyrep/generate_runner.rb +105 -0
  19. data/lib/rubyrep/initializer.rb +39 -0
  20. data/lib/rubyrep/log_helper.rb +30 -0
  21. data/lib/rubyrep/logged_change.rb +160 -0
  22. data/lib/rubyrep/logged_change_loader.rb +197 -0
  23. data/lib/rubyrep/noisy_connection.rb +80 -0
  24. data/lib/rubyrep/proxied_table_scan.rb +171 -0
  25. data/lib/rubyrep/proxy_block_cursor.rb +145 -0
  26. data/lib/rubyrep/proxy_connection.rb +431 -0
  27. data/lib/rubyrep/proxy_cursor.rb +44 -0
  28. data/lib/rubyrep/proxy_row_cursor.rb +43 -0
  29. data/lib/rubyrep/proxy_runner.rb +89 -0
  30. data/lib/rubyrep/replication_difference.rb +100 -0
  31. data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
  32. data/lib/rubyrep/replication_extenders/postgresql_replication.rb +236 -0
  33. data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
  34. data/lib/rubyrep/replication_helper.rb +142 -0
  35. data/lib/rubyrep/replication_initializer.rb +327 -0
  36. data/lib/rubyrep/replication_run.rb +142 -0
  37. data/lib/rubyrep/replication_runner.rb +166 -0
  38. data/lib/rubyrep/replicators/replicators.rb +42 -0
  39. data/lib/rubyrep/replicators/two_way_replicator.rb +361 -0
  40. data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
  41. data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
  42. data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
  43. data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
  44. data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
  45. data/lib/rubyrep/scan_runner.rb +25 -0
  46. data/lib/rubyrep/session.rb +230 -0
  47. data/lib/rubyrep/sync_helper.rb +121 -0
  48. data/lib/rubyrep/sync_runner.rb +31 -0
  49. data/lib/rubyrep/syncers/syncers.rb +112 -0
  50. data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
  51. data/lib/rubyrep/table_scan.rb +54 -0
  52. data/lib/rubyrep/table_scan_helper.rb +46 -0
  53. data/lib/rubyrep/table_sorter.rb +70 -0
  54. data/lib/rubyrep/table_spec_resolver.rb +142 -0
  55. data/lib/rubyrep/table_sync.rb +90 -0
  56. data/lib/rubyrep/task_sweeper.rb +77 -0
  57. data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
  58. data/lib/rubyrep/type_casting_cursor.rb +31 -0
  59. data/lib/rubyrep/uninstall_runner.rb +93 -0
  60. data/lib/rubyrep/version.rb +9 -0
  61. data/rubyrep +8 -0
  62. data/rubyrep.bat +4 -0
  63. data/setup.rb +1585 -0
  64. data/spec/base_runner_spec.rb +218 -0
  65. data/spec/buffered_committer_spec.rb +274 -0
  66. data/spec/command_runner_spec.rb +145 -0
  67. data/spec/committers_spec.rb +178 -0
  68. data/spec/configuration_spec.rb +203 -0
  69. data/spec/connection_extender_interface_spec.rb +141 -0
  70. data/spec/connection_extenders_registration_spec.rb +164 -0
  71. data/spec/database_proxy_spec.rb +48 -0
  72. data/spec/database_rake_spec.rb +40 -0
  73. data/spec/db_specific_connection_extenders_spec.rb +34 -0
  74. data/spec/db_specific_replication_extenders_spec.rb +38 -0
  75. data/spec/direct_table_scan_spec.rb +61 -0
  76. data/spec/dolphins.jpg +0 -0
  77. data/spec/generate_runner_spec.rb +84 -0
  78. data/spec/initializer_spec.rb +46 -0
  79. data/spec/log_helper_spec.rb +39 -0
  80. data/spec/logged_change_loader_spec.rb +68 -0
  81. data/spec/logged_change_spec.rb +470 -0
  82. data/spec/noisy_connection_spec.rb +78 -0
  83. data/spec/postgresql_replication_spec.rb +48 -0
  84. data/spec/postgresql_schema_support_spec.rb +212 -0
  85. data/spec/postgresql_support_spec.rb +63 -0
  86. data/spec/progress_bar_spec.rb +77 -0
  87. data/spec/proxied_table_scan_spec.rb +151 -0
  88. data/spec/proxy_block_cursor_spec.rb +197 -0
  89. data/spec/proxy_connection_spec.rb +423 -0
  90. data/spec/proxy_cursor_spec.rb +56 -0
  91. data/spec/proxy_row_cursor_spec.rb +66 -0
  92. data/spec/proxy_runner_spec.rb +70 -0
  93. data/spec/replication_difference_spec.rb +161 -0
  94. data/spec/replication_extender_interface_spec.rb +367 -0
  95. data/spec/replication_extenders_spec.rb +32 -0
  96. data/spec/replication_helper_spec.rb +178 -0
  97. data/spec/replication_initializer_spec.rb +509 -0
  98. data/spec/replication_run_spec.rb +443 -0
  99. data/spec/replication_runner_spec.rb +254 -0
  100. data/spec/replicators_spec.rb +36 -0
  101. data/spec/rubyrep_spec.rb +8 -0
  102. data/spec/scan_detail_reporter_spec.rb +119 -0
  103. data/spec/scan_progress_printers_spec.rb +68 -0
  104. data/spec/scan_report_printers_spec.rb +67 -0
  105. data/spec/scan_runner_spec.rb +50 -0
  106. data/spec/scan_summary_reporter_spec.rb +61 -0
  107. data/spec/session_spec.rb +253 -0
  108. data/spec/spec.opts +1 -0
  109. data/spec/spec_helper.rb +305 -0
  110. data/spec/strange_name_support_spec.rb +135 -0
  111. data/spec/sync_helper_spec.rb +169 -0
  112. data/spec/sync_runner_spec.rb +78 -0
  113. data/spec/syncers_spec.rb +171 -0
  114. data/spec/table_scan_helper_spec.rb +36 -0
  115. data/spec/table_scan_spec.rb +49 -0
  116. data/spec/table_sorter_spec.rb +30 -0
  117. data/spec/table_spec_resolver_spec.rb +111 -0
  118. data/spec/table_sync_spec.rb +140 -0
  119. data/spec/task_sweeper_spec.rb +47 -0
  120. data/spec/trigger_mode_switcher_spec.rb +83 -0
  121. data/spec/two_way_replicator_spec.rb +721 -0
  122. data/spec/two_way_syncer_spec.rb +256 -0
  123. data/spec/type_casting_cursor_spec.rb +50 -0
  124. data/spec/uninstall_runner_spec.rb +93 -0
  125. metadata +190 -0
@@ -0,0 +1,142 @@
1
+ require 'timeout'
2
+
3
+ module RR
4
+
5
+ # Executes a single replication run
6
+ class ReplicationRun
7
+
8
+ # The current Session object
9
+ attr_accessor :session
10
+
11
+ # The current TaskSweeper
12
+ attr_accessor :sweeper
13
+
14
+ # An array of ReplicationDifference which originally failed replication but should be tried one more time
15
+ def second_chancers
16
+ @second_chancers ||= []
17
+ end
18
+
19
+ # Returns the current ReplicationHelper; creates it if necessary
20
+ def helper
21
+ @helper ||= ReplicationHelper.new(self)
22
+ end
23
+
24
+ # Returns the current replicator; creates it if necessary.
25
+ def replicator
26
+ @replicator ||=
27
+ Replicators.replicators[session.configuration.options[:replicator]].new(helper)
28
+ end
29
+
30
+ # Calls the event filter for the give difference.
31
+ # * +diff+: instance of ReplicationDifference
32
+ # Returns +true+ if replication of the difference should *not* proceed.
33
+ def event_filtered?(diff)
34
+ event_filter = helper.options_for_table(diff.changes[:left].table)[:event_filter]
35
+ if event_filter && event_filter.respond_to?(:before_replicate)
36
+ not event_filter.before_replicate(
37
+ diff.changes[:left].table,
38
+ helper.type_cast(diff.changes[:left].table, diff.changes[:left].key),
39
+ helper,
40
+ diff
41
+ )
42
+ else
43
+ false
44
+ end
45
+ end
46
+
47
+ # Returns the next available ReplicationDifference.
48
+ # (Either new unprocessed differences or if not available, the first available 'second chancer'.)
49
+ #
50
+ def load_difference
51
+ @loaders ||= LoggedChangeLoaders.new(session)
52
+ @loaders.update # ensure the cache of change log records is up-to-date
53
+ diff = ReplicationDifference.new @loaders
54
+ diff.load
55
+ unless diff.loaded? or second_chancers.empty?
56
+ diff = second_chancers.shift
57
+ end
58
+ diff
59
+ end
60
+
61
+ # Executes the replication run.
62
+ def run
63
+ return unless [:left, :right].any? do |database|
64
+ changes_pending = false
65
+ t = Thread.new do
66
+ changes_pending = session.send(database).select_one(
67
+ "select id from #{session.configuration.options[:rep_prefix]}_pending_changes limit 1"
68
+ ) != nil
69
+ end
70
+ t.join session.configuration.options[:database_connection_timeout]
71
+ changes_pending
72
+ end
73
+
74
+ # Apparently sometimes above check for changes takes already so long, that
75
+ # the replication run times out.
76
+ # Check for this and if timed out, return (silently).
77
+ return if sweeper.terminated?
78
+
79
+ success = false
80
+ begin
81
+ replicator # ensure that replicator is created and has chance to validate settings
82
+
83
+ loop do
84
+ begin
85
+ diff = load_difference
86
+ break unless diff.loaded?
87
+ break if sweeper.terminated?
88
+ if diff.type != :no_diff and not event_filtered?(diff)
89
+ replicator.replicate_difference diff
90
+ end
91
+ rescue Exception => e
92
+ if e.message =~ /violates foreign key constraint|foreign key constraint fails/i and !diff.second_chance?
93
+ # Note:
94
+ # Identifying the foreign key constraint violation via regular expression is
95
+ # database dependent and *dirty*.
96
+ # It would be better to use the ActiveRecord #translate_exception mechanism.
97
+ # However as per version 3.0.5 this doesn't work yet properly.
98
+
99
+ diff.second_chance = true
100
+ second_chancers << diff
101
+ else
102
+ begin
103
+ helper.log_replication_outcome diff, e.message,
104
+ e.class.to_s + "\n" + e.backtrace.join("\n")
105
+ rescue Exception => _
106
+ # if logging to database itself fails, re-raise the original exception
107
+ raise e
108
+ end
109
+ end
110
+ end
111
+ end
112
+ success = true
113
+ ensure
114
+ if sweeper.terminated?
115
+ helper.finalize false
116
+ session.disconnect_databases
117
+ else
118
+ helper.finalize success
119
+ end
120
+ end
121
+ end
122
+
123
+ # Installs the current sweeper into the database connections
124
+ def install_sweeper
125
+ [:left, :right].each do |database|
126
+ unless session.send(database).respond_to?(:sweeper)
127
+ session.send(database).send(:extend, NoisyConnection)
128
+ end
129
+ session.send(database).sweeper = sweeper
130
+ end
131
+ end
132
+
133
+ # Creates a new ReplicationRun instance.
134
+ # * +session+: the current Session
135
+ # * +sweeper+: the current TaskSweeper
136
+ def initialize(session, sweeper)
137
+ self.session = session
138
+ self.sweeper = sweeper
139
+ install_sweeper
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,166 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+
3
+ require 'optparse'
4
+ require 'thread'
5
+ require 'monitor'
6
+
7
+ class Monitor
8
+ alias lock mon_enter
9
+ alias unlock mon_exit
10
+ end
11
+
12
+ module RR
13
+ # This class implements the functionality of the 'replicate' command.
14
+ class ReplicationRunner
15
+
16
+ CommandRunner.register 'replicate' => {
17
+ :command => self,
18
+ :description => 'Starts a replication process'
19
+ }
20
+
21
+ # Provided options. Possible values:
22
+ # * +:config_file+: path to config file
23
+ attr_accessor :options
24
+
25
+ # Should be set to +true+ if the replication runner should be terminated.
26
+ attr_accessor :termination_requested
27
+
28
+ # Parses the given command line parameter array.
29
+ # Returns the status (as per UNIX conventions: 1 if parameters were invalid,
30
+ # 0 otherwise)
31
+ def process_options(args)
32
+ status = 0
33
+ self.options = {}
34
+
35
+ parser = OptionParser.new do |opts|
36
+ opts.banner = <<EOS
37
+ Usage: #{$0} replicate [options]
38
+
39
+ Replicates two databases as per specified configuration file.
40
+ EOS
41
+ opts.separator ""
42
+ opts.separator " Specific options:"
43
+
44
+ opts.on("-c", "--config", "=CONFIG_FILE",
45
+ "Mandatory. Path to configuration file.") do |arg|
46
+ options[:config_file] = arg
47
+ end
48
+
49
+ opts.on_tail("--help", "Show this message") do
50
+ $stderr.puts opts
51
+ self.options = nil
52
+ end
53
+ end
54
+
55
+ begin
56
+ parser.parse!(args)
57
+ if options # this will be +nil+ if the --help option is specified
58
+ raise("Please specify configuration file") unless options.include?(:config_file)
59
+ end
60
+ rescue Exception => e
61
+ $stderr.puts "Command line parsing failed: #{e}"
62
+ $stderr.puts parser.help
63
+ self.options = nil
64
+ status = 1
65
+ end
66
+
67
+ return status
68
+ end
69
+
70
+ # Returns the active +Session+.
71
+ # Loads config file and creates session if necessary.
72
+ def session
73
+ unless @session
74
+ unless @config
75
+ load options[:config_file]
76
+ @config = Initializer.configuration
77
+ end
78
+ @session = Session.new @config
79
+ end
80
+ @session
81
+ end
82
+
83
+ # Removes current +Session+.
84
+ def clear_session
85
+ @session = nil
86
+ end
87
+
88
+ # Wait for the next replication time
89
+ def pause_replication
90
+ @last_run ||= 1.year.ago
91
+ now = Time.now
92
+ @next_run = @last_run + session.configuration.options[:replication_interval]
93
+ unless now >= @next_run
94
+ waiting_time = @next_run - now
95
+ @waiter_thread.join waiting_time
96
+ end
97
+ @last_run = Time.now
98
+ end
99
+
100
+ # Initializes the waiter thread used for replication pauses and processing
101
+ # the process TERM signal.
102
+ def init_waiter
103
+ @termination_mutex = Monitor.new
104
+ @termination_mutex.lock
105
+ @waiter_thread ||= Thread.new {@termination_mutex.lock; self.termination_requested = true}
106
+ %w(TERM INT).each do |signal|
107
+ Signal.trap(signal) {puts "\nCaught '#{signal}': Initiating graceful shutdown"; @termination_mutex.unlock}
108
+ end
109
+ end
110
+
111
+ # Prepares the replication
112
+ def prepare_replication
113
+ initializer = ReplicationInitializer.new session
114
+ initializer.prepare_replication
115
+ end
116
+
117
+ # Executes a single replication run
118
+ def execute_once
119
+ session.refresh
120
+ timeout = session.configuration.options[:database_connection_timeout]
121
+ terminated = TaskSweeper.timeout(timeout) do |sweeper|
122
+ run = ReplicationRun.new session, sweeper
123
+ run.run
124
+ end.terminated?
125
+ raise "replication run timed out" if terminated
126
+ rescue Exception => e
127
+ clear_session
128
+ raise e
129
+ end
130
+
131
+ # Executes an endless loop of replication runs
132
+ def execute
133
+ init_waiter
134
+ prepare_replication
135
+
136
+ until termination_requested do
137
+ begin
138
+ execute_once
139
+ rescue Exception => e
140
+ now = Time.now.iso8601
141
+ $stderr.puts "#{now} Exception caught: #{e}"
142
+ if @last_exception_message != e.to_s # only print backtrace if something changed
143
+ @last_exception_message = e.to_s
144
+ $stderr.puts e.backtrace.map {|line| line.gsub(/^/, "#{' ' * now.length} ")}
145
+ end
146
+ end
147
+ pause_replication
148
+ end
149
+ end
150
+
151
+ # Entry points for executing a processing run.
152
+ # args: the array of command line options that were provided by the user.
153
+ def self.run(args)
154
+ runner = new
155
+
156
+ status = runner.process_options(args)
157
+ if runner.options
158
+ runner.execute
159
+ end
160
+ status
161
+ end
162
+
163
+ end
164
+ end
165
+
166
+
@@ -0,0 +1,42 @@
1
+ module RR
2
+ # Replicators are classes that implement the replication policies.
3
+ # This module provides functionality to register replicators and access the
4
+ # list of registered replicators.
5
+ # Each Replicator must register itself with Replicators#register.
6
+ # Each Replicator must implement the following methods:
7
+ #
8
+ # # Creates a new replicator (A replicator is used for one replication run only)
9
+ # # * sync_helper: a SyncHelper object providing necessary information and functionalities
10
+ # def initialize(sync_helper)
11
+ #
12
+ # # Called to sync the provided difference.
13
+ # # +difference+ is an instance of +ReplicationDifference+
14
+ # def replicate_difference(difference)
15
+ #
16
+ # # Provides default option for the replicator. Optional.
17
+ # # Returns a hash with :key => value pairs.
18
+ # def self.default_options
19
+ module Replicators
20
+ # Returns a Hash of currently registered replicators.
21
+ # (Empty Hash if no replicators were defined.)
22
+ def self.replicators
23
+ @replicators ||= {}
24
+ @replicators
25
+ end
26
+
27
+ # Returns the correct replicator class as per provided options hash
28
+ def self.configured_replicator(options)
29
+ replicators[options[:replicator]]
30
+ end
31
+
32
+ # Registers one or multiple replicators.
33
+ # syncer_hash is a Hash with
34
+ # key:: The adapter symbol as used to reference the replicator
35
+ # value:: The class implementing the replicator
36
+ def self.register(replicator_hash)
37
+ @replicators ||= {}
38
+ @replicators.merge! replicator_hash
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,361 @@
1
+ module RR
2
+ module Replicators
3
+ # This replicator implements a two way replication.
4
+ # Options:
5
+ # * :+left_change_handling+, :+right_change_handling+:
6
+ # Handling of records that were changed only in the named database.
7
+ # Can be any of the following:
8
+ # * :+ignore+: No action.
9
+ # * :+replicate+: Updates other database accordingly. *Default* *Setting*
10
+ # * +Proc+ object:
11
+ # If a Proc object is given, it is responsible for dealing with the
12
+ # record. Called with the following parameters:
13
+ # * replication_helper: The current ReplicationHelper instance.
14
+ # * difference: A ReplicationDifference instance describing the change
15
+ # * :+replication_conflict_handling+:
16
+ # Handling of conflicting record changes. Can be any of the following:
17
+ # * :+ignore+: No action. *Default* *Setting*
18
+ # * :+left_wins+: The right database is updated accordingly.
19
+ # * :+right_wins+: The left database is updated accordingly.
20
+ # * :+later_wins+:
21
+ # The more recent change is replicated.
22
+ # (If both changes have same age: left change is replicated)
23
+ # * :+earlier_wins+:
24
+ # The less recent change is replicated.
25
+ # (If both records have same age: left change is replicated)
26
+ # * +Proc+ object:
27
+ # If a Proc object is given, it is responsible for dealing with the
28
+ # record. Called with the following parameters:
29
+ # * replication_helper: The current ReplicationHelper instance.
30
+ # * difference: A ReplicationDifference instance describing the changes
31
+ # * :+logged_replication_events+:
32
+ # Specifies which types of replications are logged.
33
+ # Is either a single value or an array of multiple ones.
34
+ # Default: [:ignored_conflicts]
35
+ # Possible values:
36
+ # * :+ignored_changes+: log ignored (but not replicated) non-conflict changes
37
+ # * :+all_changes+: log all non-conflict changes
38
+ # * :+ignored_conflicts+: log ignored (but not replicated) conflicts
39
+ # * :+all_conflicts+: log all conflicts
40
+ #
41
+ # Example of using a Proc object for custom behaviour:
42
+ # lambda do |rep_helper, diff|
43
+ # # if specified as replication_conflict_handling option, logs all
44
+ # # conflicts to a text file
45
+ # File.open('/var/log/rubyrep_conflict_log', 'a') do |f|
46
+ # f.puts <<-end_str
47
+ # #{Time.now}: conflict
48
+ # * in table #{diff.changes[:left].table}
49
+ # * for record '#{diff.changes[:left].key}'
50
+ # * change type in left db: '#{diff.changes[:left].type}'
51
+ # * change type in right db: '#{diff.changes[:right].type}'
52
+ # end_str
53
+ # end
54
+ # end
55
+ class TwoWayReplicator
56
+
57
+ # Register the syncer
58
+ Replicators.register :two_way => self
59
+
60
+ # The current ReplicationHelper object
61
+ attr_accessor :rep_helper
62
+
63
+ # Provides default option for the replicator. Optional.
64
+ # Returns a hash with key => value pairs.
65
+ def self.default_options
66
+ {
67
+ :left_change_handling => :replicate,
68
+ :right_change_handling => :replicate,
69
+ :replication_conflict_handling => :ignore,
70
+ :logged_replication_events => [:ignored_conflicts],
71
+ }
72
+ end
73
+
74
+ # Checks if an option is configured correctly. Raises an ArgumentError if not.
75
+ # * +table_spec+: the table specification to which the option belongs. May be +nil+.
76
+ # * +valid_option_values+: array of valid option values
77
+ # * +option_key+: the key of the option that is to be checked
78
+ # * +option_value+: the value of the option that is to be checked
79
+ def verify_option(table_spec, valid_option_values, option_key, option_value)
80
+ unless valid_option_values.include? option_value
81
+ message = ""
82
+ message << "#{table_spec.inspect}: " if table_spec
83
+ message << "#{option_value.inspect} not a valid #{option_key.inspect} option"
84
+ raise ArgumentError.new(message)
85
+ end
86
+ end
87
+
88
+ # Verifies if the :+left_change_handling+ / :+right_change_handling+
89
+ # options are valid.
90
+ # Raises an ArgumentError if an option is invalid
91
+ def validate_change_handling_options
92
+ [:left_change_handling, :right_change_handling].each do |key|
93
+ rep_helper.session.configuration.each_matching_option(key) do |table_spec, value|
94
+ unless value.respond_to? :call
95
+ verify_option table_spec, [:ignore, :replicate], key, value
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ # Verifies if the given :+replication_conflict_handling+ options are valid.
102
+ # Raises an ArgumentError if an option is invalid.
103
+ def validate_conflict_handling_options
104
+ rep_helper.session.configuration.each_matching_option(:replication_conflict_handling) do |table_spec, value|
105
+ unless value.respond_to? :call
106
+ verify_option table_spec,
107
+ [:ignore, :left_wins, :right_wins, :later_wins, :earlier_wins],
108
+ :replication_conflict_handling, value
109
+ end
110
+ end
111
+ end
112
+
113
+ # Verifies if the given :+replication_logging+ option /options is / are valid.
114
+ # Raises an ArgumentError if invalid
115
+ def validate_logging_options
116
+ rep_helper.session.configuration.each_matching_option(:logged_replication_events) do |table_spec, values|
117
+ values = [values].flatten # ensure that I have an array
118
+ values.each do |value|
119
+ verify_option table_spec,
120
+ [:ignored_changes, :all_changes, :ignored_conflicts, :all_conflicts],
121
+ :logged_replication_events, value
122
+ end
123
+ end
124
+ end
125
+
126
+ # Initializes the TwoWayReplicator
127
+ # Raises an ArgumentError if any of the replication options is invalid.
128
+ #
129
+ # Parameters:
130
+ # * rep_helper:
131
+ # The ReplicationHelper object providing information and utility functions.
132
+ def initialize(rep_helper)
133
+ self.rep_helper = rep_helper
134
+
135
+ validate_change_handling_options
136
+ validate_conflict_handling_options
137
+ validate_logging_options
138
+ end
139
+
140
+ # Shortcut to calculate the "other" database.
141
+ OTHER_SIDE = {
142
+ :left => :right,
143
+ :right => :left
144
+ }
145
+
146
+ # Specifies how to clear conflicts.
147
+ # The outer hash keys describe the type of the winning change.
148
+ # The inner hash keys describe the type of the loosing change.
149
+ # The inser hash values describe the action to take on the loosing side.
150
+ CONFLICT_STATE_MATRIX = {
151
+ :insert => {:insert => :update, :update => :update, :delete => :insert},
152
+ :update => {:insert => :update, :update => :update, :delete => :insert},
153
+ :delete => {:insert => :delete, :update => :delete, :delete => :delete}
154
+ }
155
+
156
+ # Helper function that clears a conflict by taking the change from the
157
+ # specified winning database and updating the other database accordingly.
158
+ # * +source_db+: the winning database (either :+left+ or :+right+)
159
+ # * +diff+: the ReplicationDifference instance
160
+ # * +remaining_attempts+: the number of remaining replication attempts for this difference
161
+ def clear_conflict(source_db, diff, remaining_attempts)
162
+ source_change = diff.changes[source_db]
163
+ target_db = OTHER_SIDE[source_db]
164
+ target_change = diff.changes[target_db]
165
+
166
+ target_action = CONFLICT_STATE_MATRIX[source_change.type][target_change.type]
167
+ source_key = source_change.type == :update ? source_change.new_key : source_change.key
168
+ target_key = target_change.type == :update ? target_change.new_key : target_change.key
169
+ case target_action
170
+ when :insert
171
+ attempt_insert source_db, diff, remaining_attempts, source_key
172
+ when :update
173
+ attempt_update source_db, diff, remaining_attempts, source_key, target_key
174
+ when :delete
175
+ attempt_delete source_db, diff, remaining_attempts, target_key
176
+ end
177
+ end
178
+
179
+ # Logs replication of the specified difference as per configured
180
+ # :+replication_conflict_logging+ / :+left_change_logging+ / :+right_change_logging+ options.
181
+ # * +winner+: Either the winner database (:+left+ or :+right+) or :+ignore+
182
+ # * +diff+: the ReplicationDifference instance
183
+ def log_replication_outcome(winner, diff)
184
+ options = rep_helper.options_for_table(diff.changes[:left].table)
185
+ option_values = [options[:logged_replication_events]].flatten # make sure I have an array
186
+ if diff.type == :conflict
187
+ return unless option_values.include?(:all_conflicts) or option_values.include?(:ignored_conflicts)
188
+ return if winner != :ignore and not option_values.include?(:all_conflicts)
189
+ outcome = {:left => 'left_won', :right => 'right_won', :ignore => 'ignored'}[winner]
190
+ else
191
+ return unless option_values.include?(:all_changes) or option_values.include?(:ignored_changes)
192
+ return if winner != :ignore and not option_values.include?(:all_changes)
193
+ outcome = winner == :ignore ? 'ignored' : 'replicated'
194
+ end
195
+ rep_helper.log_replication_outcome diff, outcome
196
+ end
197
+
198
+ # How often a replication will be attempted (in case it fails because the
199
+ # record in question was removed from the source or inserted into the
200
+ # target database _after_ the ReplicationDifference was loaded
201
+ MAX_REPLICATION_ATTEMPTS = 2
202
+
203
+ # Attempts to read the specified record from the source database and insert
204
+ # it into the target database.
205
+ # Retries if insert fails due to missing source or suddenly existing target
206
+ # record.
207
+ # * +source_db+: either :+left+ or :+right+ - source database of replication
208
+ # * +diff+: the current ReplicationDifference instance
209
+ # * +remaining_attempts+: the number of remaining replication attempts for this difference
210
+ # * +source_key+: a column_name => value hash identifying the source record
211
+ def attempt_insert(source_db, diff, remaining_attempts, source_key)
212
+ source_change = diff.changes[source_db]
213
+ source_table = source_change.table
214
+ target_db = OTHER_SIDE[source_db]
215
+ target_table = rep_helper.corresponding_table(source_db, source_table)
216
+
217
+ values = rep_helper.load_record source_db, source_table, source_key
218
+ if values == nil
219
+ diff.amend
220
+ replicate_difference diff, remaining_attempts - 1, "source record for insert vanished"
221
+ else
222
+ attempt_change('insert', source_db, target_db, diff, remaining_attempts) do
223
+ rep_helper.insert_record target_db, target_table, values
224
+ log_replication_outcome source_db, diff
225
+ end
226
+ end
227
+ end
228
+
229
+ # Attempts to read the specified record from the source database and update
230
+ # the specified record in the target database.
231
+ # Retries if update fails due to missing source
232
+ # * +source_db+: either :+left+ or :+right+ - source database of replication
233
+ # * +diff+: the current ReplicationDifference instance
234
+ # * +remaining_attempts+: the number of remaining replication attempts for this difference
235
+ # * +source_key+: a column_name => value hash identifying the source record
236
+ # * +target_key+: a column_name => value hash identifying the source record
237
+ def attempt_update(source_db, diff, remaining_attempts, source_key, target_key)
238
+ source_change = diff.changes[source_db]
239
+ source_table = source_change.table
240
+ target_db = OTHER_SIDE[source_db]
241
+ target_table = rep_helper.corresponding_table(source_db, source_table)
242
+
243
+ values = rep_helper.load_record source_db, source_table, source_key
244
+ if values == nil
245
+ diff.amend
246
+ replicate_difference diff, remaining_attempts - 1, "source record for update vanished"
247
+ else
248
+ attempt_change('update', source_db, target_db, diff, remaining_attempts) do
249
+ number_updated = rep_helper.update_record target_db, target_table, values, target_key
250
+ if number_updated == 0
251
+ diff.amend
252
+ replicate_difference diff, remaining_attempts - 1, "target record for update vanished"
253
+ else
254
+ log_replication_outcome source_db, diff
255
+ end
256
+ end
257
+ end
258
+ end
259
+
260
+ # Helper for execution of insert / update / delete attempts.
261
+ # Wraps those attempts into savepoints and handles exceptions.
262
+ #
263
+ # Note:
264
+ # Savepoints have to be used for PostgreSQL (as a failed SQL statement
265
+ # will otherwise invalidate the complete transaction.)
266
+ #
267
+ # * +action+: short description of change (e. g.: "update" or "delete")
268
+ # * +source_db+: either :+left+ or :+right+ - source database of replication
269
+ # * +target_db+: either :+left+ or :+right+ - target database of replication
270
+ # * +diff+: the current ReplicationDifference instance
271
+ # * +remaining_attempts+: the number of remaining replication attempts for this difference
272
+ def attempt_change(action, source_db, target_db, diff, remaining_attempts)
273
+ begin
274
+ rep_helper.session.send(target_db).execute "savepoint rr_#{action}_#{remaining_attempts}"
275
+ yield
276
+ unless rep_helper.new_transaction?
277
+ rep_helper.session.send(target_db).execute "release savepoint rr_#{action}_#{remaining_attempts}"
278
+ end
279
+ rescue Exception => e
280
+ rep_helper.session.send(target_db).execute "rollback to savepoint rr_#{action}_#{remaining_attempts}"
281
+ diff.amend
282
+ replicate_difference diff, remaining_attempts - 1,
283
+ "#{action} failed with #{e.message}"
284
+ end
285
+ end
286
+
287
+ # Attempts to delete the source record from the target database.
288
+ # E. g. if +source_db is :+left+, then the record is deleted in database
289
+ # :+right+.
290
+ # * +source_db+: either :+left+ or :+right+ - source database of replication
291
+ # * +diff+: the current ReplicationDifference instance
292
+ # * +remaining_attempts+: the number of remaining replication attempts for this difference
293
+ # * +target_key+: a column_name => value hash identifying the source record
294
+ def attempt_delete(source_db, diff, remaining_attempts, target_key)
295
+ change = diff.changes[source_db]
296
+ target_db = OTHER_SIDE[source_db]
297
+ target_table = rep_helper.corresponding_table(source_db, change.table)
298
+
299
+ attempt_change('delete', source_db, target_db, diff, remaining_attempts) do
300
+ number_updated = rep_helper.delete_record target_db, target_table, target_key
301
+ if number_updated == 0
302
+ diff.amend
303
+ replicate_difference diff, remaining_attempts - 1, "target record for delete vanished"
304
+ else
305
+ log_replication_outcome source_db, diff
306
+ end
307
+ end
308
+ end
309
+
310
+ # Called to replicate the specified difference.
311
+ # * :+diff+: ReplicationDifference instance
312
+ # * :+remaining_attempts+: how many more times a replication will be attempted
313
+ # * :+previous_failure_description+: why the previous replication attempt failed
314
+ def replicate_difference(diff, remaining_attempts = MAX_REPLICATION_ATTEMPTS, previous_failure_description = nil)
315
+ raise Exception, previous_failure_description || "max replication attempts exceeded" if remaining_attempts == 0
316
+ options = rep_helper.options_for_table(diff.changes[:left].table)
317
+ if diff.type == :left or diff.type == :right
318
+ key = diff.type == :left ? :left_change_handling : :right_change_handling
319
+ option = options[key]
320
+
321
+ if option == :ignore
322
+ log_replication_outcome :ignore, diff
323
+ elsif option == :replicate
324
+ source_db = diff.type
325
+
326
+ change = diff.changes[source_db]
327
+
328
+ case change.type
329
+ when :insert
330
+ attempt_insert source_db, diff, remaining_attempts, change.key
331
+ when :update
332
+ attempt_update source_db, diff, remaining_attempts, change.new_key, change.key
333
+ when :delete
334
+ attempt_delete source_db, diff, remaining_attempts, change.key
335
+ end
336
+ else # option must be a Proc
337
+ option.call rep_helper, diff
338
+ end
339
+ elsif diff.type == :conflict
340
+ option = options[:replication_conflict_handling]
341
+ if option == :ignore
342
+ log_replication_outcome :ignore, diff
343
+ elsif option == :left_wins
344
+ clear_conflict :left, diff, remaining_attempts
345
+ elsif option == :right_wins
346
+ clear_conflict :right, diff, remaining_attempts
347
+ elsif option == :later_wins
348
+ winner_db = diff.changes[:left].last_changed_at >= diff.changes[:right].last_changed_at ? :left : :right
349
+ clear_conflict winner_db, diff, remaining_attempts
350
+ elsif option == :earlier_wins
351
+ winner_db = diff.changes[:left].last_changed_at <= diff.changes[:right].last_changed_at ? :left : :right
352
+ clear_conflict winner_db, diff, remaining_attempts
353
+ else # option must be a Proc
354
+ option.call rep_helper, diff
355
+ end
356
+ end
357
+ end
358
+
359
+ end
360
+ end
361
+ end