rubyrep 1.0.5 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,9 @@
1
+ == 1.0.6 2009-07-25
2
+
3
+ * Bug fix: do not assume anymore that replication log events appear in sequential order
4
+ * Feature: increased resilience of rubyrep to network timeouts
5
+ * Feature: introduced :after_infrastructure_setup handler
6
+
1
7
  == 1.0.5 2009-07-03
2
8
 
3
9
  * Bug fix: rubyrep replication runs should survive update or delete attempts rejected by the database.
data/Manifest.txt CHANGED
@@ -28,6 +28,8 @@ lib/rubyrep/generate_runner.rb
28
28
  lib/rubyrep/initializer.rb
29
29
  lib/rubyrep/log_helper.rb
30
30
  lib/rubyrep/logged_change.rb
31
+ lib/rubyrep/logged_change_loader.rb
32
+ lib/rubyrep/noisy_connection.rb
31
33
  lib/rubyrep/proxied_table_scan.rb
32
34
  lib/rubyrep/proxy_block_cursor.rb
33
35
  lib/rubyrep/proxy_connection.rb
@@ -60,6 +62,7 @@ lib/rubyrep/table_scan_helper.rb
60
62
  lib/rubyrep/table_sorter.rb
61
63
  lib/rubyrep/table_spec_resolver.rb
62
64
  lib/rubyrep/table_sync.rb
65
+ lib/rubyrep/task_sweeper.rb
63
66
  lib/rubyrep/trigger_mode_switcher.rb
64
67
  lib/rubyrep/type_casting_cursor.rb
65
68
  lib/rubyrep/uninstall_runner.rb
@@ -89,7 +92,9 @@ spec/dolphins.jpg
89
92
  spec/generate_runner_spec.rb
90
93
  spec/initializer_spec.rb
91
94
  spec/log_helper_spec.rb
95
+ spec/logged_change_loader_spec.rb
92
96
  spec/logged_change_spec.rb
97
+ spec/noisy_connection_spec.rb
93
98
  spec/postgresql_replication_spec.rb
94
99
  spec/postgresql_schema_support_spec.rb
95
100
  spec/postgresql_support_spec.rb
@@ -126,6 +131,7 @@ spec/table_scan_spec.rb
126
131
  spec/table_sorter_spec.rb
127
132
  spec/table_spec_resolver_spec.rb
128
133
  spec/table_sync_spec.rb
134
+ spec/task_sweeper_spec.rb
129
135
  spec/trigger_mode_switcher_spec.rb
130
136
  spec/two_way_replicator_spec.rb
131
137
  spec/two_way_syncer_spec.rb
@@ -112,6 +112,20 @@ module RR
112
112
  # * :+replication_interval+: time in seconds between replication runs
113
113
  # * :+database_connection_timeout+:
114
114
  # Time in seconds after which database connections time out.
115
+ # * :+:after_infrastructure_setup+:
116
+ # A Proc that is called after the replication infrastructure tables are
117
+ # set up. Useful to e. g. tweak the access settings for the table.
118
+ # The block is called with the current Session object.
119
+ # The block is called every time replication is started, even if the
120
+ # the infrastructure tables already existed.
121
+ #
122
+ # Example of an :+after_infrastructure_setup+ handler:
123
+ # lambda do |session|
124
+ # [:left, :right].each do |database|
125
+ # session.send(left).execute \
126
+ # "GRANT SELECT, UPDATE, INSERT ON rr_pending_changes TO scott"
127
+ # end
128
+ # end
115
129
  attr_reader :options
116
130
 
117
131
  # Merges the specified +options+ hash into the existing options
@@ -1,184 +1,5 @@
1
1
  module RR
2
2
 
