lhm-teak 3.6.0

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 (112) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +43 -0
  3. data/.gitignore +12 -0
  4. data/.rubocop.yml +183 -0
  5. data/.travis.yml +21 -0
  6. data/Appraisals +24 -0
  7. data/CHANGELOG.md +254 -0
  8. data/Gemfile +5 -0
  9. data/Gemfile.lock +67 -0
  10. data/LICENSE +27 -0
  11. data/README.md +335 -0
  12. data/Rakefile +33 -0
  13. data/dev.yml +45 -0
  14. data/docker-compose.yml +60 -0
  15. data/gemfiles/activerecord_5.2.gemfile +9 -0
  16. data/gemfiles/activerecord_5.2.gemfile.lock +66 -0
  17. data/gemfiles/activerecord_6.0.gemfile +7 -0
  18. data/gemfiles/activerecord_6.0.gemfile.lock +68 -0
  19. data/gemfiles/activerecord_6.1.gemfile +7 -0
  20. data/gemfiles/activerecord_6.1.gemfile.lock +67 -0
  21. data/gemfiles/activerecord_7.0.0.alpha2.gemfile +7 -0
  22. data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +65 -0
  23. data/lhm.gemspec +38 -0
  24. data/lib/lhm/atomic_switcher.rb +46 -0
  25. data/lib/lhm/chunk_finder.rb +62 -0
  26. data/lib/lhm/chunk_insert.rb +61 -0
  27. data/lib/lhm/chunker.rb +95 -0
  28. data/lib/lhm/cleanup/current.rb +71 -0
  29. data/lib/lhm/command.rb +48 -0
  30. data/lib/lhm/connection.rb +108 -0
  31. data/lib/lhm/entangler.rb +112 -0
  32. data/lib/lhm/intersection.rb +51 -0
  33. data/lib/lhm/invoker.rb +100 -0
  34. data/lib/lhm/locked_switcher.rb +76 -0
  35. data/lib/lhm/migration.rb +51 -0
  36. data/lib/lhm/migrator.rb +244 -0
  37. data/lib/lhm/printer.rb +63 -0
  38. data/lib/lhm/proxysql_helper.rb +10 -0
  39. data/lib/lhm/railtie.rb +9 -0
  40. data/lib/lhm/sql_helper.rb +77 -0
  41. data/lib/lhm/sql_retry.rb +180 -0
  42. data/lib/lhm/table.rb +121 -0
  43. data/lib/lhm/table_name.rb +23 -0
  44. data/lib/lhm/test_support.rb +35 -0
  45. data/lib/lhm/throttler/slave_lag.rb +162 -0
  46. data/lib/lhm/throttler/threads_running.rb +53 -0
  47. data/lib/lhm/throttler/time.rb +29 -0
  48. data/lib/lhm/throttler.rb +36 -0
  49. data/lib/lhm/timestamp.rb +11 -0
  50. data/lib/lhm/version.rb +6 -0
  51. data/lib/lhm-shopify.rb +1 -0
  52. data/lib/lhm.rb +156 -0
  53. data/scripts/helpers/wait-for-dbs.sh +21 -0
  54. data/scripts/mysql/reader/create_replication.sql +10 -0
  55. data/scripts/mysql/writer/create_test_db.sql +1 -0
  56. data/scripts/mysql/writer/create_users.sql +6 -0
  57. data/scripts/proxysql/proxysql.cnf +117 -0
  58. data/shipit.rubygems.yml +0 -0
  59. data/spec/.lhm.example +4 -0
  60. data/spec/README.md +58 -0
  61. data/spec/fixtures/bigint_table.ddl +4 -0
  62. data/spec/fixtures/composite_primary_key.ddl +6 -0
  63. data/spec/fixtures/composite_primary_key_dest.ddl +6 -0
  64. data/spec/fixtures/custom_primary_key.ddl +6 -0
  65. data/spec/fixtures/custom_primary_key_dest.ddl +6 -0
  66. data/spec/fixtures/destination.ddl +6 -0
  67. data/spec/fixtures/lines.ddl +7 -0
  68. data/spec/fixtures/origin.ddl +6 -0
  69. data/spec/fixtures/permissions.ddl +5 -0
  70. data/spec/fixtures/small_table.ddl +4 -0
  71. data/spec/fixtures/tracks.ddl +5 -0
  72. data/spec/fixtures/users.ddl +14 -0
  73. data/spec/fixtures/wo_id_int_column.ddl +6 -0
  74. data/spec/integration/atomic_switcher_spec.rb +129 -0
  75. data/spec/integration/chunk_insert_spec.rb +30 -0
  76. data/spec/integration/chunker_spec.rb +269 -0
  77. data/spec/integration/cleanup_spec.rb +147 -0
  78. data/spec/integration/database.yml +25 -0
  79. data/spec/integration/entangler_spec.rb +68 -0
  80. data/spec/integration/integration_helper.rb +252 -0
  81. data/spec/integration/invoker_spec.rb +33 -0
  82. data/spec/integration/lhm_spec.rb +659 -0
  83. data/spec/integration/lock_wait_timeout_spec.rb +30 -0
  84. data/spec/integration/locked_switcher_spec.rb +50 -0
  85. data/spec/integration/proxysql_spec.rb +34 -0
  86. data/spec/integration/sql_retry/db_connection_helper.rb +52 -0
  87. data/spec/integration/sql_retry/lock_wait_spec.rb +127 -0
  88. data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +114 -0
  89. data/spec/integration/sql_retry/proxysql_helper.rb +22 -0
  90. data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +109 -0
  91. data/spec/integration/table_spec.rb +83 -0
  92. data/spec/integration/toxiproxy_helper.rb +40 -0
  93. data/spec/test_helper.rb +69 -0
  94. data/spec/unit/atomic_switcher_spec.rb +29 -0
  95. data/spec/unit/chunk_finder_spec.rb +73 -0
  96. data/spec/unit/chunk_insert_spec.rb +67 -0
  97. data/spec/unit/chunker_spec.rb +176 -0
  98. data/spec/unit/connection_spec.rb +111 -0
  99. data/spec/unit/entangler_spec.rb +187 -0
  100. data/spec/unit/intersection_spec.rb +51 -0
  101. data/spec/unit/lhm_spec.rb +46 -0
  102. data/spec/unit/locked_switcher_spec.rb +46 -0
  103. data/spec/unit/migrator_spec.rb +144 -0
  104. data/spec/unit/printer_spec.rb +85 -0
  105. data/spec/unit/sql_helper_spec.rb +28 -0
  106. data/spec/unit/table_name_spec.rb +39 -0
  107. data/spec/unit/table_spec.rb +47 -0
  108. data/spec/unit/throttler/slave_lag_spec.rb +322 -0
  109. data/spec/unit/throttler/threads_running_spec.rb +64 -0
  110. data/spec/unit/throttler_spec.rb +124 -0
  111. data/spec/unit/unit_helper.rb +26 -0
  112. metadata +366 -0
