rubyrep 1.0.7 → 1.0.8

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.
data/History.txt CHANGED
@@ -1,3 +1,11 @@
1
+ == 1.0.8 2009-10-01
2
+
3
+ * Feature: replication more robust against connection failures
4
+ * Feature: initial syncs before replication can be disabled with :initial_sync option
5
+ * Bug fix: better detection of failed update and delete replications
6
+ * Bug fix: fix scenario where replication could be logged as successful even when failed
7
+ * Bug fix: make proxied replication work under JRuby
8
+
1
9
  == 1.0.7 2009-07-26
2
10
 
3
11
  * Bug fix: buffered LoggedChangeLoader#update to avoid timeouts with large replication backlogs.
data/Manifest.txt CHANGED
@@ -67,6 +67,8 @@ lib/rubyrep/trigger_mode_switcher.rb
67
67
  lib/rubyrep/type_casting_cursor.rb
68
68
  lib/rubyrep/uninstall_runner.rb
69
69
  lib/rubyrep/version.rb
70
+ rubyrep
71
+ rubyrep.bat
70
72
  script/destroy
71
73
  script/generate
72
74
  script/txt2html
@@ -89,6 +89,12 @@ module RR
89
89
  end
90
90
  end
91
91
 
92
+ # Returns +true+ if a new transaction was started since the last
93
+ # insert / update / delete.
94
+ def new_transaction?
95
+ @change_counter == 0
96
+ end
97
+
92
98
  # A new committer is created for each table sync.
93
99
  # * session: a Session object representing the current database session
94
100
  def initialize(session)
@@ -68,6 +68,12 @@ module RR
68
68
  self.session = session
69
69
  self.connections = {:left => session.left, :right => session.right}
70
70
  end
71
+
72
+ # Returns +true+ if a new transaction was started since the last
73
+ # insert / update / delete.
74
+ def new_transaction?
75
+ false
76
+ end
71
77
 
72
78
  # Inserts the specified record in the specified +database+ (either :left or :right).
73
79
  # +table+ is the name of the target table.
@@ -30,6 +30,7 @@ module RR
30
30
  :table_ordering => true,
31
31
  :scan_progress_printer => :progress_bar,
32
32
  :use_ansi => true_if_running_in_a_terminal_and_not_under_windows,
33
+ :initial_sync => true,
33
34
  :adjust_sequences => true,
34
35
  :sequence_adjustment_buffer => 0,
35
36
  :sequence_increment => 2,
@@ -98,8 +99,13 @@ module RR
98
99
  # used for the initial sync of a table.)
99
100
  # If no +:syncer+ option is specified, than a syncer as named by this
100
101
  # option is used.
102
+ # * :+initial_sync+:
103
+ # If +true+, syncs a table when initializing replication.
104
+ # Disable with care!
105
+ # (I. e. ensure that the table(s) have indeed same data in both databases
106
+ # before starting replication.)
101
107
  # * :+adjust_sequences+:
102
- # If true, adjust sequences to avoid number conflicts between left and
108
+ # If +true+, adjust sequences to avoid number conflicts between left and
103
109
  # right database during replication.
104
110
  # * :+sequence_adjustement_buffer+:
105
111
  # When updating a sequence, this is the additional gap to avoid sequence
@@ -93,6 +93,10 @@ module RR
93
93
  # This class represents a remote activerecord database connection.
94
94
  # Normally created by DatabaseProxy
95
95
  class ProxyConnection
96
+ # Ensure that the proxy object always stays on server side and only remote
97
+ # references are returned to the client.
98
+ include DRbUndumped
99
+
96
100
  extend Forwardable
97
101
 
98
102
  # The database connection
@@ -105,14 +109,14 @@ module RR
105
109
  def_delegators :connection,
106
110
  :columns, :quote_column_name,
107
111
  :quote_table_name, :execute,
108
- :select_one, :select_all, :tables,
112
+ :select_one, :select_all, :tables, :update, :delete,
109
113
  :begin_db_transaction, :rollback_db_transaction, :commit_db_transaction,
110
114
  :referenced_tables,
111
115
  :create_or_replace_replication_trigger_function,
112
116
  :create_replication_trigger, :drop_replication_trigger, :replication_trigger_exists?,
113
117
  :sequence_values, :update_sequences, :clear_sequence_setup,
114
- :create_table, :drop_table, :add_big_primary_key
115
-
118
+ :drop_table, :add_big_primary_key, :add_column, :remove_column
119
+
116
120
  # Caching the primary keys. This is a hash with
117
121
  # * key: table name
118
122
  # * value: array of primary key names
@@ -155,6 +159,13 @@ module RR
155
159
  result
156
160
  end
157
161
 
162
+ # Creates a table
163
+ # Call forwarded to ActiveRecord::ConnectionAdapters::SchemaStatements#create_table
164
+ # Provides an empty block (to prevent DRB from calling back the client)
165
+ def create_table(*params)
166
+ connection.create_table(*params) {}
167
+ end
168
+
158
169
  # Returns a Hash of currently registerred cursors
159
170
  def cursors
160
171
  @cursors ||= {}
@@ -361,8 +372,9 @@ module RR
361
372
  # * +org_key+:
362
373
  # A hash of column_name => value pairs. If +nil+, use the key specified by
363
374
  # +values+ instead.
375
+ # Returns the number of modified records.
364
376
  def update_record(table, values, org_key = nil)
365
- execute table_update_query(table, values, org_key)
377
+ update table_update_query(table, values, org_key)
366
378
  end
367
379
 
368
380
  # Returns an SQL delete query for the given +table+ and +values+
@@ -379,8 +391,9 @@ module RR
379
391
  # Deletes the specified record from the named +table+.
