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,52 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
2
+
3
+ require 'drb'
4
+
5
+ require 'rubyrep'
6
+
7
+ module RR
8
+ # The proxy to a remote database connection
9
+ class DatabaseProxy
10
+
11
+ # Ensure that the proxy object always stays on server side and only remote
12
+ # references are returned to the client.
13
+ include DRbUndumped
14
+
15
+ # Default tcp port to listen on
16
+ DEFAULT_PORT = 9876
17
+
18
+ # A simple Hash to hold Session object
19
+ # Purpose: preventing them from being garbage collected when they are only referenced through Drb
20
+ attr_accessor :session_register
21
+
22
+ def initialize
23
+ self.session_register = {}
24
+ end
25
+
26
+ # Create a ProxyConnection according to provided configuration Hash.
27
+ # +config+ is a hash as described by ActiveRecord::Base#establish_connection
28
+ def create_session(config)
29
+ session = ProxyConnection.new config
30
+ self.session_register[session] = session
31
+ session
32
+ end
33
+
34
+ # Destroys the given session from the session register
35
+ def destroy_session(session)
36
+ session.destroy
37
+ session_register.delete session
38
+ end
39
+
40
+ # Returns 'pong'. Used to verify that a working proxy is running.
41
+ def ping
42
+ 'pong'
43
+ end
44
+
45
+ # Terminates this proxy
46
+ def terminate!
47
+ # AL: The only way I could find to kill the main thread from a sub thread
48
+ Thread.main.raise SystemExit
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,75 @@
1
+ module RR
2
+
3
+ # Scans two tables for differences.
4
+ # Doesn't have any reporting functionality by itself.
5
+ # Instead DirectTableScan#run yields all the differences for the caller to do with as it pleases.
6
+ # Usage:
7
+ # 1. Create a new DirectTableScan object and hand it all necessary information
8
+ # 2. Call DirectTableScan#run to do the actual comparison
9
+ # 3. The block handed to DirectTableScan#run receives all differences
10
+ class DirectTableScan < TableScan
11
+ include TableScanHelper
12
+
13
+ # The TypeCastingCursor for the left table
14
+ attr_accessor :left_caster
15
+
16
+ # The TypeCastingCursor for the right table
17
+ attr_accessor :right_caster
18
+
19
+ # Creates a new DirectTableScan instance
20
+ # * session: a Session object representing the current database session
21
+ # * left_table: name of the table in the left database
22
+ # * right_table: name of the table in the right database. If not given, same like left_table
23
+ def initialize(session, left_table, right_table = nil)
24
+ super
25
+ end
26
+
27
+ # Runs the table scan.
28
+ # Calls the block for every found difference.
29
+ # Differences are yielded with 2 parameters
30
+ # * type: describes the difference, either :left (row only in left table), :right (row only in right table) or :conflict
31
+ # * row: For :left or :right cases a hash describing the row; for :conflict an array of left and right row.
32
+ # A row is a hash of column_name => value pairs.
33
+ def run(&blck)
34
+ left_cursor = right_cursor = nil
35
+ left_cursor = session.left.select_cursor(
36
+ :table => left_table,
37
+ :row_buffer_size => scan_options[:row_buffer_size],
38
+ :type_cast => true
39
+ )
40
+ right_cursor = session.right.select_cursor(
41
+ :table => right_table,
42
+ :row_buffer_size => scan_options[:row_buffer_size],
43
+ :type_cast => true
44
+ )
45
+ left_row = right_row = nil
46
+ update_progress 0 # ensures progress bar is printed even if there are no records
47
+ while left_row or right_row or left_cursor.next? or right_cursor.next?
48
+ # if there is no current left row, _try_ to load the next one
49
+ left_row ||= left_cursor.next_row if left_cursor.next?
50
+ # if there is no current right row, _try_ to load the next one
51
+ right_row ||= right_cursor.next_row if right_cursor.next?
52
+ rank = rank_rows left_row, right_row
53
+ case rank
54
+ when -1
55
+ yield :left, left_row
56
+ left_row = nil
57
+ update_progress 1
58
+ when 1
59
+ yield :right, right_row
60
+ right_row = nil
61
+ update_progress 1
62
+ when 0
63
+ update_progress 2
64
+ if not left_row == right_row
65
+ yield :conflict, [left_row, right_row]
66
+ end
67
+ left_row = right_row = nil
68
+ end
69
+ # check for corresponding right rows
70
+ end
71
+ ensure
72
+ [left_cursor, right_cursor].each {|cursor| cursor.clear if cursor}
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,105 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+
3
+ require 'optparse'
4
+
5
+ module RR
6
+ # This class implements the functionality of the 'generate' command.
7
+ class GenerateRunner
8
+
9
+ CONFIG_TEMPLATE = <<EOF
10
+ RR::Initializer::run do |config|
11
+ config.left = {
12
+ :adapter => 'postgresql', # or 'mysql2'
13
+ :database => 'SCOTT',
14
+ :username => 'scott',
15
+ :password => 'tiger',
16
+ :host => '172.16.1.1'
17
+ }
18
+
19
+ config.right = {
20
+ :adapter => 'postgresql',
21
+ :database => 'SCOTT',
22
+ :username => 'scott',
23
+ :password => 'tiger',
24
+ :host => '172.16.1.2'
25
+ }
26
+
27
+ config.include_tables 'dept'
28
+ config.include_tables /^e/ # regexp matching all tables starting with e
29
+ # config.include_tables /./ # regexp matching all tables in the database
30
+ end
31
+ EOF
32
+
33
+ CommandRunner.register 'generate' => {
34
+ :command => self,
35
+ :description => 'Generates a configuration file template'
36
+ }
37
+
38
+ # Provided options. Possible values:
39
+ # * +:config_file+: path to config file
40
+ attr_accessor :options
41
+
42
+ # Parses the given command line parameter array.
43
+ # Returns the status (as per UNIX conventions: 1 if parameters were invalid,
44
+ # 0 otherwise)
45
+ def process_options(args)
46
+ status = 0
47
+ self.options = {}
48
+
49
+ parser = OptionParser.new do |opts|
50
+ opts.banner = <<EOS
51
+ Usage: #{$0} generate [file_name]
52
+
53
+ Generates a configuration file template under name [file_name].
54
+ EOS
55
+ opts.separator ""
56
+ opts.separator " Specific options:"
57
+
58
+ opts.on_tail("--help", "Show this message") do
59
+ $stderr.puts opts
60
+ self.options = nil
61
+ end
62
+ end
63
+
64
+ begin
65
+ unprocessed_args = parser.parse!(args)
66
+ if options # this will be +nil+ if the --help option is specified
67
+ raise("Please specify the name of the configuration file") if unprocessed_args.empty?
68
+ options[:file_name] = unprocessed_args[0]
69
+ end
70
+ rescue Exception => e
71
+ $stderr.puts "Command line parsing failed: #{e}"
72
+ $stderr.puts parser.help
73
+ self.options = nil
74
+ status = 1
75
+ end
76
+
77
+ return status
78
+ end
79
+
80
+ # Generates a configuration file template.
81
+ def execute
82
+ if File.exists?(options[:file_name])
83
+ raise("Cowardly refuse to overwrite existing file '#{options[:file_name]}'")
84
+ end
85
+ File.open(options[:file_name], 'w') do |f|
86
+ f.write CONFIG_TEMPLATE
87
+ end
88
+ end
89
+
90
+ # Entry points for executing a processing run.
91
+ # args: the array of command line options that were provided by the user.
92
+ def self.run(args)
93
+ runner = new
94
+
95
+ status = runner.process_options(args)
96
+ if runner.options
97
+ runner.execute
98
+ end
99
+ status
100
+ end
101
+
102
+ end
103
+ end
104
+
105
+
@@ -0,0 +1,39 @@
1
+ module RR
2
+
3
+ # The settings of the current deployment are passed to Rubyrep through the
4
+ # Initializer::run method.
5
+ # This method yields a Configuration object for overwriting of the default
6
+ # settings.
7
+ # Accordingly a configuration file should look something like this:
8
+ #
9
+ # Rubyrep::Initializer.run do |config|
10
+ # config.left = ...
11
+ # end
12
+ class Initializer
13
+
14
+ # Sets a new Configuration object
15
+ # Current configuration values are lost and replaced with the default
16
+ # settings.
17
+ def self.reset
18
+ @@configuration = Configuration.new
19
+ end
20
+ reset
21
+
22
+ # Returns the current Configuration object
23
+ def self.configuration
24
+ @@configuration
25
+ end
26
+
27
+ # Allows direct overwriting of the Configuration
28
+ def self.configuration=(configuration)
29
+ @@configuration = configuration
30
+ end
31
+
32
+ # Yields the current Configuration object to enable overwriting of
33
+ # configuration values.
34
+ # Refer to the Initializer class documentation for a usage example.
35
+ def self.run
36
+ yield configuration
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ module RR
2
+
3
+ # Shared functionality for SyncHelper and LogHelper
4
+ module LogHelper
5
+
6
+ # Takes outcome and details and makes them fit (for available space) in the
7
+ # 'descrition' and 'long_description' columns of the event log.
8
+ # Parameters:
9
+ # * outcome: short description
10
+ # * details: long description
11
+ # Returns (cut off if necessary)
12
+ # * outcome
13
+ # * details (also containig the full outcome if it had to be cut off for short description)
14
+ def fit_description_columns(outcome, details)
15
+ outcome = outcome.to_s
16
+ if outcome.length > ReplicationInitializer::DESCRIPTION_SIZE
17
+ fitting_outcome = outcome[0...ReplicationInitializer::DESCRIPTION_SIZE]
18
+ fitting_details = outcome + "\n"
19
+ else
20
+ fitting_outcome = outcome
21
+ fitting_details = ""
22
+ end
23
+ fitting_details += details if details
24
+ fitting_details = fitting_details[0...ReplicationInitializer::LONG_DESCRIPTION_SIZE]
25
+ fitting_details = nil if fitting_details.empty?
26
+
27
+ return fitting_outcome, fitting_details
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,160 @@
1
+ module RR
2
+
3
+ # Describes a single logged record change.
4
+ #
5
+ # Note:
6
+ # The change loading functionality depends on the current database session
7
+ # being executed in an open database transaction.
8
+ # Also at the end of change processing the transaction must be committed.
9
+ class LoggedChange
10
+
11
+ # The current LoggedChangeLoader
12
+ attr_accessor :loader
13
+
14
+ # The current Session
15
+ def session
16
+ @session ||= loader.session
17
+ end
18
+
19
+ # The current database (either +:left+ or +:right+)
20
+ def database
21
+ @database ||= loader.database
22
+ end
23
+
24
+ # The name of the changed table
25
+ attr_accessor :table
26
+
27
+ # When the first change to the record happened
28
+ attr_accessor :first_changed_at
29
+
30
+ # When the last change to the record happened
31
+ attr_accessor :last_changed_at
32
+
33
+ # Type of the change. Either :+insert+, :+update+ or :+delete+.
34
+ attr_accessor :type
35
+
36
+ # A column_name => value hash identifying the changed record
37
+ attr_accessor :key
38
+
39
+ # Only used for updates: a column_name => value hash of the original primary
40
+ # key of the updated record
41
+ attr_accessor :new_key
42
+
43
+ # Creates a new LoggedChange instance.
44
+ # * +loader+: the current LoggedChangeLoader
45
+ # * +database+: either :+left+ or :+right+
46
+ def initialize(loader)
47
+ self.loader = loader
48
+ self.type = :no_change
49
+ end
50
+
51
+ # A hash describing how the change state morph based on newly found change
52
+ # records.
53
+ # * key: String consisting of 2 letters
54
+ # * first letter: describes current type change (nothing, insert, update, delete)
55
+ # * second letter: the new change type as read of the change log table
56
+ # * value:
57
+ # The resulting change type.
58
+ # [1]: such cases shouldn't happen. but just in case, choose the most
59
+ # sensible solution.
60
+ TYPE_CHANGES = {
61
+ 'NI' => 'I',
62
+ 'NU' => 'U',
63
+ 'ND' => 'D',
64
+ 'II' => 'I', # [1]
65
+ 'IU' => 'I',
66
+ 'ID' => 'N',
67
+ 'UI' => 'U', # [1]
68
+ 'UU' => 'U',
69
+ 'UD' => 'D',
70
+ 'DI' => 'U',
71
+ 'DU' => 'U', # [1]
72
+ 'DD' => 'D', # [1]
73
+ }
74
+
75
+ # A hash translating the short 1-letter types to the according symbols
76
+ SHORT_TYPES = {
77
+ 'I' => :insert,
78
+ 'U' => :update,
79
+ 'D' => :delete,
80
+ 'N' => :no_change
81
+ }
82
+ # A hash translating the symbold types to according 1 letter types
83
+ LONG_TYPES = SHORT_TYPES.invert
84
+
85
+ # Returns the configured key separator
86
+ def key_sep
87
+ @key_sep ||= session.configuration.options[:key_sep]
88
+ end
89
+
90
+ # Returns a column_name => value hash based on the provided +raw_key+ string
91
+ # (which is a string in the format as read directly from the change log table).
92
+ def key_to_hash(raw_key)
93
+ result = {}
94
+ #raw_key.split(key_sep).each_slice(2) {|a| result[a[0]] = a[1]}
95
+ raw_key.split(key_sep).each_slice(2) {|field_name, value| result[field_name] = value}
96
+ result
97
+ end
98
+
99
+ # Loads the change as per #table and #key. Works if the LoggedChange instance
100
+ # is totally new or was already loaded before.
101
+ def load
102
+ current_type = LONG_TYPES[type]
103
+
104
+ org_key = new_key || key
105
+ # change to key string as can be found in change log table
106
+ org_key = session.send(database).primary_key_names(table).map do |key_name|
107
+ "#{key_name}#{key_sep}#{org_key[key_name]}"
108
+ end.join(key_sep)
109
+ current_key = org_key
110
+
111
+ while change = loader.load(table, current_key)
112
+
113
+ new_type = change['change_type']
114
+ current_type = TYPE_CHANGES["#{current_type}#{new_type}"]
115
+
116
+ self.first_changed_at ||= change['change_time']
117
+ self.last_changed_at = change['change_time']
118
+
119
+ if change['change_type'] == 'U' and change['change_new_key'] != current_key
120
+ current_key = change['change_new_key']
121
+ end
122
+ end
123
+
124
+ self.type = SHORT_TYPES[current_type]
125
+ self.new_key = nil
126
+ if type == :update
127
+ self.key ||= key_to_hash(org_key)
128
+ self.new_key = key_to_hash(current_key)
129
+ else
130
+ self.key = key_to_hash(current_key)
131
+ end
132
+ end
133
+
134
+ # Loads the change with the specified key for the named +table+.
135
+ # * +table+: name of the table
136
+ # * +key+: a column_name => value hash for all primary key columns of the table
137
+ def load_specified(table, key)
138
+ self.table = table
139
+ self.key = key
140
+ load
141
+ end
142
+
143
+ # Loads the oldest available change
144
+ def load_oldest
145
+ begin
146
+ change = loader.oldest_change
147
+ break unless change
148
+ self.key = key_to_hash(change['change_key'])
149
+ self.table = change['change_table']
150
+ load
151
+ end until type != :no_change
152
+ end
153
+
154
+ # Prevents session from going into YAML output
155
+ def to_yaml_properties
156
+ instance_variables.sort.reject {|var_name| ['@session', '@loader'].include? var_name}
157
+ end
158
+
159
+ end
160
+ end