3
- class Session
4
-
5
- # Returns the +LoggedChangeLoader+ of the specified database.
6
- # * database: either :+left+ or :+right+
7
- def change_loader(database)
8
- @change_loaders ||= {}
9
- unless change_loader = @change_loaders[database]
10
- change_loader = @change_loaders[database] = LoggedChangeLoader.new(self, database)
11
- end
12
- change_loader
13
- end
14
-
15
- # Forces an update of the change log cache
16
- def reload_changes
17
- change_loader(:left).update :forced => true
18
- change_loader(:right).update :forced => true
19
- end
20
-
21
- end
22
-
23
- # Caches the entries in the change log table
24
- class LoggedChangeLoader
25
-
26
- # The current +Session+.
27
- attr_accessor :session
28
-
29
- # The current +ProxyConnection+.
30
- attr_accessor :connection
31
-
32
- # Index to the next unprocessed change in the +change_array+.
33
- attr_accessor :current_index
34
-
35
- # ID of the last cached change log record.
36
- attr_accessor :current_id
37
-
38
- # Array with all cached changes.
39
- # Processed change log records are replaced with +nil+.
40
- attr_accessor :change_array
41
-
42
- # Tree (hash) structure for fast access to all cached changes.
43
- # First level of tree:
44
- # * key: table name
45
- # * value: 2nd level tree
46
- # 2nd level tree:
47
- # * key: the change_key value of the according change log records.
48
- # * value:
49
- # An array of according change log records (column_name => value hash).
50
- # Additional entry of each change log hash:
51
- # * key: 'array_index'
52
- # * value: index to the change log record in +change_array+
53
- attr_accessor :change_tree
54
-
55
- # Date of last update of the cache
56
- attr_accessor :last_updated
57
-
58
- # Initializes / resets the cache.
59
- def init_cache
60
- self.change_tree = {}
61
- self.change_array = []
62
- self.current_index = 0
63
- end
64
- private :init_cache
65
-
66
- # Create a new change log record cache.
67
- # * +session+: The current +Session+
68
- # * +database+: Either :+left+ or :+right+
69
- def initialize(session, database)
70
- self.session = session
71
- self.connection = session.send(database)
72
-
73
- init_cache
74
- self.current_id = -1
75
- self.last_updated = 1.year.ago
76
- end
77
-
78
- # Updates the cache.
79
- # Options is a hash determining when the update is actually executed:
80
- # * :+expire_time+: cache is older than the given number of seconds
81
- # * :+forced+: if +true+ update the cache even if not yet expired
82
- def update(options = {:forced => false, :expire_time => 1})
83
- return unless options[:forced] or Time.now - self.last_updated >= options[:expire_time]
84
-
85
- self.last_updated = Time.now
86
-
87
- # First, let's use a LIMIT clause (via :row_buffer_size option) to verify
88
- # if there are any pending changes.
89
- # (If there are many pending changes, this is (at least with PostgreSQL)
90
- # much faster.)
91
- cursor = connection.select_cursor(
92
- :table => change_log_table,
93
- :from => {'id' => current_id},
94
- :exclude_starting_row => true,
95
- :row_buffer_size => 1
96
- )
97
- return unless cursor.next?
98
-
99
- # Something is here. Let's actually load it.
100
- cursor = connection.select_cursor(
101
- :table => change_log_table,
102
- :from => {'id' => current_id},
103
- :exclude_starting_row => true,
104
- :type_cast => true
105
- )
106
- while cursor.next?
107
- change = cursor.next_row
108
- self.current_id = change['id']
109
- self.change_array << change
110
- change['array_index'] = self.change_array.size - 1
111
-
112
- table_change_tree = change_tree[change['change_table']] ||= {}
113
- key_changes = table_change_tree[change['change_key']] ||= []
114
- key_changes << change
115
- end
116
- cursor.clear
117
- end
118
-
119
- # Returns the creation time of the oldest unprocessed change log record.
120
- def oldest_change_time
121
- change = oldest_change
122
- change['change_time'] if change
123
- end
124
-
125
- # Returns the oldest unprocessed change log record (column_name => value hash).
126
- def oldest_change
127
- update
128
- oldest_change = nil
129
- unless change_array.empty?
130
- while (oldest_change = change_array[self.current_index]) == nil
131
- self.current_index += 1
132
- end
133
- end
134
- oldest_change
135
- end
136
-
137
- # Returns the specified change log record (column_name => value hash).
138
- # * +change_table+: the name of the table that was changed
139
- # * +change_key+: the change key of the modified record
140
- def load(change_table, change_key)
141
- update
142
- change = nil
143
- table_change_tree = change_tree[change_table]
144
- if table_change_tree
145
- key_changes = table_change_tree[change_key]
146
- if key_changes
147
- # get change object and delete from key_changes
148
- change = key_changes.shift
149
-
150
- # delete change from change_array
151
- change_array[change['array_index']] = nil
152
-
153
- # delete change from database
154
- connection.execute "delete from #{change_log_table} where id = #{change['id']}"
155
-
156
- # delete key_changes if empty
157
- if key_changes.empty?
158
- table_change_tree.delete change_key
159
- end
160
-
161
- # delete table_change_tree if empty
162
- if table_change_tree.empty?
163
- change_tree.delete change_table
164
- end
165
-
166
- # reset everything if no more changes remain
167
- if change_tree.empty?
168
- init_cache
169
- end
170
- end
171
- end
172
- change
173
- end
174
-
175
- # Returns the name of the change log table
176
- def change_log_table
177
- @change_log_table ||= "#{session.configuration.options[:rep_prefix]}_pending_changes"
178
- end
179
- private :change_log_table
180
- end
181
-
182
3
  # Describes a single logged record change.
