rails_redshift_replicator 0.0.1
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +34 -0
- data/app/assets/javascripts/rails_redshift_replicator/application.js +13 -0
- data/app/assets/stylesheets/rails_redshift_replicator/application.css +15 -0
- data/app/controllers/rails_redshift_replicator/application_controller.rb +5 -0
- data/app/helpers/rails_redshift_replicator/application_helper.rb +4 -0
- data/app/models/rails_redshift_replicator/replication.rb +98 -0
- data/app/views/layouts/rails_redshift_replicator/application.html.erb +14 -0
- data/config/locales/rails_redshift_replicator.en.yml +20 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20160503214955_create_rails_redshift_replicator_replications.rb +24 -0
- data/db/migrate/20160509193335_create_table_rails_redshift_replicator_deleted_ids.rb +8 -0
- data/lib/generators/rails_redshift_replicator/install_generator.rb +25 -0
- data/lib/generators/templates/rails_redshift_replicator.rb +74 -0
- data/lib/rails_redshift_replicator.rb +229 -0
- data/lib/rails_redshift_replicator/adapters/generic.rb +40 -0
- data/lib/rails_redshift_replicator/adapters/mysql2.rb +22 -0
- data/lib/rails_redshift_replicator/adapters/postgresql.rb +37 -0
- data/lib/rails_redshift_replicator/adapters/sqlite.rb +27 -0
- data/lib/rails_redshift_replicator/deleter.rb +67 -0
- data/lib/rails_redshift_replicator/engine.rb +14 -0
- data/lib/rails_redshift_replicator/exporters/base.rb +215 -0
- data/lib/rails_redshift_replicator/exporters/full_replicator.rb +9 -0
- data/lib/rails_redshift_replicator/exporters/identity_replicator.rb +9 -0
- data/lib/rails_redshift_replicator/exporters/timed_replicator.rb +9 -0
- data/lib/rails_redshift_replicator/file_manager.rb +134 -0
- data/lib/rails_redshift_replicator/importers/base.rb +158 -0
- data/lib/rails_redshift_replicator/importers/full_replicator.rb +17 -0
- data/lib/rails_redshift_replicator/importers/identity_replicator.rb +15 -0
- data/lib/rails_redshift_replicator/importers/timed_replicator.rb +18 -0
- data/lib/rails_redshift_replicator/model/extension.rb +45 -0
- data/lib/rails_redshift_replicator/model/hair_trigger_extension.rb +8 -0
- data/lib/rails_redshift_replicator/replicable.rb +143 -0
- data/lib/rails_redshift_replicator/rlogger.rb +12 -0
- data/lib/rails_redshift_replicator/tools/analyze.rb +18 -0
- data/lib/rails_redshift_replicator/tools/vacuum.rb +77 -0
- data/lib/rails_redshift_replicator/version.rb +3 -0
- data/lib/tasks/rails_redshift_replicator_tasks.rake +4 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/post.rb +4 -0
- data/spec/dummy/app/models/tag.rb +4 -0
- data/spec/dummy/app/models/user.rb +5 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +26 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +37 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/rails_redshift_replicator.rb +59 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/locales/rails_redshift_replicator.en.yml +19 -0
- data/spec/dummy/config/routes.rb +4 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20160504120421_create_test_tables.rb +40 -0
- data/spec/dummy/db/migrate/20160509225445_create_triggers_posts_delete_or_tags_delete_or_users_delete.rb +33 -0
- data/spec/dummy/db/migrate/20160511000937_create_rails_redshift_replicator_replications.rails_redshift_replicator.rb +25 -0
- data/spec/dummy/db/migrate/20160511000938_create_table_rails_redshift_replicator_deleted_ids.rails_redshift_replicator.rb +9 -0
- data/spec/dummy/db/schema.rb +99 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +1623 -0
- data/spec/dummy/log/test.log +95379 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/rails_redshift_replicator_development +0 -0
- data/spec/factories/rails_redshift_replicator_replications.rb +31 -0
- data/spec/integration/rails_redshift_replicator_spec.rb +148 -0
- data/spec/integration/setup_spec.rb +149 -0
- data/spec/lib/rails_redshift_replicator/deleter_spec.rb +90 -0
- data/spec/lib/rails_redshift_replicator/exporters/base_spec.rb +326 -0
- data/spec/lib/rails_redshift_replicator/exporters/full_replicator_spec.rb +33 -0
- data/spec/lib/rails_redshift_replicator/exporters/identity_replicator_spec.rb +40 -0
- data/spec/lib/rails_redshift_replicator/exporters/timed_replicator_spec.rb +43 -0
- data/spec/lib/rails_redshift_replicator/file_manager_spec.rb +90 -0
- data/spec/lib/rails_redshift_replicator/importers/base_spec.rb +102 -0
- data/spec/lib/rails_redshift_replicator/importers/full_replicator_spec.rb +27 -0
- data/spec/lib/rails_redshift_replicator/importers/identity_replicator_spec.rb +26 -0
- data/spec/lib/rails_redshift_replicator/importers/timed_replicator_spec.rb +26 -0
- data/spec/lib/rails_redshift_replicator/model/extension_spec.rb +36 -0
- data/spec/lib/rails_redshift_replicator/replicable_spec.rb +230 -0
- data/spec/lib/rails_redshift_replicator/rlogger_spec.rb +22 -0
- data/spec/lib/rails_redshift_replicator/tools/analyze_spec.rb +15 -0
- data/spec/lib/rails_redshift_replicator/tools/vacuum_spec.rb +65 -0
- data/spec/lib/rails_redshift_replicator_spec.rb +110 -0
- data/spec/models/rails_redshift_replicator/replication_spec.rb +104 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/support/csv/invalid_user.csv +12 -0
- data/spec/support/csv/valid_post.csv +2 -0
- data/spec/support/csv/valid_tags_users.csv +1 -0
- data/spec/support/csv/valid_user.csv +2 -0
- data/spec/support/rails_redshift_replicator_helpers.rb +95 -0
- metadata +430 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require 'aws-sdk'
|
|
2
|
+
module RailsRedshiftReplicator
|
|
3
|
+
class FileManager
|
|
4
|
+
attr_reader :exporter
|
|
5
|
+
|
|
6
|
+
def s3_client
|
|
7
|
+
@client ||= Aws::S3::Client.new(
|
|
8
|
+
region: RailsRedshiftReplicator.s3_bucket_params[:region],
|
|
9
|
+
access_key_id: RailsRedshiftReplicator.aws_credentials[:key],
|
|
10
|
+
secret_access_key: RailsRedshiftReplicator.aws_credentials[:secret]
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# File location on s3
|
|
15
|
+
# @return [String] file location
|
|
16
|
+
def self.s3_file_key(source_table, file)
|
|
17
|
+
File.join RailsRedshiftReplicator.s3_bucket_params[:prefix], source_table, file
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(exporter = nil)
|
|
21
|
+
@exporter = exporter
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def bucket
|
|
25
|
+
RailsRedshiftReplicator.s3_bucket_params[:bucket]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def delete
|
|
29
|
+
response = s3_replication_files
|
|
30
|
+
if response.contents
|
|
31
|
+
response.contents.each do |file|
|
|
32
|
+
RailsRedshiftReplicator.logger.info I18n.t(:deleting_file, key: file.key, scope: :rails_redshift_replicator)
|
|
33
|
+
s3_client.delete_object(bucket: bucket, key: file.key)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def s3_replication_files
|
|
39
|
+
s3_client.list_objects(bucket: bucket, prefix: exporter.replication.key)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def temp_file_name
|
|
43
|
+
"#{exporter.source_table}_#{Time.now.to_i}.csv"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Writes all results to one file for future splitting.
|
|
47
|
+
# @param file_name [String] name of the local export file
|
|
48
|
+
# @return [Integer] number of records to export.
|
|
49
|
+
def write_csv(file_name, records)
|
|
50
|
+
line_number = exporter.connection_adapter.write(local_file(file_name), records)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Path to the local export file
|
|
54
|
+
# @param name [String] file name
|
|
55
|
+
# @return [String] path to file
|
|
56
|
+
def local_file(name)
|
|
57
|
+
@local_file ||= "#{RailsRedshiftReplicator.local_replication_path}/#{name}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Splits the CSV into a number of files determined by the number of Redshift Slices
|
|
61
|
+
# @note This method requires an executable split and is compliant with Mac and Linux versions of it.
|
|
62
|
+
# @param name [String] file name
|
|
63
|
+
# @param counts [Integer] number of files
|
|
64
|
+
def split_file(name, record_count)
|
|
65
|
+
counts = row_count_threshold(record_count)
|
|
66
|
+
file_name = local_file(name)
|
|
67
|
+
`#{RailsRedshiftReplicator.split_command} -l #{counts} #{file_name} #{file_name}.`
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Number of lines per file
|
|
71
|
+
# @param counts [Integer] number of records
|
|
72
|
+
# @return [Integer] Number of lines per export file
|
|
73
|
+
def row_count_threshold(counts)
|
|
74
|
+
(counts.to_f/exporter.replication.slices).ceil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Returns the s3 key to be used
|
|
79
|
+
# @return [String] file key with extension
|
|
80
|
+
def file_key_in_format(file_name, format)
|
|
81
|
+
if format == "gzip"
|
|
82
|
+
self.class.s3_file_key exporter.source_table, gzipped(file_name)
|
|
83
|
+
else
|
|
84
|
+
self.class.s3_file_key exporter.source_table, file_name
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Rename file to use .gz extension
|
|
89
|
+
# @return [String]
|
|
90
|
+
def gzipped(file)
|
|
91
|
+
file.gsub(".csv", ".gz")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def upload_gzip(files)
|
|
95
|
+
without_base = files_without_base(files)
|
|
96
|
+
without_base.each do |file|
|
|
97
|
+
basename = File.basename(file)
|
|
98
|
+
command = "#{RailsRedshiftReplicator.gzip_command} -c #{file} > #{gzipped(file)}"
|
|
99
|
+
RailsRedshiftReplicator.logger.info I18n.t(:gzip_notice, file: file, gzip_file: gzipped(file), command: command, scope: :rails_redshift_replicator)
|
|
100
|
+
`#{command}`
|
|
101
|
+
s3_client.put_object(
|
|
102
|
+
key: self.class.s3_file_key(exporter.source_table, gzipped(basename)),
|
|
103
|
+
body: File.open(gzipped(file)),
|
|
104
|
+
bucket: bucket
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
files.each { |f| FileUtils.rm f }
|
|
108
|
+
without_base.each { |f| FileUtils.rm gzipped(f) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def files_without_base(files)
|
|
112
|
+
files.reject{|f| f.split('.').last.in? %w(gz csv)}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Uploads splitted CSVs
|
|
116
|
+
# @param files [Array<String>] array of files paths to upload
|
|
117
|
+
def upload_csv(files)
|
|
118
|
+
files.each do |file|
|
|
119
|
+
basename = File.basename(file)
|
|
120
|
+
next if basename == File.basename(exporter.replication.key)
|
|
121
|
+
RailsRedshiftReplicator.logger.info I18n.t(:uploading_notice,
|
|
122
|
+
file: file,
|
|
123
|
+
key: self.class.s3_file_key(exporter.source_table, basename),
|
|
124
|
+
scope: :rails_redshift_replicator)
|
|
125
|
+
s3_client.put_object(
|
|
126
|
+
key: self.class.s3_file_key(exporter.source_table, basename),
|
|
127
|
+
body: File.open(file),
|
|
128
|
+
bucket: bucket
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
files.each { |f| FileUtils.rm f }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
require 'pg'
|
|
2
|
+
module RailsRedshiftReplicator
|
|
3
|
+
module Importers
|
|
4
|
+
class Base
|
|
5
|
+
attr_accessor :replication
|
|
6
|
+
def initialize(replication)
|
|
7
|
+
return if replication.blank?
|
|
8
|
+
@replication = replication
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def import
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# History Cap has a minimum of 2
|
|
16
|
+
def evaluate_history_cap
|
|
17
|
+
if cap = RailsRedshiftReplicator.history_cap
|
|
18
|
+
RailsRedshiftReplicator::Replication.older_than(replication.source_table, cap).delete_all
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def file_manager
|
|
23
|
+
@file_manager ||= RailsRedshiftReplicator::FileManager.new(self)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Runs Redshift COPY command to import data from S3
|
|
27
|
+
# (http://docs.aws.amazon.com/redshift/latest/dg/r_COPY.html)
|
|
28
|
+
# @param [String] table name
|
|
29
|
+
# @param options [Hash]
|
|
30
|
+
# @option options [Boolean] :mark_as_imported If record should be flagged as imported
|
|
31
|
+
# @option options [Boolean] :noload If true, data will be validated but not imported
|
|
32
|
+
def copy(table_name = replication.target_table, options = {})
|
|
33
|
+
begin
|
|
34
|
+
RailsRedshiftReplicator.logger.info I18n.t(:importing_file, file: import_file, target_table: table_name, scope: :rails_redshift_replicator)
|
|
35
|
+
result = ::RailsRedshiftReplicator.connection.exec copy_statement(table_name, options)
|
|
36
|
+
replication.imported! if result.result_status == 1 && options[:mark_as_imported]
|
|
37
|
+
rescue => e
|
|
38
|
+
drop_table(table_name) if options[:can_drop_target_on_error]
|
|
39
|
+
if e.message.index("stl_load_errors")
|
|
40
|
+
get_redshift_error
|
|
41
|
+
notify_error
|
|
42
|
+
else
|
|
43
|
+
replication.update_attribute :last_error, e.exception.inspect
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Builds the copy statement
|
|
49
|
+
# @param (see #copy)
|
|
50
|
+
# @return [String] sql statement to run
|
|
51
|
+
def copy_statement(table_name, options = {})
|
|
52
|
+
format_options = replication.csv? ? "CSV" : "GZIP DELIMITER ',' ESCAPE REMOVEQUOTES"
|
|
53
|
+
sql = <<-CS
|
|
54
|
+
COPY #{table_name} from '#{import_file}' #{"NOLOAD" if options[:noload]}
|
|
55
|
+
REGION '#{RailsRedshiftReplicator.s3_bucket_params[:region]}'
|
|
56
|
+
CREDENTIALS 'aws_access_key_id=#{RailsRedshiftReplicator.aws_credentials[:key]};aws_secret_access_key=#{RailsRedshiftReplicator.aws_credentials[:secret]}'
|
|
57
|
+
#{format_options}
|
|
58
|
+
#{copy_options}
|
|
59
|
+
CS
|
|
60
|
+
sql.squish
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def copy_options
|
|
64
|
+
RailsRedshiftReplicator.copy_options.values.join(" ")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [String] location of import files on s3
|
|
68
|
+
def import_file
|
|
69
|
+
"s3://#{RailsRedshiftReplicator.s3_bucket_params[:bucket]}/#{replication.key}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Retrieves the last copy error for a given file on redshift
|
|
73
|
+
def get_redshift_error
|
|
74
|
+
sql = <<-RE.squish
|
|
75
|
+
SELECT filename, line_number, colname, type, raw_field_value, raw_line, err_reason
|
|
76
|
+
FROM STL_LOAD_ERRORS
|
|
77
|
+
WHERE filename like '%#{import_file}%'
|
|
78
|
+
ORDER BY starttime desc
|
|
79
|
+
LIMIT 1
|
|
80
|
+
RE
|
|
81
|
+
result = ::RailsRedshiftReplicator.connection.exec(sql).entries
|
|
82
|
+
error = result.first.map{ |k, v| [k, v.strip].join('=>') }.join(";")
|
|
83
|
+
replication.update_attribute :last_error, error
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# TODO
|
|
87
|
+
def notify_error
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Creates a temporary table on redshift
|
|
91
|
+
def create_temp_table
|
|
92
|
+
RailsRedshiftReplicator.connection.exec "CREATE TEMP TABLE #{temporary_table_name} (LIKE #{replication.target_table})"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Creates a permanent table for later renaming
|
|
96
|
+
def create_side_table
|
|
97
|
+
RailsRedshiftReplicator.connection.exec "CREATE TABLE #{temporary_table_name} (LIKE #{replication.target_table})"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Runs a merge or replace operation on a redshift table
|
|
101
|
+
# The table is replaced on a FullReplicator strategy
|
|
102
|
+
# The table is merged on a TimedReplicator strategy
|
|
103
|
+
# @param :mode [Symbol] the operation type
|
|
104
|
+
def merge_or_replace(mode:)
|
|
105
|
+
target = replication.target_table
|
|
106
|
+
stage = temporary_table_name
|
|
107
|
+
sql = send("#{mode}_statement", target, stage)
|
|
108
|
+
::RailsRedshiftReplicator.connection.exec sql
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Builds the merge sql statement.
|
|
112
|
+
# At first, it deletes the matching records from the target and temporary tables on the target table.
|
|
113
|
+
# After it imports everything from the temporary table into the target table.
|
|
114
|
+
# @param target [String]
|
|
115
|
+
# @param stage [String] temporary table
|
|
116
|
+
# @return [String] Sql Statement
|
|
117
|
+
# (http://docs.aws.amazon.com/redshift/latest/dg/merge-replacing-existing-rows.html)
|
|
118
|
+
def merge_statement(target, stage)
|
|
119
|
+
<<-SQLMERGE
|
|
120
|
+
begin transaction;
|
|
121
|
+
|
|
122
|
+
delete from #{target}
|
|
123
|
+
using #{stage}
|
|
124
|
+
where #{target}.id = #{stage}.id;
|
|
125
|
+
insert into #{target}
|
|
126
|
+
select * from #{stage};
|
|
127
|
+
|
|
128
|
+
end transaction;
|
|
129
|
+
SQLMERGE
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Builds the replace sql statement.
|
|
133
|
+
# @param (see #merge_statement)
|
|
134
|
+
# @return (see #merge_statement)
|
|
135
|
+
# (http://docs.aws.amazon.com/redshift/latest/dg/performing-a-deep-copy.html)
|
|
136
|
+
def replace_statement(target, stage)
|
|
137
|
+
<<-SQLREPLACE
|
|
138
|
+
begin transaction;
|
|
139
|
+
drop table #{target};
|
|
140
|
+
alter table #{stage} rename to #{target};
|
|
141
|
+
end transaction;
|
|
142
|
+
SQLREPLACE
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Deletes the temporary table
|
|
146
|
+
# @param table_name [String]
|
|
147
|
+
def drop_table(table_name = temporary_table_name)
|
|
148
|
+
::RailsRedshiftReplicator.connection.exec "drop table if exists #{table_name}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Returns a random name for a temporary table
|
|
152
|
+
# @return [String] table name
|
|
153
|
+
def temporary_table_name
|
|
154
|
+
@temp_table ||= "temp_#{replication.target_table}_#{Time.now.to_i}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module RailsRedshiftReplicator
|
|
2
|
+
module Importers
|
|
3
|
+
class FullReplicator < Base
|
|
4
|
+
def import
|
|
5
|
+
import_start = replication.importing!
|
|
6
|
+
create_side_table
|
|
7
|
+
copy temporary_table_name, mark_as_imported: false, can_drop_target_on_error: true
|
|
8
|
+
return if replication.error?
|
|
9
|
+
merge_or_replace(mode: :replace)
|
|
10
|
+
replication.clear_errors!
|
|
11
|
+
replication.imported! import_duration: (Time.now-import_start).ceil
|
|
12
|
+
evaluate_history_cap
|
|
13
|
+
file_manager.delete if RailsRedshiftReplicator.delete_s3_file_after_import
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module RailsRedshiftReplicator
|
|
2
|
+
module Importers
|
|
3
|
+
class IdentityReplicator < Base
|
|
4
|
+
def import
|
|
5
|
+
import_start = replication.importing!
|
|
6
|
+
copy replication.target_table, mark_as_imported: true
|
|
7
|
+
return if replication.error?
|
|
8
|
+
replication.clear_errors!
|
|
9
|
+
replication.update_attributes import_duration: (Time.now-import_start).ceil
|
|
10
|
+
evaluate_history_cap
|
|
11
|
+
file_manager.delete if RailsRedshiftReplicator.delete_s3_file_after_import
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module RailsRedshiftReplicator
|
|
2
|
+
module Importers
|
|
3
|
+
class TimedReplicator < Base
|
|
4
|
+
def import
|
|
5
|
+
import_start = replication.importing!
|
|
6
|
+
create_temp_table
|
|
7
|
+
copy temporary_table_name, mark_as_imported: false, can_drop_target_on_error: true
|
|
8
|
+
return if replication.error?
|
|
9
|
+
merge_or_replace(mode: :merge)
|
|
10
|
+
drop_table temporary_table_name
|
|
11
|
+
replication.clear_errors!
|
|
12
|
+
replication.imported! import_duration: (Time.now-import_start).ceil
|
|
13
|
+
evaluate_history_cap
|
|
14
|
+
file_manager.delete if RailsRedshiftReplicator.delete_s3_file_after_import
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module RailsRedshiftReplicator
|
|
2
|
+
module Model
|
|
3
|
+
module Extension
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.send :extend, ClassMethods
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def has_redshift_replication(replication_type, options = {})
|
|
10
|
+
cattr_accessor :rails_redshift_replicator_replicable
|
|
11
|
+
replication_type = replication_type.to_s
|
|
12
|
+
raise I18n.t(:replication_type_not_supported,
|
|
13
|
+
replication_type: replication_type,
|
|
14
|
+
types: RailsRedshiftReplicator.base_exporter_types.join(","),
|
|
15
|
+
scope: :exception_messages) unless replication_type.in? RailsRedshiftReplicator.base_exporter_types
|
|
16
|
+
extend Actions
|
|
17
|
+
options[:source_table] ||= self.table_name
|
|
18
|
+
self.rails_redshift_replicator_replicable = RailsRedshiftReplicator::Replicable.new(replication_type, options)
|
|
19
|
+
RailsRedshiftReplicator.add_replicable({ options[:source_table] => rails_redshift_replicator_replicable })
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
module Actions
|
|
24
|
+
def rrr_export
|
|
25
|
+
rails_redshift_replicator_replicable.export
|
|
26
|
+
end
|
|
27
|
+
def rrr_import
|
|
28
|
+
rails_redshift_replicator_replicable.import
|
|
29
|
+
end
|
|
30
|
+
def rrr_replicate
|
|
31
|
+
rails_redshift_replicator_replicable.replicate
|
|
32
|
+
end
|
|
33
|
+
def rrr_vacuum
|
|
34
|
+
rails_redshift_replicator_replicable.vacuum
|
|
35
|
+
end
|
|
36
|
+
def rrr_analyze
|
|
37
|
+
rails_redshift_replicator_replicable.analyze
|
|
38
|
+
end
|
|
39
|
+
def rrr_deleter
|
|
40
|
+
rails_redshift_replicator_replicable.deleter
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
ActiveRecord::Base.send :include, RailsRedshiftReplicator::Model::Extension
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
HairTrigger.class_eval do
|
|
2
|
+
# includes triggers defined on replicables
|
|
3
|
+
def self.current_triggers
|
|
4
|
+
canonical_triggers = models.map(&:triggers).flatten.compact || []
|
|
5
|
+
canonical_triggers += RailsRedshiftReplicator.replicables.values.map(&:triggers).flatten.compact
|
|
6
|
+
canonical_triggers.each(&:prepare!) # interpolates any vars so we match the migrations
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
module RailsRedshiftReplicator
|
|
2
|
+
class Replicable
|
|
3
|
+
include HairTrigger::Base
|
|
4
|
+
attr_reader :source_table, :target_table, :replication_field, :replication_type, :tracking_deleted, :options
|
|
5
|
+
# alias for hairtrigger
|
|
6
|
+
alias table_name source_table
|
|
7
|
+
|
|
8
|
+
# @param replication_type [String, Symbol]
|
|
9
|
+
# @param options [Hash] Replication options
|
|
10
|
+
# @option options [String, Symbol] :source_table name of the source table to replicate
|
|
11
|
+
# @option options [String, Symbol] :target_table name of the target table on redshift
|
|
12
|
+
# @option options [String, Symbol] :replication_field name of the replication field
|
|
13
|
+
def initialize(replication_type, options = {})
|
|
14
|
+
@replication_type = replication_type
|
|
15
|
+
@options = options
|
|
16
|
+
@source_table = options[:source_table].to_s
|
|
17
|
+
@target_table = (options[:target_table] || source_table).to_s
|
|
18
|
+
replication_field = options[:replication_field] || exporter_class.replication_field
|
|
19
|
+
@replication_field = replication_field && replication_field.to_s
|
|
20
|
+
@tracking_deleted = delete_tracking_enabled?
|
|
21
|
+
if tracking_deleted
|
|
22
|
+
trigger.after(:delete) do
|
|
23
|
+
"INSERT INTO rails_redshift_replicator_deleted_ids(source_table, object_id) VALUES('#{source_table}', OLD.id);"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reset_last_record
|
|
29
|
+
last_imported_replication = RailsRedshiftReplicator::Replication.from_table(source_table).with_state(:imported).last
|
|
30
|
+
last_imported_replication && last_imported_replication.update_attribute(:last_record, nil)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def delete_tracking_enabled?
|
|
34
|
+
RailsRedshiftReplicator.enable_delete_tracking &&
|
|
35
|
+
(options[:enable_delete_tracking].blank? || options[:enable_delete_tracking]) &&
|
|
36
|
+
table_supports_tracking?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def table_supports_tracking?
|
|
40
|
+
ActiveRecord::Base.connection.columns(source_table).map(&:name).include? "id"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def replicate
|
|
44
|
+
export
|
|
45
|
+
import
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def import
|
|
49
|
+
@last_replication = nil
|
|
50
|
+
if last_replication.present?
|
|
51
|
+
if last_replication.uploaded?
|
|
52
|
+
perform_import
|
|
53
|
+
elsif last_replication.imported?
|
|
54
|
+
RailsRedshiftReplicator.logger.info I18n.t(:nothing_to_import, table_name: source_table, scope: :rails_redshift_replicator)
|
|
55
|
+
elsif max_retries_reached?
|
|
56
|
+
last_replication.cancel!
|
|
57
|
+
else
|
|
58
|
+
resume_replication
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
RailsRedshiftReplicator.logger.info I18n.t(:nothing_to_import, table_name: source_table, scope: :rails_redshift_replicator)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def export
|
|
66
|
+
@last_replication = nil
|
|
67
|
+
if last_replication.blank? || (last_replication && last_replication.imported?)
|
|
68
|
+
perform_export
|
|
69
|
+
else
|
|
70
|
+
if max_retries_reached?
|
|
71
|
+
last_replication.cancel!
|
|
72
|
+
perform_export
|
|
73
|
+
else
|
|
74
|
+
resume_replication
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def perform_export(replication = nil)
|
|
80
|
+
exporter_class.new(self, replication).export_and_upload
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def perform_import
|
|
84
|
+
importer_class.new(last_replication).import
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def max_retries_reached?
|
|
88
|
+
if RailsRedshiftReplicator.max_retries && (RailsRedshiftReplicator.max_retries == last_replication.retries)
|
|
89
|
+
RailsRedshiftReplicator.logger.warn I18n.t(:max_retries_reached, id: last_replication, table_name: source_table, scope: :rails_redshift_replicator)
|
|
90
|
+
return true
|
|
91
|
+
else
|
|
92
|
+
false
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def last_replication
|
|
97
|
+
@last_replication ||= RailsRedshiftReplicator::Replication.from_table(source_table).last
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def resume_replication
|
|
101
|
+
last_replication.increment! :retries, 1
|
|
102
|
+
if last_replication.state.in? %w(enqueued exporting exported uploading)
|
|
103
|
+
log_resuming('export')
|
|
104
|
+
perform_export(last_replication)
|
|
105
|
+
else
|
|
106
|
+
log_resuming('import')
|
|
107
|
+
perform_import
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def log_resuming(action)
|
|
112
|
+
RailsRedshiftReplicator.logger.info I18n.t(:resuming_replication, table_name: source_table, action: action, state: last_replication.state, scope: :rails_redshift_replicator)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def vacuum
|
|
116
|
+
RailsRedshiftReplicator.vacuum(target_table)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def analyze
|
|
120
|
+
RailsRedshiftReplicator.analyze(target_table)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def deleter
|
|
124
|
+
RailsRedshiftReplicator::Deleter.new(self)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def exporter_class
|
|
128
|
+
@exporter_class ||= begin
|
|
129
|
+
"RailsRedshiftReplicator::Exporters::#{replication_type.to_s.classify}".constantize
|
|
130
|
+
rescue
|
|
131
|
+
raise StandardError.new I18n.t(:missing_replicator_type, scope: :rails_redshift_replicator)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def importer_class
|
|
136
|
+
@importer_class ||= begin
|
|
137
|
+
"RailsRedshiftReplicator::Importers::#{replication_type.to_s.classify}".constantize
|
|
138
|
+
rescue
|
|
139
|
+
raise StandardError.new I18n.t(:missing_replicator_type, scope: :rails_redshift_replicator)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|