380
392
  # +values+ is a hash of column_name => value pairs. (Only the primary key
381
393
  # values will be used and must be included in the hash.)
394
+ # Returns the number of deleted records.
382
395
  def delete_record(table, values)
383
- execute table_delete_query(table, values)
396
+ delete table_delete_query(table, values)
384
397
  end
385
398
  end
386
399
  end
@@ -19,6 +19,12 @@ module RR
19
19
  # Delegates to Session#corresponding_table
20
20
  def corresponding_table(db_arm, table); session.corresponding_table(db_arm, table); end
21
21
 
22
+ # Returns +true+ if a new transaction was started since the last
23
+ # insert / update / delete.
24
+ def new_transaction?
25
+ committer.new_transaction?
26
+ end
27
+
22
28
  # Delegates to Committer#insert_record
23
29
  def insert_record(database, table, values)
24
30
  committer.insert_record(database, table, values)
@@ -149,19 +149,20 @@ module RR
149
149
  # Creates the replication log table.
150
150
  def create_event_log
151
151
  silence_ddl_notices(:left) do
152
- session.left.create_table "#{options[:rep_prefix]}_logged_events", :id => false do |t|
153
- t.column :activity, :string
154
- t.column :change_table, :string
155
- t.column :diff_type, :string
156
- t.column :change_key, :string
157
- t.column :left_change_type, :string
158
- t.column :right_change_type, :string
159
- t.column :description, :string, :limit => DESCRIPTION_SIZE
160
- t.column :long_description, :string, :limit => LONG_DESCRIPTION_SIZE
161
- t.column :event_time, :timestamp
162
- t.column :diff_dump, :string, :limit => DIFF_DUMP_SIZE
163
- end
164
- session.left.add_big_primary_key "#{options[:rep_prefix]}_logged_events", 'id'
152
+ table_name = "#{options[:rep_prefix]}_logged_events"
153
+ session.left.create_table "#{options[:rep_prefix]}_logged_events"
154
+ session.left.add_column table_name, :activity, :string
155
+ session.left.add_column table_name, :change_table, :string
156
+ session.left.add_column table_name, :diff_type, :string
157
+ session.left.add_column table_name, :change_key, :string
158
+ session.left.add_column table_name, :left_change_type, :string
159
+ session.left.add_column table_name, :right_change_type, :string
160
+ session.left.add_column table_name, :description, :string, :limit => DESCRIPTION_SIZE
161
+ session.left.add_column table_name, :long_description, :string, :limit => LONG_DESCRIPTION_SIZE
162
+ session.left.add_column table_name, :event_time, :timestamp
163
+ session.left.add_column table_name, :diff_dump, :string, :limit => DIFF_DUMP_SIZE
164
+ session.left.remove_column table_name, 'id'
165
+ session.left.add_big_primary_key table_name, 'id'
165
166
  end
166
167
  end
167
168
 
@@ -169,14 +170,16 @@ module RR
169
170
  # * database: either :+left+ or :+right+
170
171
  def create_change_log(database)
171
172
  silence_ddl_notices(database) do
172
- session.send(database).create_table "#{options[:rep_prefix]}_pending_changes", :id => false do |t|
173
- t.column :change_table, :string
174
- t.column :change_key, :string
175
- t.column :change_new_key, :string
176
- t.column :change_type, :string
177
- t.column :change_time, :timestamp
178
- end
179
- session.send(database).add_big_primary_key "#{options[:rep_prefix]}_pending_changes", 'id'
173
+ connection = session.send(database)
174
+ table_name = "#{options[:rep_prefix]}_pending_changes"
175
+ connection.create_table table_name
176
+ connection.add_column table_name, :change_table, :string
177
+ connection.add_column table_name, :change_key, :string
178
+ connection.add_column table_name, :change_new_key, :string
179
+ connection.add_column table_name, :change_type, :string
180
+ connection.add_column table_name, :change_time, :timestamp
181
+ connection.remove_column table_name, 'id'
182
+ connection.add_big_primary_key table_name, 'id'
180
183
  end
181
184
  end
182
185
 
@@ -190,9 +193,12 @@ module RR
190
193
  def ensure_activity_markers
191
194
  table_name = "#{options[:rep_prefix]}_running_flags"
192
195
  [:left, :right].each do |database|
193
- unless session.send(database).tables.include? table_name
194
- session.send(database).create_table table_name, :id => false do |t|
195
- t.column :active, :integer
196
+ connection = session.send(database)
197
+ unless connection.tables.include? table_name
198
+ silence_ddl_notices(database) do
199
+ connection.create_table table_name
200
+ connection.add_column table_name, :active, :integer
201
+ connection.remove_column table_name, 'id'
196
202
  end
197
203
  end
198
204
  end
@@ -298,7 +304,9 @@ module RR
298
304
  unsynced = true
299
305
  end
300
306
  end
301
- unsynced_table_pairs << table_pair if unsynced
307
+ if unsynced and table_options[:initial_sync]
308
+ unsynced_table_pairs << table_pair
309
+ end
302
310
  end
303
311
  unsynced_table_specs = unsynced_table_pairs.map do |table_pair|
304
312
  "#{table_pair[:left]}, #{table_pair[:right]}"
@@ -35,10 +35,15 @@ module RR
35
35
  changes_pending
36
36
  end
37
37
 
38
+ # Apparently sometimes above check for changes takes already so long, that
39
+ # the replication run times out.
40
+ # Check for this and if timed out, return (silently).
41
+ return if sweeper.terminated?
42
+
38
43
  loaders = LoggedChangeLoaders.new(session)
39
44
 
45
+ success = false
40
46
  begin
