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,74 @@
1
+ require 'lhm/timestamp'
2
+ require 'lhm/sql_retry'
3
+
4
+ module Lhm
5
+ module Cleanup
6
+ class Current
7
+ def initialize(run, origin_table_name, connection, options = {})
8
+ @run = run
9
+ @table_name = TableName.new(origin_table_name)
10
+ @connection = connection
11
+ @ddls = []
12
+ @retry_helper = SqlRetry.new(
13
+ @connection,
14
+ {
15
+ log_prefix: "Cleanup::Current"
16
+ }.merge!(options.fetch(:retriable, {}))
17
+ )
18
+ end
19
+
20
+ attr_reader :run, :connection, :ddls
21
+
22
+ def execute
23
+ build_statements_for_drop_lhm_triggers_for_origin
24
+ build_statements_for_rename_lhmn_tables_for_origin
25
+ if run
26
+ execute_ddls
27
+ else
28
+ report_ddls
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def build_statements_for_drop_lhm_triggers_for_origin
35
+ lhm_triggers_for_origin.each do |trigger|
36
+ @ddls << "drop trigger if exists #{trigger}"
37
+ end
38
+ end
39
+
40
+ def lhm_triggers_for_origin
41
+ @lhm_triggers_for_origin ||= all_triggers_for_origin.select { |name| name =~ /^lhmt/ }
42
+ end
43
+
44
+ def all_triggers_for_origin
45
+ @all_triggers_for_origin ||= connection.select_values("show triggers like '%#{@table_name.original}'").collect do |trigger|
46
+ trigger.respond_to?(:trigger) ? trigger.trigger : trigger
47
+ end
48
+ end
49
+
50
+ def build_statements_for_rename_lhmn_tables_for_origin
51
+ lhmn_tables_for_origin.each do |table|
52
+ @ddls << "rename table #{table} to #{@table_name.failed}"
53
+ end
54
+ end
55
+
56
+ def lhmn_tables_for_origin
57
+ @lhmn_tables_for_origin ||= connection.select_values("show tables like '#{@table_name.new}'")
58
+ end
59
+
60
+ def execute_ddls
61
+ ddls.each do |ddl|
62
+ @retry_helper.with_retries do |retriable_connection|
63
+ retriable_connection.execute(ddl)
64
+ end
65
+ end
66
+ end
67
+
68
+ def report_ddls
69
+ puts "The following DDLs would be executed:"
70
+ ddls.each { |ddl| puts ddl }
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,48 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ module Lhm
5
+ class Error < StandardError
6
+ end
7
+
8
+ module Command
9
+ def run(&block)
10
+ Lhm.logger.info "Starting run of class=#{self.class}"
11
+ validate
12
+
13
+ if block_given?
14
+ before
15
+ block.call(self)
16
+ after
17
+ else
18
+ execute
19
+ end
20
+ rescue => e
21
+ Lhm.logger.error "Error in class=#{self.class}, reverting. exception=#{e.class} message=#{e.message}"
22
+ revert
23
+ raise
24
+ end
25
+
26
+ private
27
+
28
+ def validate
29
+ end
30
+
31
+ def revert
32
+ end
33
+
34
+ def execute
35
+ raise NotImplementedError.new(self.class.name)
36
+ end
37
+
38
+ def before
39
+ end
40
+
41
+ def after
42
+ end
43
+
44
+ def error(msg)
45
+ raise Error.new(msg)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,117 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ require 'lhm/command'
5
+ require 'lhm/sql_helper'
6
+ require 'lhm/sql_retry'
7
+
8
+ module Lhm
9
+ class Entangler
10
+ include Command
11
+ include SqlHelper
12
+
13
+ attr_reader :connection
14
+
15
+ # Creates entanglement between two tables. All creates, updates and deletes
16
+ # to origin will be repeated on the destination table.
17
+ def initialize(migration, connection = nil, options = {})
18
+ @intersection = migration.intersection
19
+ @origin = migration.origin
20
+ @destination = migration.destination
21
+ @connection = connection
22
+ @retry_helper = SqlRetry.new(
23
+ @connection,
24
+ {
25
+ log_prefix: "Entangler"
26
+ }.merge!(options.fetch(:retriable, {}))
27
+ )
28
+ end
29
+
30
+ def entangle
31
+ [
32
+ create_delete_trigger,
33
+ create_insert_trigger,
34
+ create_update_trigger
35
+ ]
36
+ end
37
+
38
+ def untangle
39
+ [
40
+ "drop trigger if exists `#{ trigger(:del) }`",
41
+ "drop trigger if exists `#{ trigger(:ins) }`",
42
+ "drop trigger if exists `#{ trigger(:upd) }`"
43
+ ]
44
+ end
45
+
46
+ def create_insert_trigger
47
+ strip %Q{
48
+ create trigger `#{ trigger(:ins) }`
49
+ after insert on `#{ @origin.name }` for each row
50
+ replace into `#{ @destination.name }` (#{ @intersection.destination.joined }) #{ SqlHelper.annotation }
51
+ values (#{ @intersection.origin.typed('NEW') })
52
+ }
53
+ end
54
+
55
+ def create_update_trigger
56
+ strip %Q{
57
+ create trigger `#{ trigger(:upd) }`
58
+ after update on `#{ @origin.name }` for each row
59
+ replace into `#{ @destination.name }` (#{ @intersection.destination.joined }) #{ SqlHelper.annotation }
60
+ values (#{ @intersection.origin.typed('NEW') })
61
+ }
62
+ end
63
+
64
+ def create_delete_trigger
65
+ strip %Q{
66
+ create trigger `#{ trigger(:del) }`
67
+ after delete on `#{ @origin.name }` for each row
68
+ delete ignore from `#{ @destination.name }` #{ SqlHelper.annotation }
69
+ where `#{ @destination.name }`.`id` = OLD.`id`
70
+ }
71
+ end
72
+
73
+ def trigger(type)
74
+ "lhmt_#{ type }_#{ @origin.name }"[0...64]
75
+ end
76
+
77
+ def expected_triggers
78
+ [trigger(:ins), trigger(:upd), trigger(:del)]
79
+ end
80
+
81
+ def validate
82
+ unless @connection.data_source_exists?(@origin.name)
83
+ error("#{ @origin.name } does not exist")
84
+ end
85
+
86
+ unless @connection.data_source_exists?(@destination.name)
87
+ error("#{ @destination.name } does not exist")
88
+ end
89
+ end
90
+
91
+ def before
92
+ entangle.each do |stmt|
93
+ @retry_helper.with_retries do |retriable_connection|
94
+ retriable_connection.execute(stmt)
95
+ end
96
+ end
97
+ end
98
+
99
+ def after
100
+ untangle.each do |stmt|
101
+ @retry_helper.with_retries do |retriable_connection|
102
+ retriable_connection.execute(stmt)
103
+ end
104
+ end
105
+ end
106
+
107
+ def revert
108
+ after
109
+ end
110
+
111
+ private
112
+
113
+ def strip(sql)
114
+ sql.strip.gsub(/\n */, "\n")
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,51 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ module Lhm
5
+ # Determine and format columns common to origin and destination.
6
+ class Intersection
7
+ def initialize(origin, destination, renames = {})
8
+ @origin = origin
9
+ @destination = destination
10
+ @renames = renames
11
+ end
12
+
13
+ def origin
14
+ (common + @renames.keys).extend(Joiners)
15
+ end
16
+
17
+ def destination
18
+ (common + @renames.values).extend(Joiners)
19
+ end
20
+
21
+ private
22
+
23
+ def common
24
+ (@origin.columns.keys & @destination.columns.keys).sort
25
+ end
26
+
27
+ module Joiners
28
+ def escaped
29
+ map { |name| tick(name) }
30
+ end
31
+
32
+ def joined
33
+ escaped.join(', ')
34
+ end
35
+
36
+ def typed(type)
37
+ map { |name| qualified(name, type) }.join(', ')
38
+ end
39
+
40
+ private
41
+
42
+ def qualified(name, type)
43
+ "`#{ type }`.`#{ name }`"
44
+ end
45
+
46
+ def tick(name)
47
+ "`#{ name }`"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,98 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ require 'lhm/chunker'
5
+ require 'lhm/entangler'
6
+ require 'lhm/atomic_switcher'
7
+ require 'lhm/locked_switcher'
8
+ require 'lhm/migrator'
9
+
10
+ module Lhm
11
+ # Copies an origin table to an altered destination table. Live activity is
12
+ # synchronized into the destination table using triggers.
13
+ #
14
+ # Once the origin and destination tables have converged, origin is archived
15
+ # and replaced by destination.
16
+ class Invoker
17
+ include SqlHelper
18
+ LOCK_WAIT_TIMEOUT_DELTA = 10
19
+ INNODB_LOCK_WAIT_TIMEOUT_MAX=1073741824.freeze # https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout
20
+ LOCK_WAIT_TIMEOUT_MAX=31536000.freeze # https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html
21
+
22
+ attr_reader :migrator, :connection
23
+
24
+ def initialize(origin, connection)
25
+ @connection = connection
26
+ @migrator = Migrator.new(origin, connection)
27
+ end
28
+
29
+ def set_session_lock_wait_timeouts
30
+ global_innodb_lock_wait_timeout = @connection.select_one("SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout'")
31
+ global_lock_wait_timeout = @connection.select_one("SHOW GLOBAL VARIABLES LIKE 'lock_wait_timeout'")
32
+
33
+ if global_innodb_lock_wait_timeout
34
+ desired_innodb_lock_wait_timeout = global_innodb_lock_wait_timeout['Value'].to_i + LOCK_WAIT_TIMEOUT_DELTA
35
+ if desired_innodb_lock_wait_timeout <= INNODB_LOCK_WAIT_TIMEOUT_MAX
36
+ @connection.execute("SET SESSION innodb_lock_wait_timeout=#{desired_innodb_lock_wait_timeout}")
37
+ end
38
+ end
39
+
40
+ if global_lock_wait_timeout
41
+ desired_lock_wait_timeout = global_lock_wait_timeout['Value'].to_i + LOCK_WAIT_TIMEOUT_DELTA
42
+ if desired_lock_wait_timeout <= LOCK_WAIT_TIMEOUT_MAX
43
+ @connection.execute("SET SESSION lock_wait_timeout=#{desired_lock_wait_timeout}")
44
+ end
45
+ end
46
+ end
47
+
48
+ def run(options = {})
49
+ normalize_options(options)
50
+ set_session_lock_wait_timeouts
51
+ migration = @migrator.run
52
+ entangler = Entangler.new(migration, @connection, options)
53
+
54
+ entangler.run do
55
+ options[:verifier] ||= Proc.new { |conn| triggers_still_exist?(conn, entangler) }
56
+ Chunker.new(migration, @connection, options).run
57
+ raise "Required triggers do not exist" unless triggers_still_exist?(@connection, entangler)
58
+ if options[:atomic_switch]
59
+ AtomicSwitcher.new(migration, @connection, options).run
60
+ else
61
+ LockedSwitcher.new(migration, @connection).run
62
+ end
63
+ end
64
+ end
65
+
66
+ def triggers_still_exist?(conn, entangler)
67
+ triggers = conn.select_values("SHOW TRIGGERS LIKE '%#{migrator.origin.name}'").select { |name| name =~ /^lhmt/ }
68
+ triggers.sort == entangler.expected_triggers.sort
69
+ end
70
+
71
+ private
72
+
73
+ def normalize_options(options)
74
+ Lhm.logger.info "Starting LHM run on table=#{@migrator.name}"
75
+
76
+ unless options.include?(:atomic_switch)
77
+ if supports_atomic_switch?
78
+ options[:atomic_switch] = true
79
+ else
80
+ raise Error.new(
81
+ "Using mysql #{version_string}. You must explicitly set " \
82
+ 'options[:atomic_switch] (re SqlHelper#supports_atomic_switch?)')
83
+ end
84
+ end
85
+
86
+ if options[:throttler]
87
+ throttler_options = options[:throttler_options] || {}
88
+ options[:throttler] = Throttler::Factory.create_throttler(options[:throttler], throttler_options)
89
+ else
90
+ options[:throttler] = Lhm.throttler
91
+ end
92
+
93
+ rescue => e
94
+ Lhm.logger.error "LHM run failed with exception=#{e.class} message=#{e.message}"
95
+ raise
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,74 @@
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
+
8
+ module Lhm
9
+ # Switches origin with destination table nonatomically using a locked write.
10
+ # LockedSwitcher adopts the Facebook strategy, with the following caveat:
11
+ #
12
+ # "Since alter table causes an implicit commit in innodb, innodb locks get
13
+ # released after the first alter table. So any transaction that sneaks in
14
+ # after the first alter table and before the second alter table gets
15
+ # a 'table not found' error. The second alter table is expected to be very
16
+ # fast though because copytable is not visible to other transactions and so
17
+ # there is no need to wait."
18
+ #
19
+ class LockedSwitcher
20
+ include Command
21
+ include SqlHelper
22
+
23
+ attr_reader :connection
24
+
25
+ def initialize(migration, connection = nil)
26
+ @migration = migration
27
+ @connection = connection
28
+ @origin = migration.origin
29
+ @destination = migration.destination
30
+ end
31
+
32
+ def statements
33
+ uncommitted { switch }
34
+ end
35
+
36
+ def switch
37
+ [
38
+ "lock table `#{ @origin.name }` write, `#{ @destination.name }` write",
39
+ "alter table `#{ @origin.name }` rename `#{ @migration.archive_name }`",
40
+ "alter table `#{ @destination.name }` rename `#{ @origin.name }`",
41
+ 'commit',
42
+ 'unlock tables'
43
+ ]
44
+ end
45
+
46
+ def uncommitted
47
+ [
48
+ 'set @lhm_auto_commit = @@session.autocommit',
49
+ 'set session autocommit = 0',
50
+ yield,
51
+ 'set session autocommit = @lhm_auto_commit'
52
+ ].flatten
53
+ end
54
+
55
+ def validate
56
+ unless @connection.data_source_exists?(@origin.name) &&
57
+ @connection.data_source_exists?(@destination.name)
58
+ error "`#{ @origin.name }` and `#{ @destination.name }` must exist"
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def revert
65
+ @connection.execute(tagged('unlock tables'))
66
+ end
67
+
68
+ def execute
69
+ statements.each do |stmt|
70
+ @connection.execute(tagged(stmt))
71
+ end
72
+ end
73
+ end
74
+ end