lhm-shopify 3.3.5

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 (94) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +34 -0
  3. data/.gitignore +17 -0
  4. data/.rubocop.yml +183 -0
  5. data/.travis.yml +21 -0
  6. data/CHANGELOG.md +216 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE +27 -0
  9. data/README.md +284 -0
  10. data/Rakefile +22 -0
  11. data/bin/.gitkeep +0 -0
  12. data/dbdeployer/config.json +32 -0
  13. data/dbdeployer/install.sh +64 -0
  14. data/dev.yml +20 -0
  15. data/gemfiles/ar-2.3_mysql.gemfile +6 -0
  16. data/gemfiles/ar-3.2_mysql.gemfile +5 -0
  17. data/gemfiles/ar-3.2_mysql2.gemfile +5 -0
  18. data/gemfiles/ar-4.0_mysql2.gemfile +5 -0
  19. data/gemfiles/ar-4.1_mysql2.gemfile +5 -0
  20. data/gemfiles/ar-4.2_mysql2.gemfile +5 -0
  21. data/gemfiles/ar-5.0_mysql2.gemfile +5 -0
  22. data/lhm.gemspec +34 -0
  23. data/lib/lhm.rb +131 -0
  24. data/lib/lhm/atomic_switcher.rb +52 -0
  25. data/lib/lhm/chunk_finder.rb +32 -0
  26. data/lib/lhm/chunk_insert.rb +51 -0
  27. data/lib/lhm/chunker.rb +87 -0
  28. data/lib/lhm/cleanup/current.rb +74 -0
  29. data/lib/lhm/command.rb +48 -0
  30. data/lib/lhm/entangler.rb +117 -0
  31. data/lib/lhm/intersection.rb +51 -0
  32. data/lib/lhm/invoker.rb +98 -0
  33. data/lib/lhm/locked_switcher.rb +74 -0
  34. data/lib/lhm/migration.rb +43 -0
  35. data/lib/lhm/migrator.rb +237 -0
  36. data/lib/lhm/printer.rb +59 -0
  37. data/lib/lhm/railtie.rb +9 -0
  38. data/lib/lhm/sql_helper.rb +77 -0
  39. data/lib/lhm/sql_retry.rb +61 -0
  40. data/lib/lhm/table.rb +121 -0
  41. data/lib/lhm/table_name.rb +23 -0
  42. data/lib/lhm/test_support.rb +35 -0
  43. data/lib/lhm/throttler.rb +36 -0
  44. data/lib/lhm/throttler/slave_lag.rb +145 -0
  45. data/lib/lhm/throttler/threads_running.rb +53 -0
  46. data/lib/lhm/throttler/time.rb +29 -0
  47. data/lib/lhm/timestamp.rb +11 -0
  48. data/lib/lhm/version.rb +6 -0
  49. data/shipit.rubygems.yml +0 -0
  50. data/spec/.lhm.example +4 -0
  51. data/spec/README.md +58 -0
  52. data/spec/fixtures/bigint_table.ddl +4 -0
  53. data/spec/fixtures/composite_primary_key.ddl +7 -0
  54. data/spec/fixtures/custom_primary_key.ddl +6 -0
  55. data/spec/fixtures/destination.ddl +6 -0
  56. data/spec/fixtures/lines.ddl +7 -0
  57. data/spec/fixtures/origin.ddl +6 -0
  58. data/spec/fixtures/permissions.ddl +5 -0
  59. data/spec/fixtures/small_table.ddl +4 -0
  60. data/spec/fixtures/tracks.ddl +5 -0
  61. data/spec/fixtures/users.ddl +14 -0
  62. data/spec/fixtures/wo_id_int_column.ddl +6 -0
  63. data/spec/integration/atomic_switcher_spec.rb +93 -0
  64. data/spec/integration/chunk_insert_spec.rb +29 -0
  65. data/spec/integration/chunker_spec.rb +185 -0
  66. data/spec/integration/cleanup_spec.rb +136 -0
  67. data/spec/integration/entangler_spec.rb +66 -0
  68. data/spec/integration/integration_helper.rb +237 -0
  69. data/spec/integration/invoker_spec.rb +33 -0
  70. data/spec/integration/lhm_spec.rb +585 -0
  71. data/spec/integration/lock_wait_timeout_spec.rb +30 -0
  72. data/spec/integration/locked_switcher_spec.rb +50 -0
  73. data/spec/integration/sql_retry/lock_wait_spec.rb +125 -0
  74. data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +101 -0
  75. data/spec/integration/table_spec.rb +91 -0
  76. data/spec/test_helper.rb +32 -0
  77. data/spec/unit/atomic_switcher_spec.rb +31 -0
  78. data/spec/unit/chunk_finder_spec.rb +73 -0
  79. data/spec/unit/chunk_insert_spec.rb +44 -0
  80. data/spec/unit/chunker_spec.rb +166 -0
  81. data/spec/unit/entangler_spec.rb +124 -0
  82. data/spec/unit/intersection_spec.rb +51 -0
  83. data/spec/unit/lhm_spec.rb +29 -0
  84. data/spec/unit/locked_switcher_spec.rb +51 -0
  85. data/spec/unit/migrator_spec.rb +146 -0
  86. data/spec/unit/printer_spec.rb +97 -0
  87. data/spec/unit/sql_helper_spec.rb +32 -0
  88. data/spec/unit/table_name_spec.rb +39 -0
  89. data/spec/unit/table_spec.rb +47 -0
  90. data/spec/unit/throttler/slave_lag_spec.rb +317 -0
  91. data/spec/unit/throttler/threads_running_spec.rb +64 -0
  92. data/spec/unit/throttler_spec.rb +124 -0
  93. data/spec/unit/unit_helper.rb +13 -0
  94. metadata +239 -0
