rubyrep 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. data/History.txt +4 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +137 -0
  4. data/README.txt +37 -0
  5. data/Rakefile +30 -0
  6. data/bin/rubyrep +8 -0
  7. data/config/hoe.rb +72 -0
  8. data/config/mysql_config.rb +25 -0
  9. data/config/postgres_config.rb +21 -0
  10. data/config/proxied_test_config.rb +14 -0
  11. data/config/redmine_config.rb +17 -0
  12. data/config/rep_config.rb +20 -0
  13. data/config/requirements.rb +32 -0
  14. data/config/test_config.rb +20 -0
  15. data/lib/rubyrep/base_runner.rb +195 -0
  16. data/lib/rubyrep/command_runner.rb +144 -0
  17. data/lib/rubyrep/committers/buffered_committer.rb +140 -0
  18. data/lib/rubyrep/committers/committers.rb +146 -0
  19. data/lib/rubyrep/configuration.rb +240 -0
  20. data/lib/rubyrep/connection_extenders/connection_extenders.rb +133 -0
  21. data/lib/rubyrep/connection_extenders/jdbc_extender.rb +284 -0
  22. data/lib/rubyrep/connection_extenders/mysql_extender.rb +168 -0
  23. data/lib/rubyrep/connection_extenders/postgresql_extender.rb +261 -0
  24. data/lib/rubyrep/database_proxy.rb +52 -0
  25. data/lib/rubyrep/direct_table_scan.rb +75 -0
  26. data/lib/rubyrep/generate_runner.rb +105 -0
  27. data/lib/rubyrep/initializer.rb +39 -0
  28. data/lib/rubyrep/logged_change.rb +326 -0
  29. data/lib/rubyrep/proxied_table_scan.rb +171 -0
  30. data/lib/rubyrep/proxy_block_cursor.rb +145 -0
  31. data/lib/rubyrep/proxy_connection.rb +318 -0
  32. data/lib/rubyrep/proxy_cursor.rb +44 -0
  33. data/lib/rubyrep/proxy_row_cursor.rb +43 -0
  34. data/lib/rubyrep/proxy_runner.rb +89 -0
  35. data/lib/rubyrep/replication_difference.rb +91 -0
  36. data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
  37. data/lib/rubyrep/replication_extenders/postgresql_replication.rb +204 -0
  38. data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
  39. data/lib/rubyrep/replication_helper.rb +104 -0
  40. data/lib/rubyrep/replication_initializer.rb +307 -0
  41. data/lib/rubyrep/replication_run.rb +48 -0
  42. data/lib/rubyrep/replication_runner.rb +138 -0
  43. data/lib/rubyrep/replicators/replicators.rb +37 -0
  44. data/lib/rubyrep/replicators/two_way_replicator.rb +334 -0
  45. data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
  46. data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
  47. data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
  48. data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
  49. data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
  50. data/lib/rubyrep/scan_runner.rb +25 -0
  51. data/lib/rubyrep/session.rb +177 -0
  52. data/lib/rubyrep/sync_helper.rb +111 -0
  53. data/lib/rubyrep/sync_runner.rb +31 -0
  54. data/lib/rubyrep/syncers/syncers.rb +112 -0
  55. data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
  56. data/lib/rubyrep/table_scan.rb +54 -0
  57. data/lib/rubyrep/table_scan_helper.rb +38 -0
  58. data/lib/rubyrep/table_sorter.rb +70 -0
  59. data/lib/rubyrep/table_spec_resolver.rb +136 -0
  60. data/lib/rubyrep/table_sync.rb +68 -0
  61. data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
  62. data/lib/rubyrep/type_casting_cursor.rb +31 -0
  63. data/lib/rubyrep/uninstall_runner.rb +92 -0
  64. data/lib/rubyrep/version.rb +9 -0
  65. data/lib/rubyrep.rb +68 -0
  66. data/script/destroy +14 -0
  67. data/script/generate +14 -0
  68. data/script/txt2html +74 -0
  69. data/setup.rb +1585 -0
  70. data/sims/performance/big_rep_spec.rb +100 -0
  71. data/sims/performance/big_scan_spec.rb +57 -0
  72. data/sims/performance/big_sync_spec.rb +141 -0
  73. data/sims/performance/performance.rake +228 -0
  74. data/sims/sim_helper.rb +24 -0
  75. data/spec/base_runner_spec.rb +218 -0
  76. data/spec/buffered_committer_spec.rb +271 -0
  77. data/spec/command_runner_spec.rb +145 -0
  78. data/spec/committers_spec.rb +174 -0
  79. data/spec/configuration_spec.rb +198 -0
  80. data/spec/connection_extender_interface_spec.rb +138 -0
  81. data/spec/connection_extenders_registration_spec.rb +129 -0
  82. data/spec/database_proxy_spec.rb +48 -0
  83. data/spec/database_rake_spec.rb +40 -0
  84. data/spec/db_specific_connection_extenders_spec.rb +34 -0
  85. data/spec/db_specific_replication_extenders_spec.rb +38 -0
  86. data/spec/direct_table_scan_spec.rb +61 -0
  87. data/spec/generate_runner_spec.rb +84 -0
  88. data/spec/initializer_spec.rb +46 -0
  89. data/spec/logged_change_spec.rb +480 -0
  90. data/spec/postgresql_replication_spec.rb +48 -0
  91. data/spec/postgresql_support_spec.rb +57 -0
  92. data/spec/progress_bar_spec.rb +77 -0
  93. data/spec/proxied_table_scan_spec.rb +151 -0
  94. data/spec/proxy_block_cursor_spec.rb +197 -0
  95. data/spec/proxy_connection_spec.rb +399 -0
  96. data/spec/proxy_cursor_spec.rb +56 -0
  97. data/spec/proxy_row_cursor_spec.rb +66 -0
  98. data/spec/proxy_runner_spec.rb +70 -0
  99. data/spec/replication_difference_spec.rb +160 -0
  100. data/spec/replication_extender_interface_spec.rb +365 -0
  101. data/spec/replication_extenders_spec.rb +32 -0
  102. data/spec/replication_helper_spec.rb +121 -0
  103. data/spec/replication_initializer_spec.rb +477 -0
  104. data/spec/replication_run_spec.rb +166 -0
  105. data/spec/replication_runner_spec.rb +213 -0
  106. data/spec/replicators_spec.rb +31 -0
  107. data/spec/rubyrep_spec.rb +8 -0
  108. data/spec/scan_detail_reporter_spec.rb +119 -0
  109. data/spec/scan_progress_printers_spec.rb +68 -0
  110. data/spec/scan_report_printers_spec.rb +67 -0
  111. data/spec/scan_runner_spec.rb +50 -0
  112. data/spec/scan_summary_reporter_spec.rb +61 -0
  113. data/spec/session_spec.rb +212 -0
  114. data/spec/spec.opts +1 -0
  115. data/spec/spec_helper.rb +295 -0
  116. data/spec/sync_helper_spec.rb +157 -0
  117. data/spec/sync_runner_spec.rb +78 -0
  118. data/spec/syncers_spec.rb +171 -0
  119. data/spec/table_scan_helper_spec.rb +29 -0
  120. data/spec/table_scan_spec.rb +49 -0
  121. data/spec/table_sorter_spec.rb +31 -0
  122. data/spec/table_spec_resolver_spec.rb +102 -0
  123. data/spec/table_sync_spec.rb +84 -0
  124. data/spec/trigger_mode_switcher_spec.rb +83 -0
  125. data/spec/two_way_replicator_spec.rb +551 -0
  126. data/spec/two_way_syncer_spec.rb +256 -0
  127. data/spec/type_casting_cursor_spec.rb +50 -0
  128. data/spec/uninstall_runner_spec.rb +86 -0
  129. data/tasks/database.rake +439 -0
  130. data/tasks/deployment.rake +29 -0
  131. data/tasks/environment.rake +9 -0
  132. data/tasks/java.rake +37 -0
  133. data/tasks/redmine_test.rake +47 -0
  134. data/tasks/rspec.rake +68 -0
  135. data/tasks/rubyrep.tailor +18 -0
  136. data/tasks/stats.rake +19 -0
  137. data/tasks/task_helper.rb +20 -0
  138. data.tar.gz.sig +0 -0
  139. metadata +243 -0
  140. metadata.gz.sig +0 -0