41
- success = false
42
47
  replicator # ensure that replicator is created and has chance to validate settings
43
48
 
44
49
  loop do
@@ -47,16 +52,26 @@ module RR
47
52
  diff = ReplicationDifference.new loaders
48
53
  diff.load
49
54
  break unless diff.loaded?
50
- raise "Replication run timed out" if sweeper.terminated?
55
+ break if sweeper.terminated?
51
56
  replicator.replicate_difference diff if diff.type != :no_diff
52
57
  rescue Exception => e
53
- helper.log_replication_outcome diff, e.message,
54
- e.class.to_s + "\n" + e.backtrace.join("\n")
58
+ begin
59
+ helper.log_replication_outcome diff, e.message,
60
+ e.class.to_s + "\n" + e.backtrace.join("\n")
61
+ rescue Exception => _
62
+ # if logging to database itself fails, re-raise the original exception
63
+ raise e
64
+ end
55
65
  end
56
66
  end
57
- success = true # considered to be successful if we get till here
67
+ success = true
58
68
  ensure
59
- helper.finalize success
69
+ if sweeper.terminated?
70
+ helper.finalize false
71
+ session.disconnect_databases
72
+ else
73
+ helper.finalize success
74
+ end
60
75
  end
61
76
  end
62
77
 
@@ -65,12 +65,20 @@ EOS
65
65
  # Loads config file and creates session if necessary.
66
66
  def session
67
67
  unless @session
68
- load options[:config_file]
69
- @session = Session.new Initializer.configuration
68
+ unless @config
69
+ load options[:config_file]
70
+ @config = Initializer.configuration
71
+ end
72
+ @session = Session.new @config
70
73
  end
71
74
  @session
72
75
  end
73
76
 
77
+ # Removes current +Session+.
78
+ def clear_session
79
+ @session = nil
80
+ end
81
+
74
82
  # Wait for the next replication time
75
83
  def pause_replication
76
84
  @last_run ||= 1.year.ago
@@ -100,6 +108,20 @@ EOS
100
108
  initializer.prepare_replication
101
109
  end
102
110
 
111
+ # Executes a single replication run
112
+ def execute_once
113
+ session.refresh
114
+ timeout = session.configuration.options[:database_connection_timeout]
115
+ terminated = TaskSweeper.timeout(timeout) do |sweeper|
116
+ run = ReplicationRun.new session, sweeper
117
+ run.run
118
+ end.terminated?
119
+ raise "replication run timed out" if terminated
120
+ rescue Exception => e
121
+ clear_session
122
+ raise e
123
+ end
124
+
103
125
  # Executes an endless loop of replication runs
104
126
  def execute
105
127
  init_waiter
@@ -107,13 +129,7 @@ EOS
107
129
 
108
130
  until termination_requested do
109
131
  begin
110
- session.refresh
111
- timeout = session.configuration.options[:database_connection_timeout]
112
- terminated = TaskSweeper.timeout(timeout) do |sweeper|
113
- run = ReplicationRun.new session, sweeper
114
- run.run
115
- end.terminated?
116
- raise "replication run timed out" if terminated
132
+ execute_once
117
133
  rescue Exception => e
118
134
  now = Time.now.iso8601
119
135
  $stderr.puts "#{now} Exception caught: #{e}"
@@ -227,19 +227,9 @@ module RR
227
227
  diff.amend
228
228
  replicate_difference diff, remaining_attempts - 1, "source record for insert vanished"
229
229
  else
230
- begin
231
- # note: savepoints have to be used for postgresql (as a failed SQL
232
- # statement will otherwise invalidate the complete transaction.)
233
- rep_helper.session.send(target_db).execute "savepoint rr_insert"
234
- log_replication_outcome source_db, diff
230
+ attempt_change('insert', source_db, target_db, diff, remaining_attempts) do
235
231
  rep_helper.insert_record target_db, target_table, values
236
- rescue Exception => e
237
- rep_helper.session.send(target_db).execute "rollback to savepoint rr_insert"
238
- row = rep_helper.load_record target_db, target_table, source_key
239
- raise unless row # problem is not the existence of the record in the target db
240
- diff.amend
241
- replicate_difference diff, remaining_attempts - 1,
242
- "insert failed with #{e.message}"
232
+ log_replication_outcome source_db, diff
243
233
  end
244
234
  end
245
235
  end
@@ -263,19 +253,45 @@ module RR
263
253
  diff.amend
264
254
  replicate_difference diff, remaining_attempts - 1, "source record for update vanished"
265
255
  else
266
- begin
267
- rep_helper.session.send(target_db).execute "savepoint rr_update"
268
- log_replication_outcome source_db, diff
269
- rep_helper.update_record target_db, target_table, values, target_key
270
- rescue Exception => e
271
- rep_helper.session.send(target_db).execute "rollback to savepoint rr_update"
272
- diff.amend
273
- replicate_difference diff, remaining_attempts - 1,
274
- "update failed with #{e.message}"
256
+ attempt_change('update', source_db, target_db, diff, remaining_attempts) do
257
+ number_updated = rep_helper.update_record target_db, target_table, values, target_key
258
+ if number_updated == 0
259
+ diff.amend
260
+ replicate_difference diff, remaining_attempts - 1, "target record for update vanished"
261
+ else
262
+ log_replication_outcome source_db, diff
263
+ end
275
264
  end
276
265
  end
277
266
  end
278
267
 
