lhm-shopify 3.3.5

Sign up to get free protection for your applications and to get access to all the features.
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