183
4
  #
184
5
  # Note:
@@ -187,11 +8,18 @@ module RR
187
8
  # Also at the end of change processing the transaction must be committed.
188
9
  class LoggedChange
189
10
 
11
+ # The current LoggedChangeLoader
12
+ attr_accessor :loader
13
+
190
14
  # The current Session
191
- attr_accessor :session
15
+ def session
16
+ @session ||= loader.session
17
+ end
192
18
 
193
- # The database which was changed. Either :+left+ or :+right+.
194
- attr_accessor :database
19
+ # The current database (either +:left+ or +:right+)
20
+ def database
21
+ @database ||= loader.database
22
+ end
195
23
 
196
24
  # The name of the changed table
197
25
  attr_accessor :table
@@ -213,11 +41,10 @@ module RR
213
41
  attr_accessor :new_key
214
42
 
215
43
  # Creates a new LoggedChange instance.
216
- # * +session+: the current Session
44
+ # * +loader+: the current LoggedChangeLoader
217
45
  # * +database+: either :+left+ or :+right+
218
- def initialize(session, database)
219
- self.session = session
220
- self.database = database
46
+ def initialize(loader)
47
+ self.loader = loader
221
48
  self.type = :no_change
222
49
  end
223
50
 
@@ -281,7 +108,7 @@ module RR
281
108
  end.join(key_sep)
282
109
  current_key = org_key
283
110
 
284
- while change = session.change_loader(database).load(table, current_key)
111
+ while change = loader.load(table, current_key)
285
112
 
286
113
  new_type = change['change_type']
287
114
  current_type = TYPE_CHANGES["#{current_type}#{new_type}"]
@@ -313,16 +140,10 @@ module RR
313
140
  load
314
141
  end
315
142
 
316
- # Returns the time of the oldest change. Returns +nil+ if there are no
317
- # changes left.
318
- def oldest_change_time
319
- session.change_loader(database).oldest_change_time
320
- end
321
-
322
143
  # Loads the oldest available change
323
144
  def load_oldest
324
145
  begin
325
- change = session.change_loader(database).oldest_change
146
+ change = loader.oldest_change
326
147
  break unless change
327
148
  self.key = key_to_hash(change['change_key'])
328
149
  self.table = change['change_table']
@@ -332,7 +153,7 @@ module RR
332
153
 
333
154
  # Prevents session from going into YAML output
334
155
  def to_yaml_properties
335
- instance_variables.sort.reject {|var_name| var_name == '@session'}
156
+ instance_variables.sort.reject {|var_name| ['@session', '@loader'].include? var_name}
336
157
  end
337
158
 
338
159
  end
