Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Reducing the risk of foreign key violations during replication

Props for a great root cause analysis go to TonyB
  http://groups.google.com/group/rubyrep/browse_thread/thread/0e8f4332326366b1

The best possible solution would be to
(1) do the original inserts with the foreign key set to null
(2) do an update to the right foreign key as soon as the dependent record
    is created

That solution however would require knowledge of which columns are
foreign key columns and to which tables & target columns they map.
Additionally the changes would have to be searched for when the
referenced records get created.

So second best solution (which this commit implements) that should solve
the problem in at least most of the cases:
Retry inserting the failed record at the end of the replication run.
  • Loading branch information...
commit dbe4377f58f89e688e693909df889f552c9cc9e8 1 parent a1b177f
Arndt Lehmann authored
View
4 lib/rubyrep/replication_difference.rb
@@ -19,6 +19,10 @@ def session
# * :+no_diff+: changes in both databases constitute no difference
attr_accessor :type
+ # Is set to +true+ if first replication attempt failed but it should be tried again later
+ attr_accessor :second_chance
+ alias_method :second_chance?, :second_chance
+
# A hash with keys :+left+ and / or :+right+.
# Hash values are LoggedChange instances.
def changes
View
48 lib/rubyrep/replication_run.rb
@@ -11,6 +11,11 @@ class ReplicationRun
# The current TaskSweeper
attr_accessor :sweeper
+ # An array of ReplicationDifference which originally failed replication but should be tried one more time
+ def second_chancers
+ @second_chancers ||= []
+ end
+
# Returns the current ReplicationHelper; creates it if necessary
def helper
@helper ||= ReplicationHelper.new(self)
@@ -39,6 +44,20 @@ def event_filtered?(diff)
end
end
+ # Returns the next available ReplicationDifference.
+ # (Either new unprocessed differences or if not available, the first available 'second chancer'.)
+ #
+ def load_difference
+ @loaders ||= LoggedChangeLoaders.new(session)
+ @loaders.update # ensure the cache of change log records is up-to-date
+ diff = ReplicationDifference.new @loaders
+ diff.load
+ unless diff.loaded? or second_chancers.empty?
+ diff = second_chancers.shift
+ end
+ diff
+ end
+
# Executes the replication run.
def run
return unless [:left, :right].any? do |database|
@@ -57,29 +76,36 @@ def run
# Check for this and if timed out, return (silently).
return if sweeper.terminated?
- loaders = LoggedChangeLoaders.new(session)
-
success = false
begin
replicator # ensure that replicator is created and has chance to validate settings
loop do
begin
- loaders.update # ensure the cache of change log records is up-to-date
- diff = ReplicationDifference.new loaders
- diff.load
+ diff = load_difference
break unless diff.loaded?
break if sweeper.terminated?
if diff.type != :no_diff and not event_filtered?(diff)
replicator.replicate_difference diff
end
rescue Exception => e
- begin
- helper.log_replication_outcome diff, e.message,
- e.class.to_s + "\n" + e.backtrace.join("\n")
- rescue Exception => _
- # if logging to database itself fails, re-raise the original exception
- raise e
+ if e.message =~ /violates foreign key constraint|foreign key constraint fails/i and !diff.second_chance?
+ # Note:
+ # Identifying the foreign key constraint violation via regular expression is
+ # database dependent and *dirty*.
+ # It would be better to use the ActiveRecord #translate_exception mechanism.
+ # However as per version 3.0.5 this doesn't work yet properly.
+
+ diff.second_chance = true
+ second_chancers << diff
+ else
+ begin
+ helper.log_replication_outcome diff, e.message,
+ e.class.to_s + "\n" + e.backtrace.join("\n")
+ rescue Exception => _
+ # if logging to database itself fails, re-raise the original exception
+ raise e
+ end
end
end
end
View
62 spec/replication_run_spec.rb
@@ -135,6 +135,68 @@ def filter.before_replicate(table, key, helper, diff)
end
end
+ it "run should replication records with foreign key constraints" do
+ begin
+ config = deep_copy(standard_config)
+ config.options[:committer] = :never_commit
+
+ session = Session.new(config)
+
+ session.left.insert_record 'referencing_table', {
+ 'id' => '5',
+ }
+ session.left.insert_record 'rr_pending_changes', {
+ 'change_table' => 'referencing_table',
+ 'change_key' => 'id|5',
+ 'change_type' => 'I',
+ 'change_time' => Time.now
+ }
+
+ session.left.insert_record 'referenced_table2', {
+ 'id' => '6',
+ }
+ session.left.insert_record 'rr_pending_changes', {
+ 'change_table' => 'referenced_table2',
+ 'change_key' => 'id|6',
+ 'change_type' => 'I',
+ 'change_time' => Time.now
+ }
+
+ session.left.update_record 'referencing_table', {
+ 'id' => 5,
+ 'third_fk' => '6'
+ }
+ session.left.insert_record 'rr_pending_changes', {
+ 'change_table' => 'referencing_table',
+ 'change_key' => 'id|5',
+ 'change_new_key' => 'id|5',
+ 'change_type' => 'U',
+ 'change_time' => Time.now
+ }
+
+ run = ReplicationRun.new session, TaskSweeper.new(1)
+ run.run
+
+ session.right.select_record(:table => "referencing_table", :from => {'id' => 5}).should == {
+ 'id' => 5,
+ 'first_fk' => nil,
+ 'second_fk' => nil,
+ 'third_fk' => 6
+ }
+ ensure
+ Committers::NeverCommitter.rollback_current_session
+ if session
+ session.left.execute "delete from referencing_table where id = 5"
+ session.left.execute "delete from referenced_table2 where id = 6"
+
+ session.right.execute "delete from referencing_table where id = 5"
+ session.right.execute "delete from referenced_table2 where id = 6"
+
+ session.left.execute "delete from rr_pending_changes"
+ end
+ end
+ end
+
it "run should not replicate filtered changes" do
begin
config = deep_copy(standard_config)
Please sign in to comment.
Something went wrong with that request. Please try again.