rubyrep 1.0.0

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