268
+ # Helper for execution of insert / update / delete attempts.
269
+ # Wraps those attempts into savepoints and handles exceptions.
270
+ #
271
+ # Note:
272
+ # Savepoints have to be used for PostgreSQL (as a failed SQL statement
273
+ # will otherwise invalidate the complete transaction.)
274
+ #
275
+ # * +action+: short description of change (e. g.: "update" or "delete")
276
+ # * +source_db+: either :+left+ or :+right+ - source database of replication
277
+ # * +target_db+: either :+left+ or :+right+ - target database of replication
278
+ # * +diff+: the current ReplicationDifference instance
279
+ # * +remaining_attempts+: the number of remaining replication attempts for this difference
280
+ def attempt_change(action, source_db, target_db, diff, remaining_attempts)
281
+ begin
282
+ rep_helper.session.send(target_db).execute "savepoint rr_#{action}_#{remaining_attempts}"
283
+ yield
284
+ unless rep_helper.new_transaction?
285
+ rep_helper.session.send(target_db).execute "release savepoint rr_#{action}_#{remaining_attempts}"
286
+ end
287
+ rescue Exception => e
288
+ rep_helper.session.send(target_db).execute "rollback to savepoint rr_#{action}_#{remaining_attempts}"
289
+ diff.amend
290
+ replicate_difference diff, remaining_attempts - 1,
291
+ "#{action} failed with #{e.message}"
292
+ end
293
+ end
294
+
279
295
  # Attempts to delete the source record from the target database.
280
296
  # E. g. if +source_db is :+left+, then the record is deleted in database
281
297
  # :+right+.
@@ -287,15 +303,15 @@ module RR
287
303
  change = diff.changes[source_db]
288
304
  target_db = OTHER_SIDE[source_db]
289
305
  target_table = rep_helper.corresponding_table(source_db, change.table)
290
- begin
291
- rep_helper.session.send(target_db).execute "savepoint rr_delete"
292
- log_replication_outcome source_db, diff
293
- rep_helper.delete_record target_db, target_table, target_key
294
- rescue Exception => e
295
- rep_helper.session.send(target_db).execute "rollback to savepoint rr_delete"
296
- diff.amend
297
- replicate_difference diff, remaining_attempts - 1,
298
- "delete failed with #{e.message}"
306
+
307
+ attempt_change('delete', source_db, target_db, diff, remaining_attempts) do
308
+ number_updated = rep_helper.delete_record target_db, target_table, target_key
309
+ if number_updated == 0
310
+ diff.amend
311
+ replicate_difference diff, remaining_attempts - 1, "target record for delete vanished"
312
+ else
313
+ log_replication_outcome source_db, diff
314
+ end
299
315
  end
300
316
  end
301
317
 
@@ -133,10 +133,17 @@ module RR
133
133
  unreachable
134
134
  end
135
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
+
136
143
  # Disconnnects the specified database
137
144
  # * +database+: the target database (either :+left+ or :+right+)
138
145
  def disconnect_database(database)
139
- proxy, connection = @proxies[database], @connection[database]
146
+ proxy, connection = @proxies[database], @connections[database]
140
147
  @proxies[database] = nil
141
148
  @connections[database] = nil
142
149
  if proxy
@@ -145,15 +152,19 @@ module RR
145
152
  end
146
153
 
147
154
  # Refreshes both database connections
148
- def refresh
149
- [:left, :right].each {|database| refresh_database_connection database}
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}
150
159
  end
151
160
 
152
161
  # Refreshes the specified database connection.
153
162
  # (I. e. reestablish if not active anymore.)
154
163
  # * +database+: target database (either :+left+ or :+right+)
155
- def refresh_database_connection(database)
156
- if database_unreachable?(database)
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)
157
168
  # step 1: disconnect both database connection (if still possible)
158
169
  begin
159
170
  Thread.new do
@@ -72,6 +72,7 @@ EOS
72
72
  initializer = ReplicationInitializer.new session
73
73
  initializer.restore_unconfigured_tables([])
74
74
  initializer.drop_infrastructure
75
+ puts "Uninstall completed: rubyrep tables and triggers removed!"
75
76
  end
76
77
 
77
78
  # Entry points for executing a processing run.
@@ -2,7 +2,7 @@ module RR #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 1
4
4
  MINOR = 0
5
- TINY = 7
5
+ TINY = 8
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
data/rubyrep ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+
3
+ script_dir="`dirname \"$0\"`"
4
+
5
+ jruby_path="$script_dir"/jruby/bin/jruby
6
+ rubyrep_path="$script_dir"/bin/rubyrep
7
+
8
+ $jruby_path --server $rubyrep_path $*
data/rubyrep.bat ADDED
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ set jruby_path=%~dp0jruby\bin\jruby.bat
3
+ set rubyrep_path=%~dp0bin\rubyrep
4
+ %jruby_path% --server %rubyrep_path% %1 %2 %3 %4 %5 %6 %7 %8 %9
@@ -8,16 +8,16 @@ def prepare_schema
8
8
  session = RR::Session.new
9
9
 
10
10
  [:left, :right].each do |database|
11
+ c = session.send(database)
11
12
  [:big_scan, :big_rep, :big_rep_backup].each do |table|
12
- session.send(database).drop_table table rescue nil
13
- session.send(database).create_table table do |t|
14
- t.column :diff_type, :string
15
- t.string :text1, :text2, :text3, :text4
16
- t.text :text5
17
- t.binary :text6
18
- t.integer :number1, :number2, :number3
19
- t.float :number4
20
- end rescue nil
13
+ c.drop_table table rescue nil
14
+ c.create_table table
15
+ c.add_column table, :diff_type, :string
16
+ (1..4).each {|i| c.add_column table, "text#{i}", :string}
17
+ c.add_column table, :text5, :text
18
+ c.add_column table, :text6, :binary
19
+ (1..3).each {|i| c.add_column table, "number#{i}", :integer}
20
+ c.add_column table, :number4, :float
21
21
  end