@@ -0,0 +1,36 @@
1
+ require 'lhm/throttler/time'
2
+ require 'lhm/throttler/slave_lag'
3
+ require 'lhm/throttler/threads_running'
4
+
5
+ module Lhm
6
+ module Throttler
7
+ CLASSES = { :time_throttler => Throttler::Time,
8
+ :slave_lag_throttler => Throttler::SlaveLag,
9
+ :threads_running_throttler => Throttler::ThreadsRunning }
10
+
11
+ def throttler
12
+ @throttler ||= Throttler::Time.new
13
+ end
14
+
15
+ def setup_throttler(type, options = {})
16
+ @throttler = Factory.create_throttler(type, options)
17
+ end
18
+
19
+ class Factory
20
+ def self.create_throttler(type, options = {})
21
+ case type
22
+ when Lhm::Command
23
+ type
24
+ when Symbol
25
+ CLASSES[type].new(options)
26
+ when String
27
+ CLASSES[type.to_sym].new(options)
28
+ when Class
29
+ type.new(options)
30
+ else
31
+ raise ArgumentError, 'type argument must be a Symbol, String or Class'
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ module Lhm
2
+ class Timestamp
3
+ def initialize(time)
4
+ @time = time
5
+ end
6
+
7
+ def to_s
8
+ @time.strftime "%Y_%m_%d_%H_%M_%S_#{ '%03d' % (@time.usec / 1000) }"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ module Lhm
5
+ VERSION = '3.6.0'
6
+ end
@@ -0,0 +1 @@
1
+ require "lhm"
data/lib/lhm.rb ADDED
@@ -0,0 +1,156 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ require 'lhm/table_name'
5
+ require 'lhm/table'
6
+ require 'lhm/invoker'
7
+ require 'lhm/throttler'
8
+ require 'lhm/version'
9
+ require 'lhm/cleanup/current'
10
+ require 'lhm/sql_retry'
11
+ require 'lhm/proxysql_helper'
12
+ require 'lhm/connection'
13
+ require 'lhm/test_support'
14
+ require 'lhm/railtie' if defined?(Rails::Railtie)
15
+ require 'logger'
16
+
17
+ # Large hadron migrator - online schema change tool
18
+ #
19
+ # @example
20
+ #
21
+ # Lhm.change_table(:users) do |m|
22
+ # m.add_column(:arbitrary, "INT(12)")
23
+ # m.add_index([:arbitrary, :created_at])
24
+ # m.ddl("alter table %s add column flag tinyint(1)" % m.name)
25
+ # end
26
+ #
27
+ module Lhm
28
+ extend Throttler
29
+ extend self
30
+
31
+ DEFAULT_LOGGER_OPTIONS = { level: Logger::INFO, file: STDOUT }
32
+
33
+ # Alters a table with the changes described in the block
34
+ #
35
+ # @param [String, Symbol] table_name Name of the table
36
+ # @param [Hash] options Optional options to alter the chunk / switch behavior
37
+ # @option options [Integer] :stride
38
+ # Size of a chunk (defaults to: 2,000)
39
+ # @option options [Integer] :throttle
40
+ # Time to wait between chunks in milliseconds (defaults to: 100)
41
+ # @option options [Integer] :start
42
+ # Primary Key position at which to start copying chunks
43
+ # @option options [Integer] :limit
44
+ # Primary Key position at which to stop copying chunks
45
+ # @option options [Boolean] :atomic_switch
46
+ # Use atomic switch to rename tables (defaults to: true)
47
+ # If using a version of mysql affected by atomic switch bug, LHM forces user
48
+ # to set this option (see SqlHelper#supports_atomic_switch?)
49
+ # @option options [Boolean] :reconnect_with_consistent_host
50
+ # Active / Deactivate ProxySQL-aware reconnection procedure (default to: false)
51
+ # @yield [Migrator] Yielded Migrator object records the changes
52
+ # @return [Boolean] Returns true if the migration finishes
53
+ # @raise [Error] Raises Lhm::Error in case of a error and aborts the migration
54
+ def change_table(table_name, options = {}, &block)
55
+ with_flags(options) do
56
+ origin = Table.parse(table_name, connection)
57
+ invoker = Invoker.new(origin, connection)
58
+ block.call(invoker.migrator)
59
+ invoker.run(options)
60
+ true
61
+ end
62
+ end
63
+
64
+ # Cleanup tables and triggers
65
+ #
66
+ # @param [Boolean] run execute now or just display information
67
+ # @param [Hash] options Optional options to alter cleanup behaviour
68
+ # @option options [Time] :until
69
+ # Filter to only remove tables up to specified time (defaults to: nil)
70
+ def cleanup(run = false, options = {})
71
+ lhm_tables = connection.select_values('show tables').select { |name| name =~ /^lhm(a|n)_/ }
72
+ if options[:until]
73
+ lhm_tables.select! do |table|
74
+ table_date_time = Time.strptime(table, 'lhma_%Y_%m_%d_%H_%M_%S')
75
+ table_date_time <= options[:until]
76
+ end
77
+ end
78
+
79
+ lhm_triggers = connection.select_values('show triggers').collect do |trigger|
80
+ trigger.respond_to?(:trigger) ? trigger.trigger : trigger
81
+ end.select { |name| name =~ /^lhmt/ }
82
+
83
+ drop_tables_and_triggers(run, lhm_triggers, lhm_tables)
84
+ end
85
+
86
+ def cleanup_current_run(run, table_name, options = {})
87
+ Lhm::Cleanup::Current.new(run, table_name, connection, options).execute
88
+ end
89
+
90
+ # Setups DB connection
91
+ #
92
+ # @param [ActiveRecord::Base] connection ActiveRecord Connection
93
+ def setup(connection)
94
+ @@connection = Connection.new(connection: connection)
95
+ end
96
+
97
+ # Returns DB connection (or initializes it if not created yet)
98
+ def connection
99
+ @@connection ||= begin
100
+ raise 'Please call Lhm.setup' unless defined?(ActiveRecord)
101
+ @@connection = Connection.new(connection: ActiveRecord::Base.connection)
102
+ end
103
+ end
104
+
105
+ def self.logger=(new_logger)
106
+ @@logger = new_logger
107
+ end
108
+
109
+ def self.logger
110
+ @@logger ||=
111
+ begin
112
+ logger = Logger.new(DEFAULT_LOGGER_OPTIONS[:file])
113
+ logger.level = DEFAULT_LOGGER_OPTIONS[:level]
114
+ logger.formatter = nil
115
+ logger
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def drop_tables_and_triggers(run = false, triggers, tables)
122
+ if run
123
+ triggers.each do |trigger|
124
+ connection.execute("drop trigger if exists #{trigger}")
125
+ end
126
+ logger.info("Dropped triggers #{triggers.join(', ')}")
127
+
128
+ tables.each do |table|
129
+ connection.execute("drop table if exists #{table}")
130
+ end
131
+ logger.info("Dropped tables #{tables.join(', ')}")
132
+
133
+ true
134
+ elsif tables.empty? && triggers.empty?
135
+ logger.info('Everything is clean. Nothing to do.')
136
+ true
137
+ else
138
+ logger.info("Would drop LHM backup tables: #{tables.join(', ')}.")
139
+ logger.info("Would drop LHM triggers: #{triggers.join(', ')}.")
140
+ logger.info('Run with Lhm.cleanup(true) to drop all LHM triggers and tables, or Lhm.cleanup_current_run(true, table_name) to clean up a specific LHM.')
141
+ false
142
+ end
143
+ end
144
+
145
+ def with_flags(options)
146
+ old_flags = {
147
+ reconnect_with_consistent_host: Lhm.connection.reconnect_with_consistent_host,
148
+ }
149
+
150
+ Lhm.connection.reconnect_with_consistent_host = options[:reconnect_with_consistent_host] || false
151
+
152
+ yield
153
+ ensure
154
+ Lhm.connection.reconnect_with_consistent_host = old_flags[:reconnect_with_consistent_host]
155
+ end
156
+ end
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+ # Wait for writer
3
+ echo "Waiting for MySQL-1: "
4
+ while ! (mysqladmin ping --host="127.0.0.1" --port=33006 --user=root --password=password --silent 2> /dev/null); do
5
+ echo -ne "."
6
+ sleep 1
7
+ done
8
+ # Wait for reader
9
+ echo "Waiting for MySQL-2: "
10
+ while ! (mysqladmin ping --host="127.0.0.1" --port=33007 --user=root --password=password --silent 2> /dev/null); do
11
+ echo -ne "."
12
+ sleep 1
13
+ done
14
+ # Wait for proxysql
15
+ echo "Waiting for ProxySQL:"
16
+ while ! (mysqladmin ping --host="127.0.0.1" --port=33005 --user=root --password=password --silent 2> /dev/null); do
17
+ echo -ne "."
18
+ sleep 1
19
+ done
20
+
21
+ echo "All DBs are ready"
@@ -0,0 +1,10 @@
1
+ STOP SLAVE;
2
+ CHANGE MASTER TO
3
+ MASTER_HOST='mysql-1',
4
+ MASTER_AUTO_POSITION=1,
5
+ MASTER_USER='replication',
6
+ MASTER_PASSWORD='password',
7
+ MASTER_CONNECT_RETRY=1,
8
+ MASTER_RETRY_COUNT=300; -- 5 minutes
9
+
10
+ start slave;
@@ -0,0 +1 @@
1
+ CREATE DATABASE test;
@@ -0,0 +1,6 @@
1
+ # Creates replication user in Writer
2
+ CREATE USER IF NOT EXISTS 'writer'@'%' IDENTIFIED BY 'password';
3
+ CREATE USER IF NOT EXISTS 'reader'@'%' IDENTIFIED BY 'password';
4
+
5
+ CREATE USER IF NOT EXISTS 'replication'@'%' IDENTIFIED BY 'password';
6
+ GRANT REPLICATION SLAVE ON *.* TO' replication'@'%' IDENTIFIED BY 'password';
@@ -0,0 +1,117 @@
1
+ #file proxysql.cfg
2
+
3
+ datadir="/var/lib/proxysql"
4
+ restart_on_missing_heartbeats=999999
5
+ query_parser_token_delimiters=","
6
+ query_parser_key_value_delimiters=":"
7
+ unit_of_work_identifiers="consistent_read_id"
8
+
9
+ admin_variables=
10
+ {
11
+ mysql_ifaces="0.0.0.0:6032"
12
+ admin_credentials="admin:password;remote-admin:password"
13
+ }
14
+
15
+ mysql_servers =
16
+ (
17
+ {
18
+ address="mysql-1"
19
+ port=3306
20
+ hostgroup=0
21
+ max_connections=200
22
+ },
23
+ {
24
+ address="mysql-2"
25
+ port=3306
26
+ hostgroup=1
27
+ max_connections=200
28
+ }
29
+ )
30
+
31
+ mysql_variables=
32
+ {
33
+ session_idle_ms=1
34
+ auto_increment_delay_multiplex=0
35
+
36
+ threads=8
37
+ max_connections=100000
38
+ interfaces="0.0.0.0:3306"
39
+ server_version="5.7.18-proxysql"
40
+ connect_timeout_server=10000
41
+ connect_timeout_server_max=10000
42
+ connect_retries_on_failure=0
43
+ default_charset="utf8mb4"
44
+ free_connections_pct=100
45
+ connection_warming=true
46
+ max_allowed_packet=16777216
47
+ monitor_enabled=false
48
+ query_retries_on_failure=0
49
+ shun_on_failures=999999
50
+ shun_recovery_time_sec=0
51
+ kill_backend_connection_when_disconnect=false
52
+ stats_time_backend_query=false
53
+ stats_time_query_processor=false
54
+ max_stmts_per_connection=5
55
+ default_max_latency_ms=999999
56
+ wait_timeout=1800000
57
+ eventslog_format=3
58
+ log_multiplexing_disabled=true
59
+ log_unhealthy_connections=false
60
+ }
61
+
62
+ # defines all the MySQL users
63
+ mysql_users:
64
+ (
65
+ {
66
+ username = "root"
67
+ password = "password"
68
+ default_hostgroup = 0
69
+ max_connections=1000
70
+ active = 1
71
+ },
72
+ {
73
+ username = "writer"
74
+ password = "password"
75
+ default_hostgroup = 0
76
+ max_connections=50000
77
+ active = 1
78
+ transaction_persistent=1
79
+ },
80
+ {
81
+ username = "reader"
82
+ password = "password"
83
+ default_hostgroup = 1
84
+ max_connections=50000
85
+ active = 1
86
+ transaction_persistent=1
87
+ }
88
+ )
89
+
90
+ #defines MySQL Query Rules
91
+ mysql_query_rules:
92
+ (
93
+ {
94
+ rule_id = 1
95
+ active = 1
96
+ match_digest = "@@SESSION"
97
+ multiplex = 2
98
+ },
99
+ {
100
+ rule_id = 2
101
+ active = 1
102
+ match_digest = "@@global\.server_id"
103
+ multiplex = 2
104
+ },
105
+ {
106
+ rule_id = 3
107
+ active = 1
108
+ match_digest = "@@global\.hostname"
109
+ multiplex = 2
110
+ },
111
+ {
112
+ rule_id = 4
113
+ active = 1
114
+ match_pattern = "maintenance:lhm"
115
+ destination_hostgroup = 0
116
+ }
117
+ )
File without changes
data/spec/.lhm.example ADDED
@@ -0,0 +1,4 @@
1
+ mysqldir=/usr/local/mysql
2
+ basedir=~/lhm-cluster
3
+ master_port=3306
4
+ slave_port=3307
data/spec/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # Preparing for master slave integration tests
2
+
3
+ ## Configuration
4
+
5
+ create ~/.lhm:
6
+
7
+ mysqldir=/usr/local/mysql
8
+ basedir=~/lhm-cluster
9
+ master_port=3306
10
+ slave_port=3307
11
+
12
+ mysqldir specifies the location of your mysql install. basedir is the
13
+ directory master and slave databases will get installed into.
14
+
15
+ ## Automatic setup
16
+
17
+ ### Run
18
+
19
+ bin/lhm-spec-clobber.sh
20
+
21
+ You can set the integration specs up to run against a master slave setup by
22
+ running the included that. This deletes the configured lhm master slave setup and reinstalls and configures a master slave setup.
23
+
24
+ Follow the manual instructions if you want more control over this process.
25
+
26
+ ## Manual setup
27
+
28
+ ### set up instances
29
+
30
+ bin/lhm-spec-setup-cluster.sh
31
+
32
+ ### start instances
33
+
34
+ basedir=/opt/lhm-luster
35
+ mysqld --defaults-file="$basedir/master/my.cnf"
36
+ mysqld --defaults-file="$basedir/slave/my.cnf"
37
+
38
+ ### run the grants
39
+
40
+ bin/lhm-spec-grants.sh
41
+
42
+ ## run specs
43
+
44
+ Setup the dependency gems
45
+
46
+ export BUNDLE_GEMFILE=gemfiles/ar-4.2_mysql2.gemfile
47
+ bundle install
48
+
49
+ To run specs in slave mode, set the MASTER_SLAVE=1 when running tests:
50
+
51
+ MASTER_SLAVE=1 bundle exec rake specs
52
+
53
+ # connecting
54
+
55
+ you can connect by running (with the respective ports):
56
+
57
+ mysql --protocol=TCP -p3307
58
+
@@ -0,0 +1,4 @@
1
+ CREATE TABLE `bigint_table` (
2
+ `id` BIGINT(20) auto_increment,
3
+ PRIMARY KEY (`id`)
4
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `composite_primary_key` (
2
+ `id` bigint(20) NOT NULL AUTO_INCREMENT,
3
+ `shop_id` bigint(20) NOT NULL,
4
+ CONSTRAINT `pk_composite` PRIMARY KEY (`shop_id`,`id`),
5
+ INDEX `index_key_id` (`id`)
6
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `composite_primary_key_dest` (
2
+ `id` bigint(20) NOT NULL AUTO_INCREMENT,
3
+ `shop_id` bigint(20) NOT NULL,
4
+ CONSTRAINT `pk_composite` PRIMARY KEY (`shop_id`,`id`),
5
+ INDEX `index_key_id` (`id`)
6
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `custom_primary_key` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `pk` varchar(255),
4
+ PRIMARY KEY (`pk`),
5
+ UNIQUE KEY `index_custom_primary_key_on_id` (`id`)
6
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `custom_primary_key_dest` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `pk` varchar(255),
4
+ PRIMARY KEY (`pk`),
5
+ UNIQUE KEY `index_custom_primary_key_on_id` (`id`)
6
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `destination` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `destination` int(11) DEFAULT NULL,
4
+ `common` varchar(255) DEFAULT NULL,
5
+ PRIMARY KEY (`id`)
6
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,7 @@
1
+ CREATE TABLE `lines` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `between` varchar(10),
4
+ `lines` int(11),
5
+ `key` varchar(10),
6
+ PRIMARY KEY (`id`)
7
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `origin` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `origin` int(11) DEFAULT NULL,
4
+ `common` varchar(255) DEFAULT NULL,
5
+ PRIMARY KEY (`id`)
6
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,5 @@
1
+ CREATE TABLE `permissions` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `track_id` int(11) DEFAULT NULL,
4
+ PRIMARY KEY (`id`)
5
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,4 @@
1
+ CREATE TABLE `small_table` (
2
+ `id` INT(11) auto_increment,
3
+ PRIMARY KEY (`id`)
4
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,5 @@
1
+ CREATE TABLE `tracks` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `public` int(4) DEFAULT 0,
4
+ PRIMARY KEY (`id`)
5
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,14 @@
1
+ CREATE TABLE `users` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `reference` int(11) DEFAULT NULL,
4
+ `username` varchar(255) DEFAULT NULL,
5
+ `group` varchar(255) DEFAULT 'Superfriends',
6
+ `created_at` datetime DEFAULT NULL,
7
+ `comment` varchar(20) DEFAULT NULL,
8
+ `description` text,
9
+ PRIMARY KEY (`id`),
10
+ UNIQUE KEY `index_users_on_reference` (`reference`),
11
+ KEY `index_users_on_username_and_created_at` (`username`,`created_at`),
12
+ KEY `index_with_a_custom_name` (`username`,`group`)
13
+
14
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,6 @@
1
+ -- Without id int column
2
+ CREATE TABLE `wo_id_int_column` (
3
+ `id` varchar(15) NOT NULL,
4
+ PRIMARY KEY (`id`)
5
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
6
+
@@ -0,0 +1,129 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
5
+
6
+ require 'lhm/table'
7
+ require 'lhm/migration'
8
+ require 'lhm/atomic_switcher'
9
+ require 'lhm/connection'
10
+
11
+ describe Lhm::AtomicSwitcher do
12
+ include IntegrationHelper
13
+
14
+ before(:each) { connect_master! }
15
+
16
+ describe 'switching' do
17
+ before(:each) do
18
+ Thread.abort_on_exception = true
19
+ @origin = table_create('origin')
20
+ @destination = table_create('destination')
21
+ @migration = Lhm::Migration.new(@origin, @destination)
22
+ @logs = StringIO.new
23
+ Lhm.logger = Logger.new(@logs)
24
+ @connection.execute('SET GLOBAL innodb_lock_wait_timeout=3')
25
+ @connection.execute('SET GLOBAL lock_wait_timeout=3')
26
+ end
27
+
28
+ after(:each) do
29
+ Thread.abort_on_exception = false
30
+ end
31
+
32
+ it 'should retry and log on lock wait timeouts' do
33
+ ar_connection = mock()
34
+ ar_connection.stubs(:data_source_exists?).returns(true)
35
+ ar_connection.stubs(:active?).returns(true)
36
+ ar_connection.stubs(:execute).returns([["dummy"]], [["dummy"]], [["dummy"]])
37
+ .then
38
+ .raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
39
+ .then
40
+ .returns([["dummy"]]) # Matches initial host -> triggers retry
41
+
42
+ connection = Lhm::Connection.new(connection: ar_connection, options: {
43
+ reconnect_with_consistent_host: true,
44
+ retriable: {
45
+ tries: 3,
46
+ base_interval: 0
47
+ }
48
+ })
49
+
50
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection)
51
+
52
+ assert switcher.run
53
+
54
+ log_messages = @logs.string.split("\n")
55
+ assert_equal(2, log_messages.length)
56
+ assert log_messages[0].include? "Starting run of class=Lhm::AtomicSwitcher"
57
+ # On failure of this assertion, check for Lhm::Connection#file
58
+ assert log_messages[1].include? "[AtomicSwitcher] ActiveRecord::StatementInvalid: 'Lock wait timeout exceeded; try restarting transaction.' - 1 tries"
59
+ end
60
+
61
+ it 'should give up on lock wait timeouts after a configured number of tries' do
62
+ ar_connection = mock()
63
+ ar_connection.stubs(:data_source_exists?).returns(true)
64
+ ar_connection.stubs(:active?).returns(true)
65
+ ar_connection.stubs(:execute).returns([["dummy"]], [["dummy"]], [["dummy"]])
66
+ .then
67
+ .raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
68
+ .then
69
+ .returns([["dummy"]]) # triggers retry 1
70
+ .then
71
+ .raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
72
+ .then
73
+ .returns([["dummy"]]) # triggers retry 2
74
+ .then
75
+ .raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.') # triggers retry 2
76
+
77
+ connection = Lhm::Connection.new(connection: ar_connection, options: {
78
+ reconnect_with_consistent_host: true,
79
+ retriable: {
80
+ tries: 2,
81
+ base_interval: 0
82
+ }
83
+ })
84
+
85
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection)
86
+
87
+ assert_raises(ActiveRecord::StatementInvalid) { switcher.run }
88
+ end
89
+
90
+ it 'should raise on non lock wait timeout exceptions' do
91
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection)
92
+ switcher.send :define_singleton_method, :atomic_switch do
93
+ 'SELECT * FROM nonexistent'
94
+ end
95
+ value(-> { switcher.run }).must_raise(ActiveRecord::StatementInvalid)
96
+ end
97
+
98
+ it "should raise when destination doesn't exist" do
99
+ ar_connection = mock()
100
+ ar_connection.stubs(:data_source_exists?).returns(false)
101
+
102
+ connection = Lhm::Connection.new(connection: ar_connection)
103
+
104
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection)
105
+
106
+ assert_raises(Lhm::Error) { switcher.run }
107
+ end
108
+
109
+ it 'rename origin to archive' do
110
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection)
111
+ switcher.run
112
+
113
+ slave do
114
+ value(data_source_exists?(@origin)).must_equal true
115
+ value(table_read(@migration.archive_name).columns.keys).must_include 'origin'
116
+ end
117
+ end
118
+
119
+ it 'rename destination to origin' do
120
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection)
121
+ switcher.run
122
+
123
+ slave do
124
+ value(data_source_exists?(@destination)).must_equal false
125
+ value(table_read(@origin.name).columns.keys).must_include 'destination'
126
+ end
127
+ end
128
+ end
129
+ end