@@ -0,0 +1,318 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
2
+
3
+ require 'drb'
4
+
5
+ require 'rubyrep'
6
+ require 'forwardable'
7
+
8
+ require 'active_record/connection_adapters/abstract_adapter'
9
+
10
+ module RR
11
+
12
+ # This class represents a remote activerecord database connection.
13
+ # Normally created by DatabaseProxy
14
+ class ProxyConnection
15
+ extend Forwardable
16
+
17
+ # The database connection
18
+ attr_accessor :connection
19
+
20
+ # A hash as described by ActiveRecord::Base#establish_connection
21
+ attr_accessor :config
22
+
23
+ # Forward certain methods to the proxied database connection
24
+ def_delegators :connection,
25
+ :columns, :quote_column_name,
26
+ :quote_table_name, :execute,
27
+ :select_one, :select_all, :tables,
28
+ :begin_db_transaction, :rollback_db_transaction, :commit_db_transaction,
29
+ :referenced_tables,
30
+ :create_or_replace_replication_trigger_function,
31
+ :create_replication_trigger, :drop_replication_trigger, :replication_trigger_exists?,
32
+ :sequence_values, :update_sequences, :clear_sequence_setup,
33
+ :create_table, :drop_table, :add_big_primary_key
34
+
35
+ # Caching the primary keys. This is a hash with
36
+ # * key: table name
37
+ # * value: array of primary key names
38
+ attr_accessor :primary_key_names_cache
39
+
40
+ # Hash to register cursors.
41
+ # Purpose:
42
+ # Objects only referenced remotely via DRb can be garbage collected.
43
+ # We register them in this hash to protect them from unintended garbage collection.
44
+ attr_accessor :cursors
45
+
46
+ # 2-level Hash of table_name => column_name => Column objects.
47
+ attr_accessor :table_columns
48
+
49
+ # Hash of table_name => array of column names pairs.
50
+ attr_accessor :table_column_names
51
+
52
+ # A hash of manually overwritten primary keys:
53
+ # * key: table_name
54
+ # * value: array of primary key names
55
+ attr_accessor :manual_primary_keys
56
+
57
+ # Returns an array of primary key names for the given +table_name+.
58
+ # Caches the result for future calls. Allows manual overwrites through
59
+ # the Configuration options +:primary_key_names+ or :+primary_key_only_limit+.
60
+ #
61
+ # Parameters:
62
+ # * +table_name+: name of the table
63
+ # * +options+: An option hash with the following valid options:
64
+ # * :+raw+: if +true+, than don't use manual overwrites and don't cache
65
+ def primary_key_names(table_name, options = {})
66
+ return connection.primary_key_names(table_name) if options[:raw]
67
+
68
+ self.primary_key_names_cache ||= {}
69
+ result = primary_key_names_cache[table_name]
70
+ unless result
71
+ result = manual_primary_keys[table_name] || connection.primary_key_names(table_name)
72
+ primary_key_names_cache[table_name] = result
73
+ end
74
+ result
75
+ end
76
+
77
+ # Returns a Hash of currently registerred cursors
78
+ def cursors
79
+ @cursors ||= {}
80
+ end
81
+
82
+ # Store a cursor in the register to protect it from the garbage collector.
83
+ def save_cursor(cursor)
84
+ cursors[cursor] = cursor
85
+ end
86
+
87
+ # default number of records that are read into memory at a time during
88
+ # select queries
89
+ DEFAULT_ROW_BUFFER_SIZE = 1000
90
+
91
+ # Returns a cusor as produced by the #select_cursor method of the connection
92
+ # extenders.
93
+ #
94
+ # Two modes of operation: Either
95
+ # * execute the specified query (takes precedense) OR
96
+ # * first build the query based on options forwarded to #table_select_query
97
+ # +options+ is a hash with
98
+ # * :+query+: executes the given query
99
+ # * :+type_cast+: if +true+, build a type casting cursor around the result
100
+ # * :+table+: name of the table from which to read data
101
+ # * further options as taken by #table_select_query to build the query
102
+ # * :+row_buffer_size+:
103
+ # Integer controlling how many rows a read into memory at one time.
104
+ def select_cursor(options)
105
+ row_buffer_size = options[:row_buffer_size] || DEFAULT_ROW_BUFFER_SIZE
106
+ query = options[:query] || table_select_query(options[:table], options)
107
+ cursor = connection.select_cursor query, row_buffer_size
108
+ if options[:type_cast]
109
+ cursor = TypeCastingCursor.new(self, options[:table], cursor)
110
+ end
111
+ cursor
112
+ end
113
+
114
+ # Create a session on the proxy side according to provided configuration hash.
115
+ # +config+ is a hash as described by ActiveRecord::Base#establish_connection
116
+ def initialize(config)
117
+ self.connection = ConnectionExtenders.db_connect config
118
+ self.config = config
119
+ self.manual_primary_keys = {}
120
+ end
121
+
122
+ # Checks if the connection is still active and if not, reestablished it.
123
+ def refresh
124
+ unless self.connection.active?
125
+ self.connection = ConnectionExtenders.db_connect config
126
+ end
127
+ end
128
+
129
+ # Destroys the session
130
+ def destroy
131
+ self.connection.disconnect!
132
+
133
+ cursors.each_key do |cursor|
134
+ cursor.destroy
135
+ end
136
+ cursors.clear
137
+ end
138
+
139
+ # Quotes the given value. It is assumed that the value belongs to the specified column name and table name.
140
+ # Caches the column objects for higher speed.
141
+ def quote_value(table, column, value)
142
+ self.table_columns ||= {}
143
+ unless table_columns.include? table
144
+ table_columns[table] = {}
145
+ columns(table).each {|c| table_columns[table][c.name] = c}
146
+ end
147
+ connection.quote value, table_columns[table][column]
148
+ end
149
+
150
+ # Create a cursor for the given table.
151
+ # * +cursor_class+: should specify the Cursor class (e. g. ProxyBlockCursor or ProxyRowCursor).
152
+ # * +table+: name of the table
153
+ # * +options+: An option hash that is used to construct the SQL query. See ProxyCursor#construct_query for details.
154
+ def create_cursor(cursor_class, table, options = {})
155
+ cursor = cursor_class.new self, table
156
+ cursor.prepare_fetch options
157
+ save_cursor cursor
158
+ cursor
159
+ end
160
+
161
+ # Destroys the provided cursor and removes it from the register
162
+ def destroy_cursor(cursor)
163
+ cursor.destroy
164
+ cursors.delete cursor
165
+ end
166
+
167
+ # Returns an array of column names of the given table name.
168
+ # The array is ordered in the sequence as returned by the database.
169
+ # The result is cached for higher speed.
170
+ def column_names(table)
171
+ self.table_column_names ||= {}
172
+ unless table_column_names.include? table
173
+ table_column_names[table] = columns(table).map {|c| c.name}
174
+ end
175
+ table_column_names[table]
176
+ end
177
+
178
+ # Returns a list of quoted column names for the given +table+ as comma
179
+ # separated string.
180
+ def quote_column_list(table)
181
+ column_names(table).map do |column_name|
182
+ quote_column_name(column_name)
183
+ end.join(', ')
184
+ end
185
+ private :quote_column_list
186
+
187
+ # Returns a list of quoted primary key names for the given +table+ as comma
188
+ # separated string.
189
+ def quote_key_list(table)
190
+ primary_key_names(table).map do |column_name|
191
+ quote_column_name(column_name)
192
+ end.join(', ')
193
+ end
194
+ private :quote_key_list
195
+
196
+
197
+ # Generates an sql condition string for the given +table+ based on
198
+ # * +row+: a hash of primary key => value pairs designating the target row
199
+ # * +condition+: the type of sql condition (something like '>=' or '=', etc.)
200
+ def row_condition(table, row, condition)
201
+ query_part = ""
202
+ query_part << ' (' << quote_key_list(table) << ') ' << condition
203
+ query_part << ' (' << primary_key_names(table).map do |key|
204
+ quote_value(table, key, row[key])
205
+ end.join(', ') << ')'
206
+ query_part
207
+ end
208
+ private :row_condition
209
+
210
+ # Returns an SQL query string for the given +table+ based on the provided +options+.
211
+ # +options+ is a hash that can contain any of the following:
212
+ # * :+from+: nil OR the hash of primary key => value pairs designating the start of the selection
213
+ # * :+exclude_starting_row+: if true, do not include the row specified by :+from+
214
+ # * :+to+: nil OR the hash of primary key => value pairs designating the end of the selection
215
+ # * :+row_keys+: an array of primary key => value hashes specify the target rows.
216
+ def table_select_query(table, options = {})
217
+ query = "select #{quote_column_list(table)}"
218
+ query << " from #{quote_table_name(table)}"
219
+ query << " where" if [:from, :to, :row_keys].any? {|key| options.include? key}
220
+ first_condition = true
221
+ if options[:from]
222
+ first_condition = false
223
+ matching_condition = options[:exclude_starting_row] ? '>' : '>='
224
+ query << row_condition(table, options[:from], matching_condition)
225
+ end
226
+ if options[:to]
227
+ query << ' and' unless first_condition
228
+ first_condition = false
229
+ query << row_condition(table, options[:to], '<=')
230
+ end
231
+ if options[:row_keys]
232
+ query << ' and' unless first_condition
233
+ if options[:row_keys].empty?
234
+ query << ' false'
235
+ else
236
+ query << ' (' << quote_key_list(table) << ') in ('
237
+ first_key = true
238
+ options[:row_keys].each do |row|
239
+ query << ', ' unless first_key
240
+ first_key = false
241
+ query << '(' << primary_key_names(table).map do |key|
242
+ quote_value(table, key, row[key])
243
+ end.join(', ') << ')'
244
+ end
245
+ query << ')'
246
+ end
247
+ end
248
+ query << " order by #{quote_key_list(table)}"
249
+
250
+ query
251
+ end
252
+
253
+ # Returns an SQL insert query for the given +table+ and +values+.
254
+ # +values+ is a hash of column_name => value pairs.
255
+ def table_insert_query(table, values)
256
+ query = "insert into #{quote_table_name(table)}"
257
+ query << '(' << values.keys.map do |column_name|
258
+ quote_column_name(column_name)
259
+ end.join(', ') << ') '
260
+ query << 'values(' << values.map do |column_name, value|
261
+ quote_value(table, column_name, value)
262
+ end.join(', ') << ')'
263
+ query
264
+ end
265
+
266
+ # Inserts the specified records into the named +table+.
267
+ # +values+ is a hash of column_name => value pairs.
268
+ def insert_record(table, values)
269
+ execute table_insert_query(table, values)
270
+ end
271
+
272
+ # Returns an SQL update query.
273
+ # * +table+: name of the target table
274
+ # * +values+: a hash of column_name => value pairs
275
+ # * +org_key+:
276
+ # A hash of column_name => value pairs. If +nil+, use the key specified by
277
+ # +values+ instead.
278
+ def table_update_query(table, values, org_key = nil)
279
+ org_key ||= values
280
+ query = "update #{quote_table_name(table)} set "
281
+ query << values.map do |column_name, value|
282
+ "#{quote_column_name(column_name)} = #{quote_value(table, column_name, value)}"
283
+ end.join(', ')
284
+ query << " where (" << quote_key_list(table) << ") = ("
285
+ query << primary_key_names(table).map do |key|
286
+ quote_value(table, key, org_key[key])
287
+ end.join(', ') << ")"
288
+ end
289
+
290
+ # Updates the specified records of the specified table.
291
+ # * +table+: name of the target table
292
+ # * +values+: a hash of column_name => value pairs.
293
+ # * +org_key+:
294
+ # A hash of column_name => value pairs. If +nil+, use the key specified by
295
+ # +values+ instead.
296
+ def update_record(table, values, org_key = nil)
297
+ execute table_update_query(table, values, org_key)
298
+ end
299
+
300
+ # Returns an SQL delete query for the given +table+ and +values+
301
+ # +values+ is a hash of column_name => value pairs. (Only the primary key
302
+ # values will be used and must be included in the hash.)
303
+ def table_delete_query(table, values)
304
+ query = "delete from #{quote_table_name(table)}"
305
+ query << " where (" << quote_key_list(table) << ") = ("
306
+ query << primary_key_names(table).map do |key|
307
+ quote_value(table, key, values[key])
308
+ end.join(', ') << ")"
309
+ end
310
+
311
+ # Deletes the specified record from the named +table+.
312
+ # +values+ is a hash of column_name => value pairs. (Only the primary key
313
+ # values will be used and must be included in the hash.)
314
+ def delete_record(table, values)
315
+ execute table_delete_query(table, values)
316
+ end
317
+ end
318
+ end
@@ -0,0 +1,44 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
2
+
3
+ require 'rubyrep'
4
+
5
+ module RR
6
+ # Provides shared functionality for ProxyRowCursor and ProxyBlockCursor
7
+ class ProxyCursor
8
+
9
+ # The current ProxyConnection.
10
+ attr_accessor :connection
11
+
12
+ # The name of the current table.
13
+ attr_accessor :table
14
+
15
+ # Array of primary key names for current table.
16
+ attr_accessor :primary_key_names
17
+
18
+ # The current cursor.
19
+ attr_accessor :cursor
20
+
21
+ # Shared initializations
22
+ # * connection: the current proxy connection
23
+ # * table: table_name
24
+ def initialize(connection, table)
25
+ self.connection = connection
26
+ self.table = table
27
+ self.primary_key_names = connection.primary_key_names table
28
+ end
29
+
30
+ # Initiate a query for the specified row range.
31
+ # +options+: An option hash that is used to construct the SQL query. See ProxyCursor#construct_query for details.
32
+ def prepare_fetch(options = {})
33
+ self.cursor = connection.select_cursor(
34
+ options.merge(:table => table, :type_cast => true)
35
+ )
36
+ end
37
+
38
+ # Releases all ressources
39
+ def destroy
40
+ self.cursor.clear if self.cursor
41
+ self.cursor = nil
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,43 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
2
+
3
+ require 'digest/sha1'
4
+
5
+ require 'rubyrep'
6
+
7
+ module RR
8
+
9
+ # This class is used to scan a given table range
10
+ # Can return rows either themselves or only their checksum
11
+ class ProxyRowCursor < ProxyCursor
12
+
13
+ # The column_name => value hash of the current row.
14
+ attr_accessor :current_row
15
+
16
+ # Creates a new cursor
17
+ # * session: the current proxy session
18
+ # * table: table_name
19
+ def initialize(session, table)
20
+ super
21
+ end
22
+
23
+ # Returns true if there are unprocessed rows in the table range
24
+ def next?
25
+ cursor.next?
26
+ end
27
+
28
+ # Returns the next row in cursor
29
+ def next_row
30
+ cursor.next_row
31
+ end
32
+
33
+ # Returns for the next row
34
+ # * a hash of :column_name => value pairs of the primary keys
35
+ # * checksum string for that row
36
+ def next_row_keys_and_checksum
37
+ self.current_row = cursor.next_row
38
+ keys = self.current_row.reject {|key, | not primary_key_names.include? key}
39
+ checksum = Digest::SHA1.hexdigest(Marshal.dump(self.current_row))
40
+ return keys, checksum
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,89 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+
3
+ require 'optparse'
4
+ require 'drb'
5
+
6
+ module RR
7
+ # This class implements the functionality of the rrproxy.rb command.
8
+ class ProxyRunner
9
+
10
+ CommandRunner.register 'proxy' => {
11
+ :command => self,
12
+ :description => 'Proxies connections from rubyrep commands to the database'
13
+ }
14
+
15
+ # Default options to start a DatabaseProxy server
16
+ DEFAULT_OPTIONS = {
17
+ :port => DatabaseProxy::DEFAULT_PORT,
18
+ :host => ''
19
+ }
20
+
21
+ # Parses the given command line parameter array.
22
+ # Returns
23
+ # * the options hash or nil if command line parsing failed
24
+ # * status (as per UNIX conventions: 1 if parameters were invalid, 0 otherwise)
25
+ def get_options(args)
26
+ options = DEFAULT_OPTIONS
27
+ status = 0
28
+
29
+ parser = OptionParser.new do |opts|
30
+ opts.banner = "Usage: #{$0} proxy [options]"
31
+ opts.separator ""
32
+ opts.separator "Specific options:"
33
+
34
+ opts.on("-h","--host", "=IP_ADDRESS", "IP address to listen on. Default: binds to all IP addresses of the computer") do |arg|
35
+ options[:host] = arg
36
+ end
37
+
38
+ opts.on("-p","--port", "=PORT_NUMBER", Integer, "TCP port to listen on. Default port: #{DatabaseProxy::DEFAULT_PORT}") do |arg|
39
+ options[:port] = arg
40
+ end
41
+
42
+ opts.on_tail("--help", "Show this message") do
43
+ $stderr.puts opts
44
+ options = nil
45
+ end
46
+ end
47
+
48
+ begin
49
+ parser.parse!(args)
50
+ rescue Exception => e
51
+ $stderr.puts "Command line parsing failed: #{e}"
52
+ $stderr.puts parser.help
53
+ options = nil
54
+ status = 1
55
+ end
56
+
57
+ return options, status
58
+ end
59
+
60
+ # Builds the druby URL from the given options and returns it
61
+ def build_url(options)
62
+ "druby://#{options[:host]}:#{options[:port]}"
63
+ end
64
+
65
+ # Starts a proxy server under the given druby URL
66
+ def start_server(url)
67
+ proxy = DatabaseProxy.new
68
+
69
+ DRb.start_service(url, proxy)
70
+ DRb.thread.join
71
+ end
72
+
73
+ # Runs the ProxyRunner (processing of command line & starting of server)
74
+ # args: the array of command line options with which to start the server
75
+ def self.run(args)
76
+ runner = ProxyRunner.new
77
+
78
+ options, status = runner.get_options(args)
79
+ if options
80
+ url = runner.build_url(options)
81
+ runner.start_server(url)
82
+ end
83
+ status
84
+ end
85
+
86
+ end
87
+ end
88
+
89
+
@@ -0,0 +1,91 @@
1
+ module RR
2
+
3
+ # Describes a (record specific) difference between both databases as identifed
4
+ # via change log.
5
+ class ReplicationDifference
6
+
7
+ # The current Session.
8
+ attr_accessor :session
9
+
10
+ # The type of the difference. Either
11
+ # * :+left+: change in left database
12
+ # * :+right+: change in right database
13
+ # * :+conflict+: change in both databases
14
+ # * :+no_diff+: changes in both databases constitute no difference
15
+ attr_accessor :type
16
+
17
+ # A hash with keys :+left+ and / or :+right+.
18
+ # Hash values are LoggedChange instances.
19
+ def changes
20
+ @changes ||= {}
21
+ end
22
+
23
+ # Creates a new ReplicationDifference instance.
24
+ # +session+ is the current Session.
25
+ def initialize(session)
26
+ self.session = session
27
+ end
28
+
29
+ # Should be set to +true+ if this ReplicationDifference instance was
30
+ # successfully loaded.
31
+ attr_writer :loaded
32
+
33
+ # Returns +true+ if a replication difference was loaded
34
+ def loaded?
35
+ @loaded
36
+ end
37
+
38
+ # Shortcut to calculate the "other" database.
39
+ OTHER_SIDE = {
40
+ :left => :right,
41
+ :right => :left
42
+ }
43
+
44
+ # Resulting diff type based on types of left changes (outer hash) and right
45
+ # changes (inner hash)
46
+ DIFF_TYPES = {
47
+ :insert => {:insert => :conflict, :update => :conflict, :delete => :conflict, :no_change => :left},
48
+ :update => {:insert => :conflict, :update => :conflict, :delete => :conflict, :no_change => :left},
49
+ :delete => {:insert => :conflict, :update => :conflict, :delete => :no_change, :no_change => :left},
50
+ :no_change => {:insert => :right, :update => :right, :delete => :right, :no_change => :no_change}
51
+ }
52
+
53
+ # Amends a difference according to new entries in the change log table
54
+ def amend
55
+ session.reload_changes
56
+ changes[:left].load
57
+ changes[:right].load
58
+ self.type = DIFF_TYPES[changes[:left].type][changes[:right].type]
59
+ end
60
+
61
+ # Loads a difference
62
+ def load
63
+ change_times = {}
64
+ [:left, :right].each do |database|
65
+ changes[database] = LoggedChange.new session, database
66
+ change_times[database] = changes[database].oldest_change_time
67
+ end
68
+ return if change_times[:left] == nil and change_times[:right] == nil
69
+
70
+ oldest = nil
71
+ [:left, :right].each do |database|
72
+ oldest = OTHER_SIDE[database] if change_times[database] == nil
73
+ end
74
+ oldest ||= change_times[:left] <= change_times[:right] ? :left : :right
75
+ changes[oldest].load_oldest
76
+
77
+ changes[OTHER_SIDE[oldest]].load_specified(
78
+ session.corresponding_table(oldest, changes[oldest].table),
79
+ changes[oldest].key)
80
+
81
+ self.type = DIFF_TYPES[changes[:left].type][changes[:right].type]
82
+ self.loaded = true
83
+ end
84
+
85
+ # Prevents session from going into YAML output
86
+ def to_yaml_properties
87
+ instance_variables.sort.reject {|var_name| var_name == '@session'}
88
+ end
89
+
90
+ end
91
+ end