rubyrep 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. data/History.txt +4 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +137 -0
  4. data/README.txt +37 -0
  5. data/Rakefile +30 -0
  6. data/bin/rubyrep +8 -0
  7. data/config/hoe.rb +72 -0
  8. data/config/mysql_config.rb +25 -0
  9. data/config/postgres_config.rb +21 -0
  10. data/config/proxied_test_config.rb +14 -0
  11. data/config/redmine_config.rb +17 -0
  12. data/config/rep_config.rb +20 -0
  13. data/config/requirements.rb +32 -0
  14. data/config/test_config.rb +20 -0
  15. data/lib/rubyrep/base_runner.rb +195 -0
  16. data/lib/rubyrep/command_runner.rb +144 -0
  17. data/lib/rubyrep/committers/buffered_committer.rb +140 -0
  18. data/lib/rubyrep/committers/committers.rb +146 -0
  19. data/lib/rubyrep/configuration.rb +240 -0
  20. data/lib/rubyrep/connection_extenders/connection_extenders.rb +133 -0
  21. data/lib/rubyrep/connection_extenders/jdbc_extender.rb +284 -0
  22. data/lib/rubyrep/connection_extenders/mysql_extender.rb +168 -0
  23. data/lib/rubyrep/connection_extenders/postgresql_extender.rb +261 -0
  24. data/lib/rubyrep/database_proxy.rb +52 -0
  25. data/lib/rubyrep/direct_table_scan.rb +75 -0
  26. data/lib/rubyrep/generate_runner.rb +105 -0
  27. data/lib/rubyrep/initializer.rb +39 -0
  28. data/lib/rubyrep/logged_change.rb +326 -0
  29. data/lib/rubyrep/proxied_table_scan.rb +171 -0
  30. data/lib/rubyrep/proxy_block_cursor.rb +145 -0
  31. data/lib/rubyrep/proxy_connection.rb +318 -0
  32. data/lib/rubyrep/proxy_cursor.rb +44 -0
  33. data/lib/rubyrep/proxy_row_cursor.rb +43 -0
  34. data/lib/rubyrep/proxy_runner.rb +89 -0
  35. data/lib/rubyrep/replication_difference.rb +91 -0
  36. data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
  37. data/lib/rubyrep/replication_extenders/postgresql_replication.rb +204 -0
  38. data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
  39. data/lib/rubyrep/replication_helper.rb +104 -0
  40. data/lib/rubyrep/replication_initializer.rb +307 -0
  41. data/lib/rubyrep/replication_run.rb +48 -0
  42. data/lib/rubyrep/replication_runner.rb +138 -0
  43. data/lib/rubyrep/replicators/replicators.rb +37 -0
  44. data/lib/rubyrep/replicators/two_way_replicator.rb +334 -0
  45. data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
  46. data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
  47. data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
  48. data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
  49. data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
  50. data/lib/rubyrep/scan_runner.rb +25 -0
  51. data/lib/rubyrep/session.rb +177 -0
  52. data/lib/rubyrep/sync_helper.rb +111 -0
  53. data/lib/rubyrep/sync_runner.rb +31 -0
  54. data/lib/rubyrep/syncers/syncers.rb +112 -0
  55. data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
  56. data/lib/rubyrep/table_scan.rb +54 -0
  57. data/lib/rubyrep/table_scan_helper.rb +38 -0
  58. data/lib/rubyrep/table_sorter.rb +70 -0
  59. data/lib/rubyrep/table_spec_resolver.rb +136 -0
  60. data/lib/rubyrep/table_sync.rb +68 -0
  61. data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
  62. data/lib/rubyrep/type_casting_cursor.rb +31 -0
  63. data/lib/rubyrep/uninstall_runner.rb +92 -0
  64. data/lib/rubyrep/version.rb +9 -0
  65. data/lib/rubyrep.rb +68 -0
  66. data/script/destroy +14 -0
  67. data/script/generate +14 -0
  68. data/script/txt2html +74 -0
  69. data/setup.rb +1585 -0
  70. data/sims/performance/big_rep_spec.rb +100 -0
  71. data/sims/performance/big_scan_spec.rb +57 -0
  72. data/sims/performance/big_sync_spec.rb +141 -0
  73. data/sims/performance/performance.rake +228 -0
  74. data/sims/sim_helper.rb +24 -0
  75. data/spec/base_runner_spec.rb +218 -0
  76. data/spec/buffered_committer_spec.rb +271 -0
  77. data/spec/command_runner_spec.rb +145 -0
  78. data/spec/committers_spec.rb +174 -0
  79. data/spec/configuration_spec.rb +198 -0
  80. data/spec/connection_extender_interface_spec.rb +138 -0
  81. data/spec/connection_extenders_registration_spec.rb +129 -0
  82. data/spec/database_proxy_spec.rb +48 -0
  83. data/spec/database_rake_spec.rb +40 -0
  84. data/spec/db_specific_connection_extenders_spec.rb +34 -0
  85. data/spec/db_specific_replication_extenders_spec.rb +38 -0
  86. data/spec/direct_table_scan_spec.rb +61 -0
  87. data/spec/generate_runner_spec.rb +84 -0
  88. data/spec/initializer_spec.rb +46 -0
  89. data/spec/logged_change_spec.rb +480 -0
  90. data/spec/postgresql_replication_spec.rb +48 -0
  91. data/spec/postgresql_support_spec.rb +57 -0
  92. data/spec/progress_bar_spec.rb +77 -0
  93. data/spec/proxied_table_scan_spec.rb +151 -0
  94. data/spec/proxy_block_cursor_spec.rb +197 -0
  95. data/spec/proxy_connection_spec.rb +399 -0
  96. data/spec/proxy_cursor_spec.rb +56 -0
  97. data/spec/proxy_row_cursor_spec.rb +66 -0
  98. data/spec/proxy_runner_spec.rb +70 -0
  99. data/spec/replication_difference_spec.rb +160 -0
  100. data/spec/replication_extender_interface_spec.rb +365 -0
  101. data/spec/replication_extenders_spec.rb +32 -0
  102. data/spec/replication_helper_spec.rb +121 -0
  103. data/spec/replication_initializer_spec.rb +477 -0
  104. data/spec/replication_run_spec.rb +166 -0
  105. data/spec/replication_runner_spec.rb +213 -0
  106. data/spec/replicators_spec.rb +31 -0
  107. data/spec/rubyrep_spec.rb +8 -0
  108. data/spec/scan_detail_reporter_spec.rb +119 -0
  109. data/spec/scan_progress_printers_spec.rb +68 -0
  110. data/spec/scan_report_printers_spec.rb +67 -0
  111. data/spec/scan_runner_spec.rb +50 -0
  112. data/spec/scan_summary_reporter_spec.rb +61 -0
  113. data/spec/session_spec.rb +212 -0
  114. data/spec/spec.opts +1 -0
  115. data/spec/spec_helper.rb +295 -0
  116. data/spec/sync_helper_spec.rb +157 -0
  117. data/spec/sync_runner_spec.rb +78 -0
  118. data/spec/syncers_spec.rb +171 -0
  119. data/spec/table_scan_helper_spec.rb +29 -0
  120. data/spec/table_scan_spec.rb +49 -0
  121. data/spec/table_sorter_spec.rb +31 -0
  122. data/spec/table_spec_resolver_spec.rb +102 -0
  123. data/spec/table_sync_spec.rb +84 -0
  124. data/spec/trigger_mode_switcher_spec.rb +83 -0
  125. data/spec/two_way_replicator_spec.rb +551 -0
  126. data/spec/two_way_syncer_spec.rb +256 -0
  127. data/spec/type_casting_cursor_spec.rb +50 -0
  128. data/spec/uninstall_runner_spec.rb +86 -0
  129. data/tasks/database.rake +439 -0
  130. data/tasks/deployment.rake +29 -0
  131. data/tasks/environment.rake +9 -0
  132. data/tasks/java.rake +37 -0
  133. data/tasks/redmine_test.rake +47 -0
  134. data/tasks/rspec.rake +68 -0
  135. data/tasks/rubyrep.tailor +18 -0
  136. data/tasks/stats.rake +19 -0
  137. data/tasks/task_helper.rb +20 -0
  138. data.tar.gz.sig +0 -0
  139. metadata +243 -0
  140. metadata.gz.sig +0 -0
