rubyrep 1.0.5 → 1.0.6

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