rubyrep 1.0.7 → 1.0.8

Sign up to get free protection for your applications and to get access to all the features.
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