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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +34 -0
  5. data/app/assets/javascripts/rails_redshift_replicator/application.js +13 -0
  6. data/app/assets/stylesheets/rails_redshift_replicator/application.css +15 -0
  7. data/app/controllers/rails_redshift_replicator/application_controller.rb +5 -0
  8. data/app/helpers/rails_redshift_replicator/application_helper.rb +4 -0
  9. data/app/models/rails_redshift_replicator/replication.rb +98 -0
  10. data/app/views/layouts/rails_redshift_replicator/application.html.erb +14 -0
  11. data/config/locales/rails_redshift_replicator.en.yml +20 -0
  12. data/config/routes.rb +2 -0
  13. data/db/migrate/20160503214955_create_rails_redshift_replicator_replications.rb +24 -0
  14. data/db/migrate/20160509193335_create_table_rails_redshift_replicator_deleted_ids.rb +8 -0
  15. data/lib/generators/rails_redshift_replicator/install_generator.rb +25 -0
  16. data/lib/generators/templates/rails_redshift_replicator.rb +74 -0
  17. data/lib/rails_redshift_replicator.rb +229 -0
  18. data/lib/rails_redshift_replicator/adapters/generic.rb +40 -0
  19. data/lib/rails_redshift_replicator/adapters/mysql2.rb +22 -0
  20. data/lib/rails_redshift_replicator/adapters/postgresql.rb +37 -0
  21. data/lib/rails_redshift_replicator/adapters/sqlite.rb +27 -0
  22. data/lib/rails_redshift_replicator/deleter.rb +67 -0
  23. data/lib/rails_redshift_replicator/engine.rb +14 -0
  24. data/lib/rails_redshift_replicator/exporters/base.rb +215 -0
  25. data/lib/rails_redshift_replicator/exporters/full_replicator.rb +9 -0
  26. data/lib/rails_redshift_replicator/exporters/identity_replicator.rb +9 -0
  27. data/lib/rails_redshift_replicator/exporters/timed_replicator.rb +9 -0
  28. data/lib/rails_redshift_replicator/file_manager.rb +134 -0
  29. data/lib/rails_redshift_replicator/importers/base.rb +158 -0
  30. data/lib/rails_redshift_replicator/importers/full_replicator.rb +17 -0
  31. data/lib/rails_redshift_replicator/importers/identity_replicator.rb +15 -0
  32. data/lib/rails_redshift_replicator/importers/timed_replicator.rb +18 -0
  33. data/lib/rails_redshift_replicator/model/extension.rb +45 -0
  34. data/lib/rails_redshift_replicator/model/hair_trigger_extension.rb +8 -0
  35. data/lib/rails_redshift_replicator/replicable.rb +143 -0
  36. data/lib/rails_redshift_replicator/rlogger.rb +12 -0
  37. data/lib/rails_redshift_replicator/tools/analyze.rb +18 -0
  38. data/lib/rails_redshift_replicator/tools/vacuum.rb +77 -0
  39. data/lib/rails_redshift_replicator/version.rb +3 -0
  40. data/lib/tasks/rails_redshift_replicator_tasks.rake +4 -0
  41. data/spec/dummy/README.rdoc +28 -0
  42. data/spec/dummy/Rakefile +6 -0
  43. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  44. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  45. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  46. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  47. data/spec/dummy/app/models/post.rb +4 -0
  48. data/spec/dummy/app/models/tag.rb +4 -0
  49. data/spec/dummy/app/models/user.rb +5 -0
  50. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  51. data/spec/dummy/bin/bundle +3 -0
  52. data/spec/dummy/bin/rails +4 -0
  53. data/spec/dummy/bin/rake +4 -0
  54. data/spec/dummy/bin/setup +29 -0
  55. data/spec/dummy/config.ru +4 -0
  56. data/spec/dummy/config/application.rb +26 -0
  57. data/spec/dummy/config/boot.rb +5 -0
  58. data/spec/dummy/config/database.yml +37 -0
  59. data/spec/dummy/config/environment.rb +5 -0
  60. data/spec/dummy/config/environments/development.rb +41 -0
  61. data/spec/dummy/config/environments/production.rb +79 -0
  62. data/spec/dummy/config/environments/test.rb +42 -0
  63. data/spec/dummy/config/initializers/assets.rb +11 -0
  64. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  65. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  66. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  67. data/spec/dummy/config/initializers/inflections.rb +16 -0
  68. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  69. data/spec/dummy/config/initializers/rails_redshift_replicator.rb +59 -0
  70. data/spec/dummy/config/initializers/session_store.rb +3 -0
  71. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  72. data/spec/dummy/config/locales/en.yml +23 -0
  73. data/spec/dummy/config/locales/rails_redshift_replicator.en.yml +19 -0
  74. data/spec/dummy/config/routes.rb +4 -0
  75. data/spec/dummy/config/secrets.yml +22 -0
  76. data/spec/dummy/db/development.sqlite3 +0 -0
  77. data/spec/dummy/db/migrate/20160504120421_create_test_tables.rb +40 -0
  78. data/spec/dummy/db/migrate/20160509225445_create_triggers_posts_delete_or_tags_delete_or_users_delete.rb +33 -0
  79. data/spec/dummy/db/migrate/20160511000937_create_rails_redshift_replicator_replications.rails_redshift_replicator.rb +25 -0
  80. data/spec/dummy/db/migrate/20160511000938_create_table_rails_redshift_replicator_deleted_ids.rails_redshift_replicator.rb +9 -0
  81. data/spec/dummy/db/schema.rb +99 -0
  82. data/spec/dummy/db/test.sqlite3 +0 -0
  83. data/spec/dummy/log/development.log +1623 -0
  84. data/spec/dummy/log/test.log +95379 -0
  85. data/spec/dummy/public/404.html +67 -0
  86. data/spec/dummy/public/422.html +67 -0
  87. data/spec/dummy/public/500.html +66 -0
  88. data/spec/dummy/public/favicon.ico +0 -0
  89. data/spec/dummy/rails_redshift_replicator_development +0 -0
  90. data/spec/factories/rails_redshift_replicator_replications.rb +31 -0
  91. data/spec/integration/rails_redshift_replicator_spec.rb +148 -0
  92. data/spec/integration/setup_spec.rb +149 -0
  93. data/spec/lib/rails_redshift_replicator/deleter_spec.rb +90 -0
  94. data/spec/lib/rails_redshift_replicator/exporters/base_spec.rb +326 -0
  95. data/spec/lib/rails_redshift_replicator/exporters/full_replicator_spec.rb +33 -0
  96. data/spec/lib/rails_redshift_replicator/exporters/identity_replicator_spec.rb +40 -0
  97. data/spec/lib/rails_redshift_replicator/exporters/timed_replicator_spec.rb +43 -0
  98. data/spec/lib/rails_redshift_replicator/file_manager_spec.rb +90 -0
  99. data/spec/lib/rails_redshift_replicator/importers/base_spec.rb +102 -0
  100. data/spec/lib/rails_redshift_replicator/importers/full_replicator_spec.rb +27 -0
  101. data/spec/lib/rails_redshift_replicator/importers/identity_replicator_spec.rb +26 -0
  102. data/spec/lib/rails_redshift_replicator/importers/timed_replicator_spec.rb +26 -0
  103. data/spec/lib/rails_redshift_replicator/model/extension_spec.rb +36 -0
  104. data/spec/lib/rails_redshift_replicator/replicable_spec.rb +230 -0
  105. data/spec/lib/rails_redshift_replicator/rlogger_spec.rb +22 -0
  106. data/spec/lib/rails_redshift_replicator/tools/analyze_spec.rb +15 -0
  107. data/spec/lib/rails_redshift_replicator/tools/vacuum_spec.rb +65 -0
  108. data/spec/lib/rails_redshift_replicator_spec.rb +110 -0
  109. data/spec/models/rails_redshift_replicator/replication_spec.rb +104 -0
  110. data/spec/spec_helper.rb +36 -0
  111. data/spec/support/csv/invalid_user.csv +12 -0
  112. data/spec/support/csv/valid_post.csv +2 -0
  113. data/spec/support/csv/valid_tags_users.csv +1 -0
  114. data/spec/support/csv/valid_user.csv +2 -0
  115. data/spec/support/rails_redshift_replicator_helpers.rb +95 -0
  116. metadata +430 -0
@@ -0,0 +1,9 @@
1
+ module RailsRedshiftReplicator
2
+ module Exporters
3
+ class TimedReplicator < Base
4
+ def self.replication_field
5
+ "updated_at"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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