andyjeffries-rubyrep 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
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,65 @@
1
+ module RR
2
+ module ScanProgressPrinters
3
+
4
+ # A helper class to print a text progress bar.
5
+ class ProgressBar
6
+
7
+ MAX_MARKERS = 25 #length of the progress bar (in characters)
8
+
9
+ # Register ProgressBar with the given command line options.
10
+ # (Command line format as specified by OptionParser#on.)
11
+ # First argument is the key through which the printer can be refered in
12
+ # the configuration file
13
+ RR::ScanProgressPrinters.register :progress_bar, self,
14
+ "-b", "--progress-bar[=length]",
15
+ "Show the progress of the table scanning process as progress bar."
16
+
17
+ # Receives the command line argument
18
+ cattr_accessor :arg
19
+
20
+ # Returns the length (in characters) of the progress bar.
21
+ def max_markers
22
+ @max_markers ||= arg ? arg.to_i : MAX_MARKERS
23
+ end
24
+
25
+ # Creates a new progress bar.
26
+ # * +max_steps+: number of steps at completion
27
+ # * +session+: the current Session
28
+ # * +left_table+: name of the left database table
29
+ # * +right_table+: name of the right database table
30
+ def initialize(max_steps, session, left_table, right_table)
31
+ @use_ansi = session.configuration.options_for_table(left_table)[:use_ansi]
32
+ @max_steps, @current_steps = max_steps, 0
33
+ @steps_per_marker = @max_steps.to_f / max_markers
34
+ @current_markers, @current_percentage = 0, 0
35
+ end
36
+
37
+ # Increases progress by +step_increment+ steps.
38
+ def step(step_increment = 1)
39
+ @current_steps+= step_increment
40
+ new_markers = @max_steps != 0 ? (@current_steps / @steps_per_marker).to_i : max_markers
41
+
42
+ new_percentage = @max_steps != 0 ? @current_steps * 100 / @max_steps : 100
43
+ if @use_ansi and new_percentage != @current_percentage
44
+ # This part uses ANSI escape sequences to show a running percentage
45
+ # to the left of the progress bar
46
+ print "\e[1D" * (@current_markers + 5) if @current_percentage != 0 # go left
47
+ print "#{new_percentage}%".rjust(4) << " "
48
+ print "\e[1C" * @current_markers if @current_markers != 0 # go back right
49
+ $stdout.flush
50
+ @current_percentage = new_percentage
51
+ end
52
+
53
+ if new_markers > @current_markers
54
+ print '.' * (new_markers - @current_markers)
55
+ @current_markers = new_markers
56
+ $stdout.flush
57
+ end
58
+ if @current_steps == @max_steps
59
+ print '.' * (max_markers - @current_markers) + ' '
60
+ $stdout.flush
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,65 @@
1
+ module RR
2
+ # Manages scan progress printers. Scan progress printers implement functionality
3
+ # to report the progress of a table scan.
4
+ # *Each* table scan is handled by a *separate* printer instance.
5
+ #
6
+ # Scan progress printers need to register themselves and their command line options
7
+ # with #register.
8
+ #
9
+ # A scan progress printer needs to implement at the minimum the following
10
+ # functionality:
11
+ #
12
+ # # Receives the command line argument as yielded by OptionParser#on.
13
+ # def self.arg=(arg)
14
+ #
15
+ # # Creation of a new ScanProgressPrinter.
16
+ # # * +max_steps+: number of steps at completion
17
+ # # * +session+: the current Session
18
+ # # * +left_table+: name of the left database table
19
+ # # * +right_table+: name of the right database table
20
+ # def initialize(max_steps, left_table, right_table)
21
+ #
22
+ # # Progress is advanced by +progress+ number of steps.
23
+ # def step(progress)
24
+ #
25
+ module ScanProgressPrinters
26
+
27
+ # Hash of registered ScanProgressPrinters.
28
+ # Each entry is a hash with the following key and related value:
29
+ # * key: Identifier of the progress printer
30
+ # * value: A hash with payload information. Possible values:
31
+ # * :+printer_class+: The ScanProgressPrinter class.
32
+ # * :+opts+: An array defining the command line options (handed to OptionParter#on).
33
+ def self.printers
34
+ @@progress_printers ||= {}
35
+ end
36
+
37
+ # Needs to be called by ScanProgressPrinters to register themselves (+printer+)
38
+ # and their command line options.
39
+ # * :+printer_id+ is the symbol through which the printer can be referenced.
40
+ # * :+printer_class+ is the ScanProgressPrinter class,
41
+ # * :+opts+ is an array defining the command line options (handed to OptionParter#on).
42
+ def self.register(printer_id, printer_class, *opts)
43
+ printers[printer_id] = {
44
+ :printer_class => printer_class,
45
+ :opts => opts
46
+ }
47
+ end
48
+
49
+ # Registers all report printer command line options into the given
50
+ # OptionParser.
51
+ # Once the command line is parsed with OptionParser#parse! it will
52
+ # yield the correct printer class.
53
+ #
54
+ # Note:
55
+ # If multiple printers are specified in the command line, all are yielded.
56
+ def self.on_printer_selection(opts)
57
+ printers.each_value do |printer|
58
+ opts.on(*printer[:opts]) do |arg|
59
+ printer[:printer_class].arg = arg
60
+ yield printer[:printer_class]
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,111 @@
1
+ require 'fileutils'
2
+ require 'tmpdir'
3
+ require 'tempfile'
4
+ require 'yaml'
5
+
6
+ module RR::ScanReportPrinters
7
+ # A ScanReportPrinter producing a summary (number of differences) only.
8
+ class ScanDetailReporter < ScanSummaryReporter
9
+
10
+ # Register ScanSummaryReporter with the given command line options.
11
+ # (Command line format as specified by OptionParser#on.)
12
+ RR::ScanReportPrinters.register self, "-d", "--detailed[=mode]",
13
+ "Print the number of differences of each table. E. g.",
14
+ " left_table / right_table [differences]",
15
+ "followed by a full dump of the differences in YAML format.",
16
+ "The 'mode' argument determines how the row differences are printed:",
17
+ " * full shows the full records",
18
+ " * keys shows the primary key columns only",
19
+ " * diff shows the primary key and differing columsn only"
20
+
21
+ # The current Session object
22
+ attr_accessor :session
23
+
24
+ # The temporary File receiving the differences
25
+ attr_accessor :tmpfile
26
+
27
+ # Mode of reporting. Should be either
28
+ # * :+full+
29
+ # * :+keys+ or
30
+ # * :+diff+
31
+ attr_accessor :report_mode
32
+
33
+ # Array of names of the primary key columns of the table currently being
34
+ # scanned.
35
+ attr_accessor :primary_key_names
36
+
37
+ # A scan run is to be started using this scan result printer.
38
+ # +arg+ is the command line argument as yielded by OptionParser#on.
39
+ def initialize(session, arg)
40
+ super session, ""
41
+ self.session = session
42
+
43
+ self.report_mode = case arg
44
+ when 'diff'
45
+ :diff
46
+ when 'keys'
47
+ :keys
48
+ else
49
+ :full
50
+ end
51
+ end
52
+
53
+ # A scan of the given 'left' table and corresponding 'right' table is executed.
54
+ # Needs to yield so that the actual scan can be executed.
55
+ def scan(left_table, right_table)
56
+
57
+ super left_table, right_table
58
+
59
+ ensure
60
+ self.primary_key_names = nil
61
+ if self.tmpfile
62
+ self.tmpfile.close
63
+ self.tmpfile.open
64
+ self.tmpfile.each_line {|line| puts line}
65
+ self.tmpfile.close!
66
+ self.tmpfile = nil
67
+ end
68
+ end
69
+
70
+ # Returns a cleaned row as per current +report_mode+.
71
+ # +row+ is either a column_name => value hash or an array of 2 such rows.
72
+ def clear_columns(row)
73
+ case report_mode
74
+ when :full
75
+ row
76
+ when :keys
77
+ row = row[0] if row.kind_of?(Array)
78
+ self.primary_key_names ||= session.left.primary_key_names(self.left_table)
79
+ row.reject {|column, value| !self.primary_key_names.include?(column)}
80
+ when :diff
81
+ self.primary_key_names ||= session.left.primary_key_names(self.left_table)
82
+ if row.kind_of?(Array)
83
+ new_row_array = [{}, {}]
84
+ row[0].each do |column, value|
85
+ if self.primary_key_names.include?(column) or value != row[1][column]
86
+ new_row_array[0][column] = row[0][column]
87
+ new_row_array[1][column] = row[1][column]
88
+ end
89
+ end
90
+ new_row_array
91
+ else
92
+ row
93
+ end
94
+ end
95
+ end
96
+
97
+ # Each difference is handed to the printer as described in the format
98
+ # as described e. g. in DirectTableScan#run
99
+ def report_difference(type, row)
100
+ self.tmpfile ||= Tempfile.new 'rubyrep_scan_details'
101
+ tmpfile.puts({type => clear_columns(row)}.to_yaml)
102
+ super type, row
103
+ end
104
+
105
+ # Optional method. If a scan report printer has it, it is called after the
106
+ # last table scan is executed.
107
+ # (A good place to print a final summary.)
108
+ def scanning_finished
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,67 @@
1
+ module RR
2
+ # Manages scan report printers. Scan report printers implement functionality
3
+ # to report the row differences identified during a scan.
4
+ # *All* table scans are processed by the *same* printer instance.
5
+ #
6
+ # Scan report printers need to register themselves and their command line options
7
+ # with #register.
8
+ #
9
+ # A scan report printer needs to implement at the minimum the following
10
+ # functionality:
11
+ #
12
+ # # Creation of a new ScanReportPrinter.
13
+ # # * +session+: the current Session object
14
+ # # * +arg+: command line argument as yielded by OptionParser#on.
15
+ # def initialize(arg)
16
+ #
17
+ # # A scan of the given 'left' table and corresponding 'right' table is executed.
18
+ # # Needs to yield so that the actual scan can be executed.
19
+ # def scan(left_table, right_table)
20
+ #
21
+ # # Each difference is handed to the printer as described in the format
22
+ # # as described e. g. in DirectTableScan#run
23
+ # def report_difference(type, row)
24
+ #
25
+ # # Optional method. If a scan report printer has it, it is called after the
26
+ # # last table scan is executed.
27
+ # # (A good place to print a final summary.)
28
+ # def scanning_finished
29
+ #
30
+ module ScanReportPrinters
31
+
32
+ # Array of registered ScanReportPrinters.
33
+ # Each entry is a hash with the following keys and related values:
34
+ # * :+printer_class+: The ScanReportPrinter class.
35
+ # * :+opts+: An array defining the command line options (handed to OptionParter#on).
36
+ def self.printers
37
+ @@report_printers ||= []
38
+ end
39
+
40
+ # Needs to be called by ScanReportPrinters to register themselves (+printer+)
41
+ # and their command line options.
42
+ # * :+printer_class+ is the ScanReportPrinter class,
43
+ # * :+opts+ is an array defining the command line options (handed to OptionParter#on).
44
+ def self.register(printer_class, *opts)
45
+ printers << {
46
+ :printer_class => printer_class,
47
+ :opts => opts
48
+ }
49
+ end
50
+
51
+ # Registers all report printer command line options into the given
52
+ # OptionParser.
53
+ # Once the command line is parsed with OptionParser#parse! it will
54
+ # yield the printer class and the optional command line parameter.
55
+ #
56
+ # Note:
57
+ # If multiple printers are specified in the command line, all are created
58
+ # and yielded.
59
+ def self.on_printer_selection(opts)
60
+ printers.each do |printer|
61
+ opts.on(*printer[:opts]) do |arg|
62
+ yield printer[:printer_class], arg
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,75 @@
1
+ module RR::ScanReportPrinters
2
+ # A ScanReportPrinter producing a summary (number of differences) only.
3
+ class ScanSummaryReporter
4
+
5
+ # Register ScanSummaryReporter with the given command line options.
6
+ # (Command line format as specified by OptionParser#on.)
7
+ RR::ScanReportPrinters.register self, "-s", "--summary[=detailed]",
8
+ "Print the number of differences of each table. Either totals only, e. g.",
9
+ " left_table / right_table [differences]",
10
+ "or a detailed split by type, e. g.",
11
+ " left_table / right_table [conflicts] [left_only records] [right_only records]"
12
+
13
+ # Set to true if only the total number of differences should be reported
14
+ attr_accessor :only_totals
15
+
16
+ # Name of the left table of the current scan
17
+ attr_accessor :left_table
18
+
19
+ # Name of the right table of the current scan
20
+ attr_accessor :right_table
21
+
22
+ # Hold the result of the current scan. A hash with a running count of
23
+ # +:conflict+, +:left+ (only) or +:right+ (only) records.
24
+ attr_accessor :scan_result
25
+
26
+ # A scan run is to be started using this scan result printer.
27
+ # +arg+ is the command line argument as yielded by OptionParser#on.
28
+ def initialize(_, arg)
29
+ self.only_totals = (arg != 'detailed')
30
+ end
31
+
32
+ # A scan of the given 'left' table and corresponding 'right' table is executed.
33
+ # Needs to yield so that the actual scan can be executed.
34
+ def scan(left_table, right_table)
35
+ self.left_table = left_table
36
+ self.right_table = right_table
37
+ self.scan_result = {:conflict => 0, :left => 0, :right => 0}
38
+
39
+ header = left_table.clone
40
+ header << " / " << right_table if left_table != right_table
41
+ $stdout.write "#{header.rjust(36)} "
42
+
43
+ yield # Give control back so that the actual table scan can be done.
44
+
45
+ if only_totals
46
+ $stdout.write \
47
+ "#{rjust_value(scan_result[:conflict] + scan_result[:left] + scan_result[:right])}"
48
+ else
49
+ $stdout.write \
50
+ "#{rjust_value(scan_result[:conflict])} " +
51
+ "#{rjust_value(scan_result[:left])} " +
52
+ "#{rjust_value(scan_result[:right])}"
53
+ end
54
+ $stdout.puts
55
+ end
56
+
57
+ # Right adjusts the given number and returns according string.
58
+ def rjust_value(value)
59
+ value.to_s.rjust(3)
60
+ end
61
+ private :rjust_value
62
+
63
+ # Each difference is handed to the printer as described in the format
64
+ # as described e. g. in DirectTableScan#run
65
+ def report_difference(type, row)
66
+ scan_result[type] += 1
67
+ end
68
+
69
+ # Optional method. If a scan report printer has it, it is called after the
70
+ # last table scan is executed.
71
+ # (A good place to print a final summary.)
72
+ def scanning_finished
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+
3
+ module RR
4
+ # This class implements the functionality of the rrscan.rb command.
5
+ class ScanRunner < BaseRunner
6
+
7
+ CommandRunner.register 'scan' => {
8
+ :command => self,
9
+ :description => 'Scans for differing records between databases'
10
+ }
11
+
12
+ # Returns summary description string for the scan command.
13
+ def summary_description
14
+ "Scans for differences of the specified tables between both databases."
15
+ end
16
+
17
+ # Creates the correct scan class.
18
+ # Parameters as defined under BaseRunner#create_processor
19
+ def create_processor(left_table, right_table)
20
+ TableScanHelper.scan_class(session).new session, left_table, right_table
21
+ end
22
+ end
23
+ end
24
+
25
+
@@ -0,0 +1,230 @@
1
+ require 'drb'
2
+
3
+ module RR
4
+
5
+ # This class represents a rubyrep session.
6
+ # Creates and holds expensive objects like e. g. database connections.
7
+ class Session
8
+
9
+ # The Configuration object provided to the initializer
10
+ attr_accessor :configuration
11
+
12
+ # Returns the "left" ActiveRecord / proxy database connection
13
+ def left
14
+ @connections[:left]
15
+ end
16
+
17
+ # Stores the "left" ActiveRecord /proxy database connection
18
+ def left=(connection)
19
+ @connections[:left] = connection
20
+ end
21
+
22
+ # Returns the "right" ActiveRecord / proxy database connection
23
+ def right
24
+ @connections[:right]
25
+ end
26
+
27
+ # Stores the "right" ActiveRecord / proxy database connection
28
+ def right=(connection)
29
+ @connections[:right] = connection
30
+ end
31
+
32
+ # Hash to hold under either :left or :right the according Drb / direct DatabaseProxy
33
+ attr_accessor :proxies
34
+
35
+ # Creates a hash of manual primary key names as can be specified with the
36
+ # Configuration options :+primary_key_names+ or :+auto_key_limit+.
37
+ # * +db_arm: should be either :left or :right
38
+ #
39
+ # Returns the identified manual primary keys. This is a hash with
40
+ # * key: table_name
41
+ # * value: array of primary key names
42
+ def manual_primary_keys(db_arm)
43
+ manual_primary_keys = {}
44
+ resolver = TableSpecResolver.new self
45
+ table_pairs = resolver.resolve configuration.included_table_specs, [], false
46
+ table_pairs.each do |table_pair|
47
+ options = configuration.options_for_table(table_pair[:left])
48
+ key_names = options[:key]
49
+ if key_names == nil and options[:auto_key_limit] > 0
50
+ if left.primary_key_names(table_pair[:left], :raw => true).empty?
51
+ column_names = left.column_names(table_pair[:left])
52
+ if column_names.size <= options[:auto_key_limit]
53
+ key_names = column_names
54
+ end
55
+ end
56
+ end
57
+ if key_names
58
+ table_name = table_pair[db_arm]
59
+ manual_primary_keys[table_name] = [key_names].flatten
60
+ end
61
+ end
62
+ manual_primary_keys
63
+ end
64
+
65
+ # Returns the corresponding table in the other database.
66
+ # * +db_arm+: database of the given table (either :+left+ or :+right+)
67
+ # * +table+: name of the table
68
+ #
69
+ # If no corresponding table can be found, return the given table.
70
+ # Rationale:
71
+ # Support the case where a table was dropped from the configuration but
72
+ # there were still some unreplicated changes left.
73
+ def corresponding_table(db_arm, table)
74
+ unless @table_map
75
+ @table_map = {:left => {}, :right => {}}
76
+ resolver = TableSpecResolver.new self
77
+ table_pairs = resolver.resolve configuration.included_table_specs, [], false
78
+ table_pairs.each do |table_pair|
79
+ @table_map[:left][table_pair[:left]] = table_pair[:right]
80
+ @table_map[:right][table_pair[:right]] = table_pair[:left]
81
+ end
82
+ end
83
+ @table_map[db_arm][table] || table
84
+ end
85
+
86
+ # Returns +true+ if proxy connections are used
87
+ def proxied?
88
+ [configuration.left, configuration.right].any? \
89
+ {|arm_config| arm_config.include? :proxy_host}
90
+ end
91
+
92
+ # Returns an array of table pairs of the configured tables.
93
+ # Refer to TableSpecResolver#resolve for a detailed description of the
94
+ # return value.
95
+ # If +included_table_specs+ is provided (that is: not an empty array), it
96
+ # will be used instead of the configured table specs.
97
+ def configured_table_pairs(included_table_specs = [])
98
+ resolver = TableSpecResolver.new self
99
+ included_table_specs = configuration.included_table_specs if included_table_specs.empty?
100
+ resolver.resolve included_table_specs, configuration.excluded_table_specs
101
+ end
102
+
103
+ # Orders the array of table pairs as per primary key / foreign key relations
104
+ # of the tables. Returns the result.
105
+ # Only sorts if the configuration has set option :+table_ordering+.
106
+ # Refer to TableSpecResolver#resolve for a detailed description of the
107
+ # parameter and return value.
108
+ def sort_table_pairs(table_pairs)
109
+ if configuration.options[:table_ordering]
110
+ left_tables = table_pairs.map {|table_pair| table_pair[:left]}
111
+ sorted_left_tables = TableSorter.new(self, left_tables).sort
112
+ sorted_left_tables.map do |left_table|
113
+ table_pairs.find do |table_pair|
114
+ table_pair[:left] == left_table
115
+ end
116
+ end
117
+ else
118
+ table_pairs
119
+ end
120
+ end
121
+
122
+ # Returns +true+ if the specified database connection is not alive.
123
+ # * +database+: target database (either +:left+ or :+right+)
124
+ def database_unreachable?(database)
125
+ unreachable = true
126
+ Thread.new do
127
+ begin
128
+ if send(database) && send(database).select_one("select 1+1 as x")['x'].to_i == 2
129
+ unreachable = false # database is actually reachable
130
+ end
131
+ end rescue nil
132
+ end.join configuration.options[:database_connection_timeout]
133
+ unreachable
134
+ end
135
+
136
+ # Disconnects both database connections
137
+ def disconnect_databases
138
+ [:left, :right].each do |database|
139
+ disconnect_database(database)
140
+ end
141
+ end
142
+
143
+ # Disconnnects the specified database
144
+ # * +database+: the target database (either :+left+ or :+right+)
145
+ def disconnect_database(database)
146
+ proxy, connection = @proxies[database], @connections[database]
147
+ @proxies[database] = nil
148
+ @connections[database] = nil
149
+ if proxy
150
+ proxy.destroy_session(connection)
151
+ end
152
+ end
153
+
154
+ # Refreshes both database connections
155
+ # * +options+: A options hash with the following settings
156
+ # * :+forced+: if +true+, always establish a new database connection
157
+ def refresh(options = {})
158
+ [:left, :right].each {|database| refresh_database_connection database, options}
159
+ end
160
+
161
+ # Refreshes the specified database connection.
162
+ # (I. e. reestablish if not active anymore.)
163
+ # * +database+: target database (either :+left+ or :+right+)
164
+ # * +options+: A options hash with the following settings
165
+ # * :+forced+: if +true+, always establish a new database connection
166
+ def refresh_database_connection(database, options)
167
+ if options[:forced] or database_unreachable?(database)
168
+ # step 1: disconnect both database connection (if still possible)
169
+ begin
170
+ Thread.new do
171
+ disconnect_database database rescue nil
172
+ end.join configuration.options[:database_connection_timeout]
173
+ end
174
+
175
+ connect_exception = nil
176
+ # step 2: try to reconnect the database
177
+ Thread.new do
178
+ begin
179
+ connect_database database
180
+ rescue Exception => e
181
+ # save exception so it can be rethrown outside of the thread
182
+ connect_exception = e
183
+ end
184
+ end.join configuration.options[:database_connection_timeout]
185
+ raise connect_exception if connect_exception
186
+
187
+ # step 3: verify if database connections actually work (to detect silent connection failures)
188
+ if database_unreachable?(database)
189
+ raise "no connection to '#{database}' database"
190
+ end
191
+ end
192
+ end
193
+
194
+ # Set up the (proxied or direct) database connections to the specified
195
+ # database.
196
+ # * +database+: the target database (either :+left+ or :+right+)
197
+ def connect_database(database)
198
+ if configuration.left == configuration.right and database == :right
199
+ # If both database configurations point to the same database
200
+ # then don't create the database connection twice.
201
+ # Assumes that the left database is always connected before the right one.
202
+ self.right = self.left
203
+ else
204
+ # Connect the database / proxy
205
+ arm_config = configuration.send database
206
+ if arm_config.include? :proxy_host
207
+ drb_url = "druby://#{arm_config[:proxy_host]}:#{arm_config[:proxy_port]}"
208
+ @proxies[database] = DRbObject.new nil, drb_url
209
+ else
210
+ # Create fake proxy
211
+ @proxies[database] = DatabaseProxy.new
212
+ end
213
+ @connections[database] = @proxies[database].create_session arm_config
214
+
215
+ send(database).manual_primary_keys = manual_primary_keys(database)
216
+ end
217
+ end
218
+
219
+ # Creates a new rubyrep session with the provided Configuration
220
+ def initialize(config = Initializer::configuration)
221
+ @connections = {:left => nil, :right => nil}
222
+ @proxies = {:left => nil, :right => nil}
223
+
224
+ # Keep the database configuration for future reference
225
+ self.configuration = config
226
+
227
+ refresh
228
+ end
229
+ end
230
+ end