@@ -0,0 +1,196 @@
1
+ module RR
2
+
3
+ # Makes management of logged change loaders easier
4
+ class LoggedChangeLoaders
5
+
6
+ # The current Session
7
+ attr_accessor :session
8
+
9
+ # A hash of LoggedChangeLoader instances for the :+left+ and :+right+ database
10
+ attr_accessor :loaders
11
+
12
+ # Create new logged change loaders.
13
+ # * +session+: Current Session
14
+ def initialize(session)
15
+ self.session = session
16
+ self.loaders = {}
17
+ [:left, :right].each do |database|
18
+ loaders[database] = LoggedChangeLoader.new(session, database)
19
+ end
20
+ end
21
+
22
+ # Returns the LoggedChangeLoader for the specified (:+left+ or :+right+)
23
+ # database.
24
+ def [](database)
25
+ loaders[database]
26
+ end
27
+
28
+ # Forces an update of the change log cache
29
+ def update
30
+ [:left, :right].each {|database| self[database].update :forced => true}
31
+ end
32
+ end
33
+
34
+ # Caches the entries in the change log table
35
+ class LoggedChangeLoader
36
+
37
+ # The current +Session+.
38
+ attr_accessor :session
39
+
40
+ # Current database (either :+left+ or :+right+)
41
+ attr_accessor :database
42
+
43
+ # The current +ProxyConnection+.
44
+ attr_accessor :connection
45
+
46
+ # Index to the next unprocessed change in the +change_array+.
47
+ attr_accessor :current_index
48
+
49
+ # ID of the last cached change log record.
50
+ attr_accessor :current_id
51
+
52
+ # Array with all cached changes.
53
+ # Processed change log records are replaced with +nil+.
54
+ attr_accessor :change_array
55
+
56
+ # Tree (hash) structure for fast access to all cached changes.
57
+ # First level of tree:
58
+ # * key: table name
59
+ # * value: 2nd level tree
60
+ # 2nd level tree:
61
+ # * key: the change_key value of the according change log records.
62
+ # * value:
63
+ # An array of according change log records (column_name => value hash).
64
+ # Additional entry of each change log hash:
65
+ # * key: 'array_index'
66
+ # * value: index to the change log record in +change_array+
67
+ attr_accessor :change_tree
68
+
69
+ # Date of last update of the cache
70
+ attr_accessor :last_updated
71
+
72
+ # Initializes / resets the cache.
73
+ def init_cache
74
+ self.change_tree = {}
75
+ self.change_array = []
76
+ self.current_index = 0
77
+ end
78
+ private :init_cache
79
+
80
+ # Create a new change log record cache.
81
+ # * +session+: The current +Session+
82
+ # * +database+: Either :+left+ or :+right+
83
+ def initialize(session, database)
84
+ self.session = session
85
+ self.database = database
86
+ self.connection = session.send(database)
87
+
88
+ init_cache
89
+ self.current_id = -1
90
+ self.last_updated = 1.year.ago
91
+ end
92
+
93
+ # Updates the cache.
94
+ # Options is a hash determining when the update is actually executed:
95
+ # * :+expire_time+: cache is older than the given number of seconds
96
+ # * :+forced+: if +true+ update the cache even if not yet expired
97
+ def update(options = {:forced => false, :expire_time => 1})
98
+ return unless options[:forced] or Time.now - self.last_updated >= options[:expire_time]
99
+
100
+ self.last_updated = Time.now
101
+
102
+ # First, let's use a LIMIT clause (via :row_buffer_size option) to verify
103
+ # if there are any pending changes.
104
+ # (If there are many pending changes, this is (at least with PostgreSQL)
105
+ # much faster.)
106
+ cursor = connection.select_cursor(
107
+ :table => change_log_table,
108
+ :from => {'id' => current_id},
109
+ :exclude_starting_row => true,
110
+ :row_buffer_size => 1
111
+ )
112
+ return unless cursor.next?
113
+
114
+ # Something is here. Let's actually load it.
115
+ cursor = connection.select_cursor(
116
+ :table => change_log_table,
117
+ :from => {'id' => current_id},
118
+ :exclude_starting_row => true,
119
+ :type_cast => true
120
+ )
121
+ while cursor.next?
122
+ change = cursor.next_row
123
+ self.current_id = change['id']
124
+ self.change_array << change
125
+ change['array_index'] = self.change_array.size - 1
126
+
127
+ table_change_tree = change_tree[change['change_table']] ||= {}
128
+ key_changes = table_change_tree[change['change_key']] ||= []
129
+ key_changes << change
130
+ end
131
+ cursor.clear
132
+ end
133
+
134
+ # Returns the creation time of the oldest unprocessed change log record.
135
+ def oldest_change_time
136
+ change = oldest_change
137
+ change['change_time'] if change
138
+ end
139
+
140
+ # Returns the oldest unprocessed change log record (column_name => value hash).
141
+ def oldest_change
142
+ update
143
+ oldest_change = nil
144
+ unless change_array.empty?
145
+ while (oldest_change = change_array[self.current_index]) == nil
146
+ self.current_index += 1
147
+ end
148
+ end
149
+ oldest_change
150
+ end
151
+
152
+ # Returns the specified change log record (column_name => value hash).
153
+ # * +change_table+: the name of the table that was changed
154
+ # * +change_key+: the change key of the modified record
155
+ def load(change_table, change_key)
156
+ update
157
+ change = nil
158
+ table_change_tree = change_tree[change_table]
159
+ if table_change_tree
160
+ key_changes = table_change_tree[change_key]
161
+ if key_changes
162
+ # get change object and delete from key_changes
163
+ change = key_changes.shift
164
+
165
+ # delete change from change_array
166
+ change_array[change['array_index']] = nil
167
+
168
+ # delete change from database
169
+ connection.execute "delete from #{change_log_table} where id = #{change['id']}"
170
+
171
+ # delete key_changes if empty
172
+ if key_changes.empty?
173
+ table_change_tree.delete change_key
174
+ end
175
+
176
+ # delete table_change_tree if empty
177
+ if table_change_tree.empty?
178
+ change_tree.delete change_table
179
+ end
180
+
181
+ # reset everything if no more changes remain
182
+ if change_tree.empty?
183
+ init_cache
184
+ end
185
+ end
186
+ end
187
+ change
188
+ end
189
+
190
+ # Returns the name of the change log table
191
+ def change_log_table
192
+ @change_log_table ||= "#{session.configuration.options[:rep_prefix]}_pending_changes"
193
+ end
194
+ private :change_log_table
195
+ end
196
+ end
@@ -0,0 +1,80 @@
1
+ module RR
2
+
3
+ # Wraps an existing cursor.
4
+ # Purpose: send regular updates to the installed TaskSweeper
5
+ class NoisyCursor
6
+ # The original cusor
7
+ attr_accessor :org_cursor
8
+
9
+ # The installed task sweeper
10
+ attr_accessor :sweeper
11
+
12
+ # Create a new NoisyCursor.
13
+ # * cursor: the original cursor
14
+ # * sweeper: the target TaskSweeper
15
+ def initialize(cursor, sweeper)
16
+ self.org_cursor = cursor
17
+ self.sweeper = sweeper
18
+ end
19
+
20
+ # Delegate the uninteresting methods to the original cursor
21
+ def next?; org_cursor.next? end
22
+ def clear; org_cursor.clear end
23
+
24
+ # Returns the row as a column => value hash and moves the cursor to the next row.
25
+ def next_row
26
+ sweeper.ping
27
+ row = org_cursor.next_row
28
+ sweeper.ping
29
+ row
30
+ end
31
+ end
32
+
33
+ # Modifies ProxyConnections to send regular pings to an installed TaskSweeper
34
+ module NoisyConnection
35
+
36
+ # The installed TaskSweeper
37
+ attr_accessor :sweeper
38
+
39
+ # Modifies ProxyConnection#select_cursor to wrap the returned cursor
40
+ # into a NoisyCursor.
41
+ def select_cursor(options)
42
+ sweeper.ping
43
+ org_cursor = super
44
+ sweeper.ping
45
+ NoisyCursor.new(org_cursor, sweeper)
46
+ end
47
+
48
+ # Wraps ProxyConnection#insert_record to update the TaskSweeper
49
+ def insert_record(table, values)
50
+ sweeper.ping
51
+ result = super
52
+ sweeper.ping
53
+ result
54
+ end
55
+
56
+ # Wraps ProxyConnection#update_record to update the TaskSweeper
57
+ def update_record(table, values, org_key = nil)
58
+ sweeper.ping
59
+ result = super
60
+ sweeper.ping
61
+ result
62
+ end
63
+
64
+ # Wraps ProxyConnection#delete_record to update the TaskSweeper
65
+ def delete_record(table, values)
66
+ sweeper.ping
67
+ result = super
68
+ sweeper.ping
69
+ result
70
+ end
71
+
72
+ # Wraps ProxyConnection#commit_db_transaction to update the TaskSweeper
73
+ def commit_db_transaction
74
+ sweeper.ping
75
+ result = super
76
+ sweeper.ping
77
+ result
78
+ end
79
+ end
80
+ end
@@ -198,21 +198,14 @@ module RR
198
198
  self.manual_primary_keys = {}