@@ -0,0 +1,271 @@
1
+ module RR
2
+ module ReplicationExtenders
3
+
4
+ # Provides Mysql specific functionality for database replication
5
+ module MysqlReplication
6
+ RR::ReplicationExtenders.register :mysql => self
7
+
8
+ # Creates or replaces the replication trigger function.
9
+ # See #create_replication_trigger for a descriptions of the +params+ hash.
10
+ def create_or_replace_replication_trigger_function(params)
11
+ execute(<<-end_sql)
12
+ DROP PROCEDURE IF EXISTS #{params[:trigger_name]};
13
+ end_sql
14
+
15
+ activity_check = ""
16
+ if params[:exclude_rr_activity] then
17
+ activity_check = <<-end_sql
18
+ DECLARE active INT;
19
+ SELECT count(*) INTO active FROM #{params[:activity_table]};
20
+ IF active <> 0 THEN
21
+ LEAVE p;
22
+ END IF;
23
+ end_sql
24
+ end
25
+
26
+ execute(<<-end_sql)
27
+ CREATE PROCEDURE #{params[:trigger_name]}(change_key varchar(2000), change_new_key varchar(2000), change_type varchar(1))
28
+ p: BEGIN
29
+ #{activity_check}
30
+ INSERT INTO #{params[:log_table]}(change_table, change_key, change_new_key, change_type, change_time)
31
+ VALUES('#{params[:table]}', change_key, change_new_key, change_type, now());
32
+ END;
33
+ end_sql
34
+
35
+ end
36
+
37
+ # Returns the key clause that is used in the trigger function.
38
+ # * +trigger_var+: should be either 'NEW' or 'OLD'
39
+ # * +params+: the parameter hash as described in #create_rep_trigger
40
+ def key_clause(trigger_var, params)
41
+ "concat_ws('#{params[:key_sep]}', " +
42
+ params[:keys].map { |key| "'#{key}', #{trigger_var}.#{key}"}.join(", ") +
43
+ ")"
44
+ end
45
+ private :key_clause
46
+
47
+ # Creates a trigger to log all changes for the given table.
48
+ # +params+ is a hash with all necessary information:
49
+ # * :+trigger_name+: name of the trigger
50
+ # * :+table+: name of the table that should be monitored
51
+ # * :+keys+: array of names of the key columns of the monitored table
52
+ # * :+log_table+: name of the table receiving all change notifications
53
+ # * :+activity_table+: name of the table receiving the rubyrep activity information
54
+ # * :+key_sep+: column seperator to be used in the key column of the log table
55
+ # * :+exclude_rr_activity+:
56
+ # if true, the trigger will check and filter out changes initiated by RubyRep
57
+ def create_replication_trigger(params)
58
+ create_or_replace_replication_trigger_function params
59
+
60
+ %w(insert update delete).each do |action|
61
+ execute(<<-end_sql)
62
+ DROP TRIGGER IF EXISTS #{params[:trigger_name]}_#{action};
63
+ end_sql
64
+
65
+ # The created triggers can handle the case where the trigger procedure
66
+ # is updated (that is: temporarily deleted and recreated) while the
67
+ # trigger is running.
68
+ # For that an MySQL internal exception is raised if the trigger
69
+ # procedure cannot be found. The exception is caught by an trigger
70
+ # internal handler.
71
+ # The handler causes the trigger to retry calling the
72
+ # trigger procedure several times with short breaks in between.
73
+
74
+ trigger_var = action == 'delete' ? 'OLD' : 'NEW'
75
+ if action == 'update'
76
+ call_statement = "CALL #{params[:trigger_name]}(#{key_clause('OLD', params)}, #{key_clause('NEW', params)}, '#{action[0,1].upcase}');"
77
+ else
78
+ call_statement = "CALL #{params[:trigger_name]}(#{key_clause(trigger_var, params)}, null, '#{action[0,1].upcase}');"
79
+ end
80
+ execute(<<-end_sql)
81
+ CREATE TRIGGER #{params[:trigger_name]}_#{action}
82
+ AFTER #{action} ON #{params[:table]} FOR EACH ROW BEGIN
83
+ DECLARE number_attempts INT DEFAULT 0;
84
+ DECLARE failed INT;
85
+ DECLARE CONTINUE HANDLER FOR 1305 BEGIN
86
+ DO SLEEP(0.05);
87
+ SET failed = 1;
88
+ SET number_attempts = number_attempts + 1;
89
+ END;
90
+ REPEAT
91
+ SET failed = 0;
92
+ #{call_statement}
93
+ UNTIL failed = 0 OR number_attempts >= 40 END REPEAT;
94
+ END;
95
+ end_sql
96
+ end
97
+
98
+ end
99
+
100
+ # Removes a trigger and related trigger procedure.
101
+ # * +trigger_name+: name of the trigger
102
+ # * +table_name+: name of the table for which the trigger exists
103
+ def drop_replication_trigger(trigger_name, table_name)
104
+ %w(insert update delete).each do |action|
105
+ execute "DROP TRIGGER #{trigger_name}_#{action};"
106
+ end
107
+ execute "DROP PROCEDURE #{trigger_name};"
108
+ end
109
+
110
+ # Returns +true+ if the named trigger exists for the named table.
111
+ # * +trigger_name+: name of the trigger
112
+ # * +table_name+: name of the table
113
+ def replication_trigger_exists?(trigger_name, table_name)
114
+ !select_all("select 1 from information_schema.triggers where trigger_schema = database() and trigger_name = '#{trigger_name}_insert' and event_object_table = '#{table_name}'").empty?
115
+ end
116
+
117
+ # Returns all unadjusted sequences of the given table.
118
+ # Parameters:
119
+ # * +rep_prefix+:
120
+ # The prefix put in front of all replication related database objects as
121
+ # specified via Configuration#options.
122
+ # Is used to create the sequences table.
123
+ # * +table_name+: name of the table
124
+ # Return value: a hash with
125
+ # * key: sequence name
126
+ # * value: a hash with
127
+ # * :+increment+: current sequence increment
128
+ # * :+value+: current value
129
+ def sequence_values(rep_prefix, table_name)
130
+ # check if the table has an auto_increment column, return if not
131
+ sequence_row = select_one(<<-end_sql)
132
+ show columns from #{table_name} where extra = 'auto_increment'
133
+ end_sql
134
+ return {} unless sequence_row
135
+ column_name = sequence_row['Field']
136
+
137
+ # check if the sequences table exists, create if necessary
138
+ sequence_table_name = "#{rep_prefix}_sequences"
139
+ unless tables.include?(sequence_table_name)
140
+ create_table "#{sequence_table_name}".to_sym,
141
+ :id => false, :options => 'ENGINE=MyISAM' do |t|
142
+ t.column :name, :string
143
+ t.column :current_value, :integer
144
+ t.column :increment, :integer
145
+ t.column :offset, :integer
146
+ end
147
+ ActiveRecord::Base.connection.execute(<<-end_sql) rescue nil
148
+ ALTER TABLE "#{sequence_table_name}"
149
+ ADD CONSTRAINT #{sequence_table_name}_pkey
150
+ PRIMARY KEY (name)
151
+ end_sql
152
+ end
153
+
154
+ sequence_row = select_one("select current_value, increment, offset from #{sequence_table_name} where name = '#{table_name}'")
155
+ if sequence_row == nil
156
+ current_max = select_one(<<-end_sql)['current_max'].to_i
157
+ select max(#{column_name}) as current_max from #{table_name}
158
+ end_sql
159
+ return {column_name => {
160
+ :increment => 1,
161
+ :value => current_max
162
+ }
163
+ }
164
+ else
165
+ return {column_name => {
166
+ :increment => sequence_row['increment'].to_i,
167
+ :value => sequence_row['offset'].to_i
168
+ }
169
+ }
170
+ end
171
+ end
172
+
173
+ # Ensures that the sequences of the named table (normally the primary key
174
+ # column) are generated with the correct increment and offset.
175
+ # * +rep_prefix+: not used (necessary) for the Postgres
176
+ # * +table_name+: name of the table (not used for Postgres)
177
+ # * +increment+: increment of the sequence
178
+ # * +offset+: offset
179
+ # * +left_sequence_values+:
180
+ # hash as returned by #outdated_sequence_values for the left database
181
+ # * +right_sequence_values+:
182
+ # hash as returned by #outdated_sequence_values for the right database
183
+ # * +adjustment_buffer+:
184
+ # the "gap" that is created during sequence update to avoid concurrency problems
185
+ # E. g. an increment of 2 and offset of 1 will lead to generation of odd
186
+ # numbers.
187
+ def update_sequences(
188
+ rep_prefix, table_name, increment, offset,
189
+ left_sequence_values, right_sequence_values, adjustment_buffer)
190
+ return if left_sequence_values.empty?
191
+ column_name = left_sequence_values.keys[0]
192
+
193
+ # check if the sequences table exists, create if necessary
194
+ sequence_table_name = "#{rep_prefix}_sequences"
195
+ current_max =
196
+ [left_sequence_values[column_name][:value], right_sequence_values[column_name][:value]].max +
197
+ adjustment_buffer
198
+ new_start = current_max - (current_max % increment) + increment + offset
199
+
200
+ sequence_row = select_one("select current_value, increment, offset from #{sequence_table_name} where name = '#{table_name}'")
201
+ if sequence_row == nil
202
+ # no sequence exists yet for the table, create it and the according
203
+ # sequence trigger
204
+ execute(<<-end_sql)
205
+ insert into #{sequence_table_name}(name, current_value, increment, offset)
206
+ values('#{table_name}', #{new_start}, #{increment}, #{offset})
207
+ end_sql
208
+ trigger_name = "#{rep_prefix}_#{table_name}_sequence"
209
+ execute(<<-end_sql)
210
+ DROP TRIGGER IF EXISTS #{trigger_name};
211
+ end_sql
212
+
213
+ execute(<<-end_sql)
214
+ CREATE TRIGGER #{trigger_name}
215
+ BEFORE INSERT ON #{table_name} FOR EACH ROW BEGIN
216
+ IF NEW.#{column_name} = 0 THEN
217
+ UPDATE #{sequence_table_name}
218
+ SET current_value = LAST_INSERT_ID(current_value + increment)
219
+ WHERE name = '#{table_name}';
220
+ SET NEW.#{column_name} = LAST_INSERT_ID();
221
+ END IF;
222
+ END;
223
+ end_sql
224
+ elsif sequence_row['increment'].to_i != increment or sequence_row['offset'].to_i != offset
225
+ # sequence exists but with incorrect values; update it
226
+ execute(<<-end_sql)
227
+ update #{sequence_table_name}
228
+ set current_value = #{new_start},
229
+ increment = #{increment}, offset = #{offset}
230
+ where name = '#{table_name}'
231
+ end_sql
232
+ end
233
+ end
234
+
235
+ # Adds a big (8 byte value), auto-incrementing primary key column to the
236
+ # specified table.
237
+ # * table_name: name of the target table
238
+ # * key_name: name of the primary key column
239
+ def add_big_primary_key(table_name, key_name)
240
+ execute(<<-end_sql)
241
+ alter table #{table_name} add column #{key_name} bigint not null auto_increment primary key
242
+ end_sql
243
+ end
244
+
245
+ # Removes the custom sequence setup for the specified table.
246
+ # If no more rubyrep sequences are left, removes the sequence table.
247
+ # * +rep_prefix+: not used (necessary) for the Postgres
248
+ # * +table_name+: name of the table
249
+ def clear_sequence_setup(rep_prefix, table_name)
250
+ sequence_table_name = "#{rep_prefix}_sequences"
251
+ if tables.include?(sequence_table_name)
252
+ trigger_name = "#{rep_prefix}_#{table_name}_sequence"
253
+ trigger_row = select_one(<<-end_sql)
254
+ select * from information_schema.triggers
255
+ where trigger_schema = database()
256
+ and trigger_name = '#{trigger_name}'
257
+ end_sql
258
+ if trigger_row
259
+ execute "DROP TRIGGER #{trigger_name}"
260
+ execute "delete from #{sequence_table_name} where name = '#{table_name}'"
261
+ unless select_one("select * from #{sequence_table_name}")
262
+ # no more sequences left --> delete sequence table
263
+ drop_table sequence_table_name.to_sym
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end
271
+
@@ -0,0 +1,204 @@
1
+ module RR
2
+ module ReplicationExtenders
3
+
4
+ # Provides PostgreSQL specific functionality for database replication
5
+ module PostgreSQLReplication
6
+ RR::ReplicationExtenders.register :postgresql => self
7
+
8
+ # Returns the key clause that is used in the trigger function.
9
+ # * +trigger_var+: should be either 'NEW' or 'OLD'
10
+ # * +params+: the parameter hash as described in #create_rep_trigger
11
+ def key_clause(trigger_var, params)
12
+ params[:keys].
13
+ map { |key| "'#{key}#{params[:key_sep]}' || #{trigger_var}.#{key}"}.
14
+ join(" || '#{params[:key_sep]}' || ")
15
+ end
16
+ private :key_clause
17
+
18
+ # Creates or replaces the replication trigger function.
19
+ # See #create_replication_trigger for a descriptions of the +params+ hash.
20
+ def create_or_replace_replication_trigger_function(params)
21
+ # first check, if PL/SQL is already activated and if not, do so.
22
+ if select_all("select lanname from pg_language where lanname = 'plpgsql'").empty?
23
+ execute "CREATE LANGUAGE plpgsql"
24
+ end
25
+
26
+ activity_check = ""
27
+ if params[:exclude_rr_activity] then
28
+ activity_check = <<-end_sql
29
+ PERFORM ACTIVE FROM #{params[:activity_table]};
30
+ IF FOUND THEN
31
+ RETURN NULL;
32
+ END IF;
33
+ end_sql
34
+ end
35
+
36
+ # now create the trigger
37
+ execute(<<-end_sql)
38
+ CREATE OR REPLACE FUNCTION #{params[:trigger_name]}() RETURNS TRIGGER AS $change_trigger$
39
+ BEGIN
40
+ #{activity_check}
41
+ IF (TG_OP = 'DELETE') THEN
42
+ INSERT INTO #{params[:log_table]}(change_table, change_key, change_type, change_time)
43
+ SELECT '#{params[:table]}', #{key_clause('OLD', params)}, 'D', now();
44
+ ELSIF (TG_OP = 'UPDATE') THEN
45
+ INSERT INTO #{params[:log_table]}(change_table, change_key, change_new_key, change_type, change_time)
46
+ SELECT '#{params[:table]}', #{key_clause('OLD', params)}, #{key_clause('NEW', params)}, 'U', now();
47
+ ELSIF (TG_OP = 'INSERT') THEN
48
+ INSERT INTO #{params[:log_table]}(change_table, change_key, change_type, change_time)
49
+ SELECT '#{params[:table]}', #{key_clause('NEW', params)}, 'I', now();
50
+ END IF;
51
+ RETURN NULL; -- result is ignored since this is an AFTER trigger
52
+ END;
53
+ $change_trigger$ LANGUAGE plpgsql;
54
+ end_sql
55
+
56
+ end
57
+
58
+ # Creates a trigger to log all changes for the given table.
59
+ # +params+ is a hash with all necessary information:
60
+ # * :+trigger_name+: name of the trigger
61
+ # * :+table+: name of the table that should be monitored
62
+ # * :+keys+: array of names of the key columns of the monitored table
63
+ # * :+log_table+: name of the table receiving all change notifications
64
+ # * :+activity_table+: name of the table receiving the rubyrep activity information
65
+ # * :+key_sep+: column seperator to be used in the key column of the log table
66
+ # * :+exclude_rr_activity+:
67
+ # if true, the trigger will check and filter out changes initiated by RubyRep
68
+ def create_replication_trigger(params)
69
+ create_or_replace_replication_trigger_function params
70
+
71
+ execute(<<-end_sql)
72
+ CREATE TRIGGER #{params[:trigger_name]}
73
+ AFTER INSERT OR UPDATE OR DELETE ON #{params[:table]}
74
+ FOR EACH ROW EXECUTE PROCEDURE #{params[:trigger_name]}();
75
+ end_sql
76
+ end
77
+
78
+ # Removes a trigger and related trigger procedure.
79
+ # * +trigger_name+: name of the trigger
80
+ # * +table_name+: name of the table for which the trigger exists
81
+ def drop_replication_trigger(trigger_name, table_name)
82
+ execute "DROP TRIGGER #{trigger_name} ON #{table_name};"
83
+ execute "DROP FUNCTION #{trigger_name}();"
84
+ end
85
+
86
+ # Returns +true+ if the named trigger exists for the named table.
87
+ # * +trigger_name+: name of the trigger
88
+ # * +table_name+: name of the table
89
+ def replication_trigger_exists?(trigger_name, table_name)
90
+ search_path = select_one("show search_path")['search_path']
91
+ schemas = search_path.split(/,/).map { |p| quote(p) }.join(',')
92
+ !select_all(<<-end_sql).empty?
93
+ select 1 from information_schema.triggers
94
+ where event_object_schema in (#{schemas})
95
+ and trigger_name = '#{trigger_name}'
96
+ and event_object_table = '#{table_name}'
97
+ end_sql
98
+ end
99
+
100
+ # Returns all unadjusted sequences of the given table.
101
+ # Parameters:
102
+ # * +rep_prefix+: not used (necessary) for the Postgres
103
+ # * +table_name+: name of the table
104
+ # Return value: a hash with
105
+ # * key: sequence name
106
+ # * value: a hash with
107
+ # * :+increment+: current sequence increment
108
+ # * :+value+: current value
109
+ def sequence_values(rep_prefix, table_name)
110
+ result = {}
111
+ sequence_names = select_all(<<-end_sql).map { |row| row['relname'] }
112
+ select s.relname
113
+ from pg_class as t
114
+ join pg_depend as r on t.oid = r.refobjid
115
+ join pg_class as s on r.objid = s.oid
116
+ and s.relkind = 'S'
117
+ and t.relname = '#{table_name}'
118
+ end_sql
119
+ sequence_names.each do |sequence_name|
120
+ row = select_one("select last_value, increment_by from #{sequence_name}")
121
+ result[sequence_name] = {
122
+ :increment => row['increment_by'].to_i,
123
+ :value => row['last_value'].to_i
124
+ }
125
+ end
126
+ result
127
+ end
128
+
129
+ # Ensures that the sequences of the named table (normally the primary key
130
+ # column) are generated with the correct increment and offset.
131
+ # * +rep_prefix+: not used (necessary) for the Postgres
132
+ # * +table_name+: name of the table (not used for Postgres)
133
+ # * +increment+: increment of the sequence
134
+ # * +offset+: offset
135
+ # * +left_sequence_values+:
136
+ # hash as returned by #outdated_sequence_values for the left database
137
+ # * +right_sequence_values+:
138
+ # hash as returned by #outdated_sequence_values for the right database
139
+ # * +adjustment_buffer+:
140
+ # the "gap" that is created during sequence update to avoid concurrency problems
141
+ # E. g. an increment of 2 and offset of 1 will lead to generation of odd
142
+ # numbers.
143
+ def update_sequences(
144
+ rep_prefix, table_name, increment, offset,
145
+ left_sequence_values, right_sequence_values, adjustment_buffer)
146
+ left_sequence_values.each do |sequence_name, left_current_value|
147
+ row = select_one("select last_value, increment_by from #{sequence_name}")
148
+ current_increment = row['increment_by'].to_i
149
+ current_value = row['last_value'].to_i
150
+ unless current_increment == increment and current_value % increment == offset
151
+ max_current_value =
152
+ [left_current_value[:value], right_sequence_values[sequence_name][:value]].max +
153
+ adjustment_buffer
154
+ new_start = max_current_value - (max_current_value % increment) + increment + offset
155
+ execute(<<-end_sql)
156
+ alter sequence "#{sequence_name}" increment by #{increment} restart with #{new_start}
157
+ end_sql
158
+ end
159
+ end
160
+ end
161
+
162
+ # Restores the original sequence settings.
163
+ # (Actually it sets the sequence increment to 1. If before, it had a
164
+ # different value, then the restoration will not be correct.)
165
+ # * +rep_prefix+: not used (necessary) for the Postgres
166
+ # * +table_name+: name of the table
167
+ def clear_sequence_setup(rep_prefix, table_name)
168
+ sequence_names = select_all(<<-end_sql).map { |row| row['relname'] }
169
+ select s.relname
170
+ from pg_class as t
171
+ join pg_depend as r on t.oid = r.refobjid
172
+ join pg_class as s on r.objid = s.oid
173
+ and s.relkind = 'S'
174
+ and t.relname = '#{table_name}'
175
+ end_sql
176
+ sequence_names.each do |sequence_name|
177
+ execute(<<-end_sql)
178
+ alter sequence "#{sequence_name}" increment by 1
179
+ end_sql
180
+ end
181
+ end
182
+
183
+ # Adds a big (8 byte value), auto-incrementing primary key column to the
184
+ # specified table.
185
+ # * table_name: name of the target table
186
+ # * key_name: name of the primary key column
187
+ def add_big_primary_key(table_name, key_name)
188
+ old_message_level = select_one("show client_min_messages")['client_min_messages']
189
+ execute "set client_min_messages = warning"
190
+ execute(<<-end_sql)
191
+ alter table "#{table_name}" add column #{key_name} bigserial
192
+ end_sql
193
+
194
+ execute(<<-end_sql)
195
+ alter table "#{table_name}" add constraint #{table_name}_#{key_name}_pkey primary key (#{key_name})
196
+ end_sql
197
+
198
+ ensure
199
+ execute "set client_min_messages = #{old_message_level}"
200
+ end
201
+ end
202
+ end
203
+ end
204
+
@@ -0,0 +1,26 @@
1
+ module RR
2
+
3
+ # Replication extenders are modules that provide database specific functionality
4
+ # required for replication. They are mixed into ActiveRecord database connections.
5
+ # This module itself only provides functionality to register and retrieve
6
+ # such extenders.
7
+ module ReplicationExtenders
8
+ # Returns a Hash of currently registered replication extenders.
9
+ # (Empty Hash if no replication extenders were defined.)
10
+ def self.extenders
11
+ @extenders ||= {}
12
+ @extenders
13
+ end
14
+
15
+ # Registers one or multiple replication extender.
16
+ # extender is a Hash with
17
+ # key:: The adapter symbol as used by ActiveRecord::Connection Adapters, e. g. :postgresql
18
+ # value:: Name of the module implementing the replication extender
19
+ def self.register(extender)
20
+ @extenders ||= {}
21
+ @extenders.merge! extender
22
+ end
23
+ end
24
+ end
25
+
26
+
@@ -0,0 +1,104 @@
1
+ module RR
2
+
3
+ # Provides helper functionality for replicators.
4
+ # The methods exposed by this class are intended to provide a stable interface
5
+ # for third party replicators.
6
+ class ReplicationHelper
7
+
8
+ # The current +ReplicationRun+ instance
9
+ attr_accessor :replication_run
10
+
11
+ # The active +Session+
12
+ def session; replication_run.session; end
13
+
14
+ # Current options
15
+ def options; @options ||= session.configuration.options; end
16
+
17
+ # Delegates to Session#corresponding_table
18
+ def corresponding_table(db_arm, table); session.corresponding_table(db_arm, table); end
19
+
20
+ # Delegates to Committer#insert_record
21
+ def insert_record(database, table, values)
22
+ committer.insert_record(database, table, values)
23
+ end
24
+
25
+ # Delegates to Committer#insert_record
26
+ def update_record(database, table, values, old_key = nil)
27
+ committer.update_record(database, table, values, old_key)
28
+ end
29
+
30
+ # Delegates to Committer#insert_record
31
+ def delete_record(database, table, values)
32
+ committer.delete_record(database, table, values)
33
+ end
34
+
35
+ # Loads the specified record. Returns an according column_name => value hash.
36
+ # Parameters:
37
+ # * +database+: either :+left+ or :+right+
38
+ # * +table+: name of the table
39
+ # * +key+: A column_name => value hash for all primary key columns.
40
+ def load_record(database, table, key)
41
+ cursor = session.send(database).select_cursor(
42
+ :table => table,
43
+ :row_keys => [key],
44
+ :type_cast => true
45
+ )
46
+ row = nil
47
+ row = cursor.next_row if cursor.next?
48
+ cursor.clear
49
+ row
50
+ end
51
+
52
+ # The current Committer
53
+ attr_reader :committer
54
+ private :committer
55
+
56
+ # Asks the committer (if it exists) to finalize any open transactions
57
+ # +success+ should be true if there were no problems, false otherwise.
58
+ def finalize(success = true)
59
+ committer.finalize(success)
60
+ end
61
+
62
+ # Logs the outcome of a replication into the replication log table.
63
+ # * +diff+: the replicated ReplicationDifference
64
+ # * +outcome+: string summarizing the outcome of the replication
65
+ # * +details+: string with further details regarding the replication
66
+ def log_replication_outcome(diff, outcome, details = nil)
67
+ table = diff.changes[:left].table
68
+ key = diff.changes[:left].key
69
+ if key.size == 1
70
+ key = key.values[0]
71
+ else
72
+ key_parts = session.left.primary_key_names(table).map do |column_name|
73
+ %Q("#{column_name}"=>#{key[column_name].to_s.inspect})
74
+ end
75
+ key = key_parts.join(', ')
76
+ end
77
+ rep_details = details == nil ? nil : details[0...ReplicationInitializer::LONG_DESCRIPTION_SIZE]
78
+ diff_dump = diff.to_yaml[0...ReplicationInitializer::DIFF_DUMP_SIZE]
79
+
80
+ session.left.insert_record "#{options[:rep_prefix]}_logged_events", {
81
+ :activity => 'replication',
82
+ :change_table => table,
83
+ :diff_type => diff.type.to_s,
84
+ :change_key => key,
85
+ :left_change_type => (diff.changes[:left] ? diff.changes[:left].type.to_s : nil),
86
+ :right_change_type => (diff.changes[:right] ? diff.changes[:right].type.to_s : nil),
87
+ :description => outcome.to_s,
88
+ :long_description => rep_details,
89
+ :event_time => Time.now,
90
+ :diff_dump => diff_dump
91
+ }
92
+ end
93
+
94
+ # Creates a new SyncHelper for the given +TableSync+ instance.
95
+ def initialize(replication_run)
96
+ self.replication_run = replication_run
97
+
98
+ # Creates the committer. Important as it gives the committer the
99
+ # opportunity to start transactions
100
+ committer_class = Committers::committers[options[:committer]]
101
+ @committer = committer_class.new(session)
102
+ end
103
+ end
104
+ end