22
22
  end
23
23
  end
@@ -202,9 +202,12 @@ describe Committers::BufferedCommitter do
202
202
  stub_execute session
203
203
  committer = Committers::BufferedCommitter.new(session)
204
204
 
205
- committer.should_not_receive(:commit_db_transactions).twice
206
- committer.should_not_receive(:begin_db_transactions).twice
207
- 4.times {committer.commit}
205
+ committer.should_receive(:commit_db_transactions).twice
206
+ committer.should_receive(:begin_db_transactions).twice
207
+ committer.commit
208
+ committer.new_transaction?.should be_false
209
+ 3.times {committer.commit}
210
+ committer.new_transaction?.should be_true
208
211
  end
209
212
 
210
213
  it "insert_record should commit" do
@@ -82,6 +82,10 @@ describe Committers::DefaultCommitter do
82
82
  @committer.connections \
83
83
  .should == {:left => @session.left, :right => @session.right}
84
84
  end
85
+
86
+ it "new_transaction? should return false" do
87
+ @committer.new_transaction?.should be_false
88
+ end
85
89
 
86
90
  it_should_behave_like "Committer"
87
91
  end
@@ -296,6 +296,20 @@ describe ProxyConnection do
296
296
  end
297
297
  end
298
298
 
299
+ it "update_record should return the number of updated records" do
300
+ @connection.begin_db_transaction
301
+ begin
302
+ @connection.
303
+ update_record('scanner_records', 'id' => 1, 'name' => 'update_test').
304
+ should == 1
305
+ @connection.
306
+ update_record('scanner_records', 'id' => 0, 'name' => 'update_test').
307
+ should == 0
308
+ ensure
309
+ @connection.rollback_db_transaction
310
+ end
311
+ end
312
+
299
313
  it "update_record should handle combined primary keys" do
300
314
  @connection.begin_db_transaction
301
315
  begin
@@ -377,4 +391,17 @@ describe ProxyConnection do
377
391
  end
378
392
  end
379
393
 
394
+ it "delete_record should return the number of deleted records" do
395
+ @connection.begin_db_transaction
396
+ begin
397
+ @connection.
398
+ delete_record('extender_combined_key', 'first_id' => 1, 'second_id' => '1').
399
+ should == 1
400
+ @connection.
401
+ delete_record('extender_combined_key', 'first_id' => 1, 'second_id' => '0').
402
+ should == 0
403
+ ensure
404
+ @connection.rollback_db_transaction
405
+ end
406
+ end
380
407
  end
@@ -345,12 +345,14 @@ describe "ReplicationExtender", :shared => true do
345
345
  session = nil
346
346
  begin
347
347
  session = Session.new
348
- session.left.drop_table 'big_key_test' if session.left.tables.include? 'big_key_test'
349
- session.left.create_table 'big_key_test'.to_sym, :id => false do |t|
350
- t.column :name, :string
348
+ initializer = ReplicationInitializer.new(session)
349
+ initializer.silence_ddl_notices(:left) do
350
+ session.left.drop_table 'big_key_test' if session.left.tables.include? 'big_key_test'
351
+ session.left.create_table 'big_key_test'.to_sym
352
+ session.left.add_column 'big_key_test', :name, :string
353
+ session.left.remove_column 'big_key_test', 'id'
354
+ session.left.add_big_primary_key 'big_key_test', 'id'
351
355
  end
352
- session.left.add_big_primary_key 'big_key_test', 'id'
353
-
354
356
  # should auto generate the primary key if not manually specified
355
357
  session.left.insert_record 'big_key_test', {'name' => 'bla'}
356
358
  session.left.select_one("select id from big_key_test where name = 'bla'")['id'].
@@ -22,6 +22,15 @@ describe ReplicationHelper do
22
22
  helper.session.should == rep_run.session
23
23
  end
24
24
 
25
+ it "new_transaction? should delegate to the committer" do
26
+ session = Session.new
27
+ rep_run = ReplicationRun.new(session, TaskSweeper.new(1))
28
+ helper = ReplicationHelper.new(rep_run)
29
+ c = helper.instance_eval {@committer}
30
+ c.should_receive(:new_transaction?).and_return(true)
31
+ helper.new_transaction?.should be_true
32
+ end
33
+
25
34
  it "replication_run should return the current ReplicationRun instance" do
26
35
  rep_run = ReplicationRun.new(Session.new, TaskSweeper.new(1))
27
36
  helper = ReplicationHelper.new(rep_run)
@@ -437,8 +437,14 @@ describe ReplicationInitializer do
437
437
 
438
438
  config.include_tables 'rr_pending_changes' # added to verify that it is ignored
439
439
 
440
+ # added to verify that a disabled :initial_sync is honored
441
+ config.add_table_options 'table_with_manual_key', :initial_sync => false
442
+
440
443
  session = Session.new(config)
441
444
 
445
+ # dummy data to verify that 'table_with_manual_key' is indeed not synced
446
+ session.left.insert_record 'table_with_manual_key', :id => 1, :name => 'bla'
447
+
442
448
  $stdout = StringIO.new
443
449
  begin
444
450
  initializer = ReplicationInitializer.new(session)
@@ -470,6 +476,9 @@ describe ReplicationInitializer do
470
476
  # verify that the 'rr_pending_changes' table was not touched
471
477
  initializer.trigger_exists?(:left, 'rr_pending_changes').should be_false
472
478
 
479
+ # verify that :initial_sync => false is honored
480
+ session.right.select_all("select * from table_with_manual_key").should be_empty
481
+
473
482
  # verify that syncing is done only for unsynced tables
474
483
  SyncRunner.should_not_receive(:new)