199
199
  end
200
200
 
201
- # Checks if the connection is still active and if not, reestablished it.
202
- def refresh
203
- unless self.connection.active?
204
- self.connection = ConnectionExtenders.db_connect config
205
- end
206
- end
207
-
208
201
  # Destroys the session
209
202
  def destroy
210
- self.connection.disconnect!
211
-
212
203
  cursors.each_key do |cursor|
213
204
  cursor.destroy
214
205
  end
215
206
  cursors.clear
207
+
208
+ self.connection.disconnect!
216
209
  end
217
210
 
218
211
  # Quotes the given value. It is assumed that the value belongs to the specified column name and table name.
@@ -5,7 +5,12 @@ module RR
5
5
  class ReplicationDifference
6
6
 
7
7
  # The current Session.
8
- attr_accessor :session
8
+ def session
9
+ @session ||= loaders.session
10
+ end
11
+
12
+ # The current LoggedChangeLoaders instance
13
+ attr_accessor :loaders
9
14
 
10
15
  # The type of the difference. Either
11
16
  # * :+left+: change in left database
@@ -21,9 +26,9 @@ module RR
21
26
  end
22
27
 
23
28
  # Creates a new ReplicationDifference instance.
24
- # +session+ is the current Session.
25
- def initialize(session)
26
- self.session = session
29
+ # +loaders+ is teh current LoggedChangeLoaders instance
30
+ def initialize(loaders)
31
+ self.loaders = loaders
27
32
  end