@@ -0,0 +1,43 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ require 'lhm/intersection'
5
+ require 'lhm/timestamp'
6
+
7
+ module Lhm
8
+ class Migration
9
+ attr_reader :origin, :destination, :conditions, :renames
10
+
11
+ def initialize(origin, destination, conditions = nil, renames = {}, time = Time.now)
12
+ @origin = origin
13
+ @destination = destination
14
+ @conditions = conditions
15
+ @renames = renames
16
+ @table_name = TableName.new(@origin.name, time)
17
+ end
18
+
19
+ def archive_name
20
+ @archive_name ||= @table_name.archived
21
+ end
22
+
23
+ def intersection
24
+ Intersection.new(@origin, @destination, @renames)
25
+ end
26
+
27
+ def origin_name
28
+ @table_name.original
29
+ end
30
+
31
+ def origin_columns
32
+ @origin_columns ||= intersection.origin.typed(origin_name)
33
+ end
34
+
35
+ def destination_name
36
+ @destination_name ||= destination.name
37
+ end
38
+
39
+ def destination_columns
40
+ @destination_columns ||= intersection.destination.joined
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,237 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ require 'lhm/command'
5
+ require 'lhm/migration'
6
+ require 'lhm/sql_helper'
7
+ require 'lhm/table'
8
+
9
+ module Lhm
10
+ # Copies existing schema and applies changes using alter on the empty table.
11
+ # `run` returns a Migration which can be used for the remaining process.
12
+ class Migrator
13
+ include Command
14
+ include SqlHelper
15
+
16
+ attr_reader :name, :statements, :connection, :conditions, :renames, :origin
17
+
18
+ def initialize(table, connection = nil)
19
+ @connection = connection
20
+ @origin = table
21
+ @name = table.destination_name
22
+ @statements = []
23
+ @renames = {}
24
+ end
25
+
26
+ # Alter a table with a custom statement
27
+ #
28
+ # @example
29
+ #
30
+ # Lhm.change_table(:users) do |m|
31
+ # m.ddl("ALTER TABLE #{m.name} ADD COLUMN age INT(11)")
32
+ # end
33
+ #
34
+ # @param [String] statement SQL alter statement
35
+ # @note
36
+ #
37
+ # Don't write the table name directly into the statement. Use the #name
38
+ # getter instead, because the alter statement will be executed against a
39
+ # temporary table.
40
+ #
41
+ def ddl(statement)
42
+ statements << statement
43
+ end
44
+
45
+ # Add a column to a table
46
+ #
47
+ # @example
48
+ #
49
+ # Lhm.change_table(:users) do |m|
50
+ # m.add_column(:comment, "VARCHAR(12) DEFAULT '0'")
51
+ # end
52
+ #
53
+ # @param [String] name Name of the column to add
54
+ # @param [String] definition Valid SQL column definition
55
+ def add_column(name, definition)
56
+ ddl('alter table `%s` add column `%s` %s' % [@name, name, definition])
57
+ end
58
+
59
+ # Change an existing column to a new definition
60
+ #
61
+ # @example
62
+ #
63
+ # Lhm.change_table(:users) do |m|
64
+ # m.change_column(:comment, "VARCHAR(12) DEFAULT '0' NOT NULL")
65
+ # end
66
+ #
67
+ # @param [String] name Name of the column to change
68
+ # @param [String] definition Valid SQL column definition
69
+ def change_column(name, definition)
70
+ ddl('alter table `%s` modify column `%s` %s' % [@name, name, definition])
71
+ end
72
+
73
+ # Rename an existing column.
74
+ #
75
+ # @example
76
+ #
77
+ # Lhm.change_table(:users) do |m|
78
+ # m.rename_column(:login, :username)
79
+ # end
80
+ #
81
+ # @param [String] old Name of the column to change
82
+ # @param [String] nu New name to use for the column
83
+ def rename_column(old, nu)
84
+ col = @origin.columns[old.to_s]
85
+
86
+ definition = col[:type]
87
+
88
+ definition += ' NOT NULL' unless col[:is_nullable] == "YES"
89
+ definition += " DEFAULT #{@connection.quote(col[:column_default])}" if col[:column_default]
90
+ definition += " COMMENT #{@connection.quote(col[:comment])}" if col[:comment]
91
+ definition += " COLLATE #{@connection.quote(col[:collate])}" if col[:collate]
92
+
93
+ ddl('alter table `%s` change column `%s` `%s` %s' % [@name, old, nu, definition])
94
+ @renames[old.to_s] = nu.to_s
95
+ end
96
+
97
+ # Remove a column from a table
98
+ #
99
+ # @example
100
+ #
101
+ # Lhm.change_table(:users) do |m|
102
+ # m.remove_column(:comment)
103
+ # end
104
+ #
105
+ # @param [String] name Name of the column to delete
106
+ def remove_column(name)
107
+ ddl('alter table `%s` drop `%s`' % [@name, name])
108
+ end
109
+
110
+ # Add an index to a table
111
+ #
112
+ # @example
113
+ #
114
+ # Lhm.change_table(:users) do |m|
115
+ # m.add_index(:comment)
116
+ # m.add_index([:username, :created_at])
117
+ # m.add_index("comment(10)")
118
+ # end
119
+ #
120
+ # @param [String, Symbol, Array<String, Symbol>] columns
121
+ # A column name given as String or Symbol. An Array of Strings or Symbols
122
+ # for compound indexes. It's possible to pass a length limit.
123
+ # @param [String, Symbol] index_name
124
+ # Optional name of the index to be created
125
+ def add_index(columns, index_name = nil)
126
+ ddl(index_ddl(columns, false, index_name))
127
+ end
128
+
129
+ # Add a unique index to a table
130
+ #
131
+ # @example
132
+ #
133
+ # Lhm.change_table(:users) do |m|
134
+ # m.add_unique_index(:comment)
135
+ # m.add_unique_index([:username, :created_at])
136
+ # m.add_unique_index("comment(10)")
137
+ # end
138
+ #
139
+ # @param [String, Symbol, Array<String, Symbol>] columns
140
+ # A column name given as String or Symbol. An Array of Strings or Symbols
141
+ # for compound indexes. It's possible to pass a length limit.
142
+ # @param [String, Symbol] index_name
143
+ # Optional name of the index to be created
144
+ def add_unique_index(columns, index_name = nil)
145
+ ddl(index_ddl(columns, true, index_name))
146
+ end
147
+
148
+ # Remove an index from a table
149
+ #
150
+ # @example
151
+ #
152
+ # Lhm.change_table(:users) do |m|
153
+ # m.remove_index(:comment)
154
+ # m.remove_index([:username, :created_at])
155
+ # end
156
+ #
157
+ # @param [String, Symbol, Array<String, Symbol>] columns
158
+ # A column name given as String or Symbol. An Array of Strings or Symbols
159
+ # for compound indexes.
160
+ # @param [String, Symbol] index_name
161
+ # Optional name of the index to be removed
162
+ def remove_index(columns, index_name = nil)
163
+ columns = [columns].flatten.map(&:to_sym)
164
+ from_origin = @origin.indices.find { |_, cols| cols.map(&:to_sym) == columns }
165
+ index_name ||= from_origin[0] unless from_origin.nil?
166
+ index_name ||= idx_name(@origin.name, columns)
167
+ ddl('drop index `%s` on `%s`' % [index_name, @name])
168
+ end
169
+
170
+ # Filter the data that is copied into the new table by the provided SQL.
171
+ # This SQL will be inserted into the copy directly after the "from"
172
+ # statement - so be sure to use inner/outer join syntax and not cross joins.
173
+ #
174
+ # @example Add a conditions filter to the migration.
175
+ # Lhm.change_table(:sounds) do |m|
176
+ # m.filter("inner join users on users.`id` = sounds.`user_id` and sounds.`public` = 1")
177
+ # end
178
+ #
179
+ # @param [ String ] sql The sql filter.
180
+ #
181
+ # @return [ String ] The sql filter.
182
+ def filter(sql)
183
+ @conditions = sql
184
+ end
185
+
186
+ private
187
+
188
+ def validate
189
+ unless @connection.data_source_exists?(@origin.name)
190
+ error("could not find origin table #{ @origin.name }")
191
+ end
192
+
193
+ unless @origin.satisfies_id_column_requirement?
194
+ error('origin does not satisfy `id` key requirements')
195
+ end
196
+
197
+ dest = @origin.destination_name
198
+
199
+ if @connection.data_source_exists?(dest)
200
+ error("#{ dest } should not exist; not cleaned up from previous run?")
201
+ end
202
+ end
203
+
204
+ def execute
205
+ destination_create
206
+ @statements.each do |stmt|
207
+ @connection.execute(tagged(stmt))
208
+ end
209
+ Migration.new(@origin, destination_read, conditions, renames)
210
+ end
211
+
212
+ def destination_create
213
+ original = %{CREATE TABLE `#{ @origin.name }`}
214
+ replacement = %{CREATE TABLE `#{ @origin.destination_name }`}
215
+ stmt = @origin.ddl.gsub(original, replacement)
216
+ @connection.execute(tagged(stmt))
217
+ end
218
+
219
+ def destination_read
220
+ Table.parse(@origin.destination_name, connection)
221
+ end
222
+
223
+ def index_ddl(cols, unique = nil, index_name = nil)
224
+ assert_valid_idx_name(index_name)
225
+ type = unique ? 'unique index' : 'index'
226
+ index_name ||= idx_name(@origin.name, cols)
227
+ parts = [type, index_name, @name, idx_spec(cols)]
228
+ 'create %s `%s` on `%s` (%s)' % parts
229
+ end
230
+
231
+ def assert_valid_idx_name(index_name)
232
+ if index_name && !(index_name.is_a?(String) || index_name.is_a?(Symbol))
233
+ raise ArgumentError, 'index_name must be a string or symbol'
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,59 @@
1
+ module Lhm
2
+ module Printer
3
+ class Output
4
+ def write(message)
5
+ print message
6
+ end
7
+ end
8
+
9
+ class Base
10
+ def initialize
11
+ @output = Output.new
12
+ end
13
+ end
14
+
15
+ class Percentage < Base
16
+ def initialize
17
+ super
18
+ @max_length = 0
19
+ end
20
+
21
+ def notify(lowest, highest)
22
+ return if !highest || highest == 0
23
+ message = "%.2f%% (#{lowest}/#{highest}) complete" % (lowest.to_f / highest * 100.0)
24
+ write(message)
25
+ end
26
+
27
+ def end
28
+ write('100% complete')
29
+ @output.write "\n"
30
+ end
31
+
32
+ def exception(e)
33
+ write("failed: #{e}")
34
+ @output.write "\n"
35
+ end
36
+
37
+ private
38
+
39
+ def write(message)
40
+ if (extra = @max_length - message.length) < 0
41
+ @max_length = message.length
42
+ extra = 0
43
+ end
44
+
45
+ @output.write "\r#{message}" + (' ' * extra)
46
+ end
47
+ end
48
+
49
+ class Dot < Base
50
+ def notify(*)
51
+ @output.write '.'
52
+ end
53
+
54
+ def end
55
+ @output.write "\n"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,9 @@
1
+ module Lhm
2
+ class Railtie < Rails::Railtie
3
+ initializer "lhm.test_setup" do
4
+ if Rails.env.test? || Rails.env.development?
5
+ Lhm.execute_inline!
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,77 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ module Lhm
5
+ module SqlHelper
6
+ extend self
7
+
8
+ def annotation
9
+ '/* large hadron migration */'
10
+ end
11
+
12
+ def idx_name(table_name, cols)
13
+ column_names = column_definition(cols).map(&:first)
14
+ "index_#{ table_name }_on_#{ column_names.join('_and_') }"
15
+ end
16
+
17
+ def idx_spec(cols)
18
+ column_definition(cols).map do |name, length|
19
+ "`#{ name }`#{ length }"
20
+ end.join(', ')
21
+ end
22
+
23
+ def version_string
24
+ row = connection.select_one("show variables like 'version'")
25
+ value = struct_key(row, 'Value')
26
+ row[value]
27
+ end
28
+
29
+ def tagged(statement)
30
+ "#{ statement } #{ SqlHelper.annotation }"
31
+ end
32
+
33
+ private
34
+
35
+ def column_definition(cols)
36
+ Array(cols).map do |column|
37
+ column.to_s.match(/`?([^\(]+)`?(\([^\)]+\))?/).captures
38
+ end
39
+ end
40
+
41
+ # Older versions of MySQL contain an atomic rename bug affecting bin
42
+ # log order. Affected versions extracted from bug report:
43
+ #
44
+ # http://bugs.mysql.com/bug.php?id=39675
45
+ #
46
+ # More Info: http://dev.mysql.com/doc/refman/5.5/en/metadata-locking.html
47
+ def supports_atomic_switch?
48
+ major, minor, tiny = version_string.split('.').map(&:to_i)
49
+
50
+ case major
51
+ when 4 then return false if minor and minor < 2
52
+ when 5
53
+ case minor
54
+ when 0 then return false if tiny and tiny < 52
55
+ when 1 then return false
56
+ when 4 then return false if tiny and tiny < 4
57
+ when 5 then return false if tiny and tiny < 3
58
+ end
59
+ when 6
60
+ case minor
61
+ when 0 then return false if tiny and tiny < 11
62
+ end
63
+ end
64
+ true
65
+ end
66
+
67
+ def struct_key(struct, key)
68
+ keys = if struct.is_a? Hash
69
+ struct.keys
70
+ else
71
+ struct.members
72
+ end
73
+
74
+ keys.find { |k| k.to_s.downcase == key.to_s.downcase }
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,61 @@
1
+ require 'retriable'
2
+ require 'lhm/sql_helper'
3
+
4
+ module Lhm
5
+ # SqlRetry standardizes the interface for retry behavior in components like
6
+ # Entangler, AtomicSwitcher, ChunkerInsert.
7
+ #
8
+ # By default if an error includes the message "Lock wait timeout exceeded", or
9
+ # "Deadlock found when trying to get lock", SqlRetry will retry again
10
+ # once the MySQL client returns control to the caller, plus one second.
11
+ # It will retry a total of 10 times and output to the logger a description
12
+ # of the retry with error information, retry count, and elapsed time.
13
+ #
14
+ # This behavior can be modified by passing `options` that are documented in
15
+ # https://github.com/kamui/retriable. Additionally, a "log_prefix" option,
16
+ # which is unique to SqlRetry can be used to prefix log output.
17
+ class SqlRetry
18
+ def initialize(connection, options = {})
19
+ @connection = connection
20
+ @log_prefix = options.delete(:log_prefix)
21
+ @retry_config = default_retry_config.dup.merge!(options)
22
+ end
23
+
24
+ def with_retries
25
+ Retriable.retriable(retry_config) do
26
+ yield(@connection)
27
+ end
28
+ end
29
+
30
+ attr_reader :retry_config
31
+
32
+ private
33
+
34
+ # For a full list of configuration options see https://github.com/kamui/retriable
35
+ def default_retry_config
36
+ {
37
+ on: {
38
+ StandardError => [
39
+ /Lock wait timeout exceeded/,
40
+ /Timeout waiting for a response from the last query/,
41
+ /Deadlock found when trying to get lock/,
42
+ /Query execution was interrupted/,
43
+ /Lost connection to MySQL server during query/,
44
+ /Max connect timeout reached/,
45
+ /Unknown MySQL server host/,
46
+ ]
47
+ },
48
+ multiplier: 1, # each successive interval grows by this factor
49
+ base_interval: 1, # the initial interval in seconds between tries.
50
+ tries: 20, # Number of attempts to make at running your code block (includes initial attempt).
51
+ rand_factor: 0, # percentage to randomize the next retry interval time
52
+ max_elapsed_time: Float::INFINITY, # max total time in seconds that code is allowed to keep being retried
53
+ on_retry: Proc.new do |exception, try_number, total_elapsed_time, next_interval|
54
+ log = "#{exception.class}: '#{exception.message}' - #{try_number} tries in #{total_elapsed_time} seconds and #{next_interval} seconds until the next try."
55
+ log.prepend("[#{@log_prefix}] ") if @log_prefix
56
+ Lhm.logger.info(log)
57
+ end
58
+ }.freeze
59
+ end
60
+ end
61
+ end