475
484
  initializer.prepare_replication
@@ -477,6 +486,7 @@ describe ReplicationInitializer do
477
486
  ensure
478
487
  $stdout = org_stdout
479
488
  if session
489
+ session.left.execute "delete from table_with_manual_key"
480
490
  session.left.execute "delete from scanner_left_records_only where id = 10"
481
491
  session.right.execute "delete from scanner_left_records_only"
482
492
  [:left, :right].each do |database|
@@ -137,6 +137,76 @@ describe ReplicationRun do
137
137
  end
138
138
  end
139
139
 
140
+ it "run should re-raise original exception if logging to database fails" do
141
+ session = Session.new
142
+ session.left.begin_db_transaction
143
+ session.right.begin_db_transaction
144
+ begin
145
+ session.left.execute "delete from rr_pending_changes"
146
+ session.left.execute "delete from rr_logged_events"
147
+ session.left.insert_record 'rr_pending_changes', {
148
+ 'change_table' => 'extender_no_record',
149
+ 'change_key' => 'id|1',
150
+ 'change_type' => 'D',
151
+ 'change_time' => Time.now
152
+ }
153
+ run = ReplicationRun.new session, TaskSweeper.new(1)
154
+ run.replicator.stub!(:replicate_difference).and_return {raise Exception, 'dummy message'}
155
+ run.helper.stub!(:log_replication_outcome).and_return {raise Exception, 'blub'}
156
+ lambda {run.run}.should raise_error(Exception, 'dummy message')
157
+ ensure
158
+ session.left.rollback_db_transaction
159
+ session.right.rollback_db_transaction
160
+ end
161
+ end
162
+
163
+ it "run should return silently if timed out before work actually started" do
164
+ session = Session.new
165
+ session.left.begin_db_transaction
166
+ session.right.begin_db_transaction
167
+ begin
168
+ session.left.execute "delete from rr_pending_changes"
169
+ session.left.insert_record 'rr_pending_changes', {
170
+ 'change_table' => 'extender_no_record',
171
+ 'change_key' => 'id|1',
172
+ 'change_type' => 'D',
173
+ 'change_time' => Time.now
174
+ }
175
+ sweeper = TaskSweeper.new(1)
176
+ sweeper.stub!(:terminated?).and_return(true)
177
+ run = ReplicationRun.new session, sweeper
178
+ LoggedChangeLoaders.should_not_receive(:new)
179
+ run.run
180
+ ensure
181
+ session.left.rollback_db_transaction
182
+ session.right.rollback_db_transaction
183
+ end
184
+ end
185
+
186
+ it "run should rollback if timed out" do
187
+ session = Session.new
188
+ session.left.begin_db_transaction
189
+ session.right.begin_db_transaction
190
+ begin
191
+ session.left.execute "delete from rr_pending_changes"
192
+ session.left.execute "delete from rr_logged_events"
193
+ session.left.insert_record 'rr_pending_changes', {
194
+ 'change_table' => 'extender_no_record',
195
+ 'change_key' => 'id|1',
196
+ 'change_type' => 'D',
197
+ 'change_time' => Time.now
198
+ }
199
+ sweeper = TaskSweeper.new(1)
200
+ sweeper.should_receive(:terminated?).any_number_of_times.and_return(false, true)
201
+ run = ReplicationRun.new session, sweeper
202
+ run.helper.should_receive(:finalize).with(false)
203
+ run.run
204
+ ensure
205
+ session.left.rollback_db_transaction if session.left
206
+ session.right.rollback_db_transaction if session.right
207
+ end
208
+ end
209
+
140
210
  it "run should not catch exceptions raised during replicator initialization" do
141
211
  config = deep_copy(standard_config)
142
212
  config.options[:logged_replication_events] = [:invalid_option]
@@ -4,6 +4,7 @@ include RR
4
4
 
5
5
  describe ReplicationRunner do
6
6
  before(:each) do
7
+ Initializer.configuration = standard_config
7
8
  end
8
9
 
9
10
  it "should register itself with CommandRunner" do
@@ -168,6 +169,36 @@ describe ReplicationRunner do
168
169
  end
169
170
  end
170
171
 
172
+ it "execute_once should not clean up if successful" do
173
+ runner = ReplicationRunner.new
174
+ session = Session.new
175
+ runner.instance_variable_set(:@session, session)
176
+
177
+ runner.execute_once
178
+ runner.instance_variable_get(:@session).should == session
179
+ end
180
+
181
+ it "execute_once should clean up after failed replication runs" do
182
+ runner = ReplicationRunner.new
183
+ session = Session.new
184
+ runner.instance_variable_set(:@session, session)
185
+
186
+ session.should_receive(:refresh).and_raise('bla')
187
+ lambda {runner.execute_once}.should raise_error('bla')
188
+ runner.instance_variable_get(:@session).should be_nil
189
+ end
190
+
191
+ it "execute_once should raise exception if replication run times out" do
192
+ session = Session.new
193
+ runner = ReplicationRunner.new
194
+ runner.stub!(:session).and_return(session)
195
+ terminated = mock("terminated")
196
+ terminated.stub!(:terminated?).and_return(true)
197
+ TaskSweeper.stub!(:timeout).and_return(terminated)
198
+
199
+ lambda {runner.execute_once}.should raise_error(/timed out/)
200
+ end
201
+
171
202
  it "execute should start the replication" do
172
203
  config = deep_copy(standard_config)
173
204
  config.options[:committer] = :buffered_commit
data/spec/session_spec.rb CHANGED
@@ -118,6 +118,17 @@ describe Session do # here database connection caching is _not_ disabled
118
118
  session.right.select_one("select 1+1 as x")['x'].to_i.should == 2
