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,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