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,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