119
119
  end
120
120
 
121
+ it "disconnect_databases should disconnect both databases" do
122
+ session = Session.new(standard_config)
123
+ session.left.connection.should be_active
124
+ old_right_connection = session.right.connection
125
+ old_right_connection.should be_active
126
+ session.disconnect_databases
127
+ session.left.should be_nil
128
+ session.right.should be_nil
129
+ old_right_connection.should_not be_active
130
+ end
131
+
121
132
  it "refresh should not do anyting if the connection is still active" do
122
133
  session = Session.new
123
134
  old_connection_id = session.right.connection.object_id
@@ -125,6 +136,13 @@ describe Session do # here database connection caching is _not_ disabled
125
136
  session.right.connection.object_id.should == old_connection_id
126
137
  end
127
138
 
139
+ it "refresh should replace active connections if forced is true" do
140
+ session = Session.new
141
+ old_connection_id = session.right.connection.object_id
142
+ session.refresh :forced => true
143
+ session.right.connection.object_id.should_not == old_connection_id
144
+ end
145
+
128
146
  it "manual_primary_keys should return the specified manual primary keys" do
129
147
  config = deep_copy(standard_config)
130
148
  config.included_table_specs.clear
data/spec/spec_helper.rb CHANGED
@@ -240,9 +240,9 @@ $start_proxy_as_external_process ||= false
240
240
  # Starts a proxy under the given host and post
241
241
  def start_proxy(host, port)
242
242
  if $start_proxy_as_external_process
243
- rrproxy_path = File.join(File.dirname(__FILE__), "..", "bin", "rrproxy.rb")
243
+ bin_path = File.join(File.dirname(__FILE__), "..", "bin", "rubyrep")
244
244
  ruby = RUBY_PLATFORM =~ /java/ ? 'jruby' : 'ruby'
245
- cmd = "#{ruby} #{rrproxy_path} -h #{host} -p #{port}"
245
+ cmd = "#{ruby} #{bin_path} proxy -h #{host} -p #{port}"
246
246
  Thread.new {system cmd}
247
247
  else
248
248
  url = "druby://#{host}:#{port}"
@@ -514,6 +514,7 @@ describe Replicators::TwoWayReplicator do
514
514
  config.options[:replication_conflict_handling] = :left_wins
515
515
 
516
516
  session = Session.new(config)
517
+ session.left.execute "delete from rr_logged_events"
517
518
 