28
33
 
29
34
  # Should be set to +true+ if this ReplicationDifference instance was
@@ -52,7 +57,7 @@ module RR
52
57
 
53
58
  # Amends a difference according to new entries in the change log table
54
59
  def amend
55
- session.reload_changes
60
+ loaders.update
56
61
  changes[:left].load
57
62
  changes[:right].load
58
63
  self.type = DIFF_TYPES[changes[:left].type][changes[:right].type]
@@ -62,8 +67,8 @@ module RR
62
67
  def load
63
68
  change_times = {}
64
69
  [:left, :right].each do |database|
65
- changes[database] = LoggedChange.new session, database
66
- change_times[database] = changes[database].oldest_change_time
70
+ changes[database] = LoggedChange.new loaders[database]
71
+ change_times[database] = loaders[database].oldest_change_time
67
72
  end
68
73
  return if change_times[:left] == nil and change_times[:right] == nil
69
74
 
@@ -82,9 +87,9 @@ module RR
82
87
  self.loaded = true
83
88
  end
84
89
 
85
- # Prevents session from going into YAML output
90
+ # Prevents session and change loaders from going into YAML output
86
91
  def to_yaml_properties
87
- instance_variables.sort.reject {|var_name| var_name == '@session'}
92
+ instance_variables.sort.reject {|var_name| ['@session', '@loaders'].include? var_name}
88
93
  end
89
94
 
90
95
  end
@@ -262,6 +262,13 @@ module RR
262
262
  end
263
263
  end
264
264
 
265
+ # Calls the potentially provided :+after_init+ handler after infrastructure
266
+ # tables are created.
267
+ def call_after_infrastructure_setup_handler
268
+ handler = session.configuration.options[:after_infrastructure_setup]
269
+ handler.call(session) if handler
270
+ end
271
+
265
272
  # Prepares the database / tables for replication.
266
273
  def prepare_replication
267
274
  exclude_rubyrep_tables
@@ -269,6 +276,8 @@ module RR
269
276
  puts "Verifying RubyRep tables"
270
277
  ensure_infrastructure
271
278
 
279
+ call_after_infrastructure_setup_handler
280
+
272
281
  puts "Checking for and removing rubyrep triggers from unconfigured tables"
273
282
  restore_unconfigured_tables
274
283