518
519
  session.left.insert_record 'rr_pending_changes', {
519
520
  'change_table' => 'scanner_records',
@@ -593,6 +594,50 @@ describe Replicators::TwoWayReplicator do
593
594
  end
594
595
  end
595
596
 
597
+ it "replicate_difference should handle deletes failing due to the target record vanishing" do
598
+ begin
599
+ config = deep_copy(standard_config)
600
+ config.options[:committer] = :never_commit
601
+ config.options[:replication_conflict_handling] = :left_wins
602
+
603
+ session = Session.new(config)
604
+
605
+ session.left.insert_record 'rr_pending_changes', {
606
+ 'change_table' => 'scanner_records',
607
+ 'change_key' => 'id|3',
608
+ 'change_new_key' => nil,
609
+ 'change_type' => 'D',
610
+ 'change_time' => Time.now
611
+ }
612
+
613
+ rep_run = ReplicationRun.new session, TaskSweeper.new(1)
614
+ helper = ReplicationHelper.new(rep_run)
615
+ replicator = Replicators::TwoWayReplicator.new(helper)
616
+
617
+ diff = ReplicationDifference.new LoggedChangeLoaders.new(session)
618
+ diff.load
619
+
620
+ session.right.insert_record 'rr_pending_changes', {
621
+ 'change_table' => 'scanner_records',
622
+ 'change_key' => 'id|3',
623
+ 'change_new_key' => 'id|4',
624
+ 'change_type' => 'U',
625
+ 'change_time' => Time.now
626
+ }
627
+
628
+ replicator.replicate_difference diff, 2
629
+
630
+ session.right.select_one("select * from scanner_records where id = 4").
631
+ should be_nil
632
+ ensure
633
+ Committers::NeverCommitter.rollback_current_session
634
+ if session
635
+ session.left.execute "delete from rr_pending_changes"
636
+ session.left.execute "delete from rr_logged_events"
637
+ end
638
+ end
639
+ end
640
+
596
641
  it "replicate_difference should handle updates failing due to the source record being deleted after the original diff was loaded" do
597
642
  begin
598
643
  config = deep_copy(standard_config)
@@ -644,4 +689,53 @@ describe Replicators::TwoWayReplicator do
644
689
  end
645
690
  end
646
691
  end
692
+
693
+ it "replicate_difference should handle updates failing due to the target record being deleted after the original diff was loaded" do
694
+ begin
695
+ config = deep_copy(standard_config)
696
+ config.options[:committer] = :never_commit
697
+ config.options[:replication_conflict_handling] = :left_wins
698
+
699
+ session = Session.new(config)
700
+
701
+ session.left.insert_record 'extender_no_record', {
702
+ 'id' => '2',
703
+ 'name' => 'bla'
704
+ }
705
+ session.left.insert_record 'rr_pending_changes', {
706
+ 'change_table' => 'extender_no_record',
707
+ 'change_key' => 'id|1',
708
+ 'change_new_key' => 'id|2',
709
+ 'change_type' => 'U',
710
+ 'change_time' => Time.now
711
+ }
712
+
713
+ rep_run = ReplicationRun.new session, TaskSweeper.new(1)
714
+ helper = ReplicationHelper.new(rep_run)
715
+ replicator = Replicators::TwoWayReplicator.new(helper)
716
+
717
+ diff = ReplicationDifference.new LoggedChangeLoaders.new(session)
718
+ diff.load
719
+
720
+ session.right.insert_record 'rr_pending_changes', {
721
+ 'change_table' => 'extender_no_record',
722
+ 'change_key' => 'id|1',
723
+ 'change_type' => 'D',
724
+ 'change_time' => Time.now
725
+ }
726
+ replicator.replicate_difference diff, 2
727
+
728
+ session.right.select_one("select * from extender_no_record").should == {
729
+ 'id' => '2',
730
+ 'name' => 'bla'
731
+ }
732
+ ensure
733
+ Committers::NeverCommitter.rollback_current_session
734
+ if session
735
+ session.left.execute "delete from extender_no_record"
736
+ session.right.execute "delete from extender_no_record"
737
+ session.left.execute "delete from rr_pending_changes"
738
+ end
739
+ end
740
+ end
647
741
  end
@@ -65,22 +65,29 @@ describe UninstallRunner do
65
65
  end
66
66
 
67
67
  it "execute should uninstall all rubyrep elements" do
68
- config = deep_copy(standard_config)
69
- config.options[:rep_prefix] = 'rx'
70
- session = Session.new(config)
71
- initializer = ReplicationInitializer.new(session)
68
+ begin
69
+ org_stdout, $stdout = $stdout, StringIO.new
70
+ config = deep_copy(standard_config)
71
+ config.options[:rep_prefix] = 'rx'
72
+ session = Session.new(config)
73
+ initializer = ReplicationInitializer.new(session)
72
74
 
73
- initializer.ensure_infrastructure
74
- initializer.create_trigger :left, 'scanner_records'
75
+ initializer.ensure_infrastructure
76
+ initializer.create_trigger :left, 'scanner_records'
75
77
 
76
- runner = UninstallRunner.new
77
- runner.stub!(:session).and_return(session)
78
+ runner = UninstallRunner.new
79
+ runner.stub!(:session).and_return(session)
80
+
81
+ runner.execute
78
82
 
79
- runner.execute
83
+ initializer.trigger_exists?(:left, 'scanner_records').should be_false
84
+ initializer.change_log_exists?(:left).should be_false
85
+ session.right.tables.include?('rx_running_flags').should be_false
86
+ initializer.event_log_exists?.should be_false
80
87
 
81
- initializer.trigger_exists?(:left, 'scanner_records').should be_false
82
- initializer.change_log_exists?(:left).should be_false
83
- session.right.tables.include?('rx_running_flags').should be_false
84
- initializer.event_log_exists?.should be_false
88
+ $stdout.string =~ /uninstall completed/i
89
+ ensure
90
+ $stdout = org_stdout
91
+ end
85
92
  end
86
93
  end
data/tasks/java.rake CHANGED
@@ -1,23 +1,5 @@
1
1
  namespace :deploy do
2
2
 
3
- BASH_FILE_CONTENT = <<'EOS'
4
- #!/bin/bash
5
-
6
- script_dir="`dirname \"$0\"`"
7
-
8
- jruby_path="$script_dir"/jruby/bin/jruby
9
- rubyrep_path="$script_dir"/bin/rubyrep
10
-
11
- $jruby_path --server $rubyrep_path $*
12
- EOS
13
-
14
- BAT_FILE_CONTENT = <<'EOS'.gsub(/^(.*)$/,"\\1\r")
15
- @echo off
16
- set jruby_path=%~dp0jruby\bin\jruby.bat
17
- set rubyrep_path=%~dp0bin\rubyrep
18
- %jruby_path% --server %rubyrep_path% %1 %2 %3 %4 %5 %6 %7 %8 %9
19
- EOS
20
-
21
3
  desc "Create the java installation package"
22
4
  task :java do
23
5
  pkg_name = "rubyrep-#{RR::VERSION::STRING}"
@@ -28,8 +10,6 @@ EOS
28
10
  system "mkdir -p /tmp/#{pkg_name}/jruby"
29
11
  system "cp -r #{JRUBY_HOME}/* /tmp/#{pkg_name}/jruby/"
30
12
  system "cd /tmp/#{pkg_name}/jruby; rm -rf samples share/ri lib/ruby/gems/1.8/doc"
31
- File.open("/tmp/#{pkg_name}/rubyrep.bat", 'w') {|f| f.write(BAT_FILE_CONTENT)}
32
- File.open("/tmp/#{pkg_name}/rubyrep", 'w') {|f| f.write(BASH_FILE_CONTENT)}
33
13
  system "chmod a+x /tmp/#{pkg_name}/rubyrep"
34
14
  system "cd /tmp; rm -f #{pkg_name}.zip; zip -r #{pkg_name}.zip #{pkg_name} >/dev/null"
35
15
  system "mkdir -p pkg"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyrep
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arndt Lehmann
@@ -30,7 +30,7 @@ cert_chain:
30
30
  NwT26VZnE2nr8g==
31
31
  -----END CERTIFICATE-----
32
32
 
33
- date: 2009-07-26 00:00:00 +09:00
33
+ date: 2009-10-01 00:00:00 +09:00
34
34
  default_executable:
35
35
  dependencies:
36
36
  - !ruby/object:Gem::Dependency
@@ -144,6 +144,8 @@ files:
144
144
  - lib/rubyrep/type_casting_cursor.rb
145
145
  - lib/rubyrep/uninstall_runner.rb
146
146
  - lib/rubyrep/version.rb
147
+ - rubyrep
148
+ - rubyrep.bat
147
149
  - script/destroy
148
150
  - script/generate
149
151
  - script/txt2html
metadata.gz.sig CHANGED
Binary file