lhm-teak 3.6.0

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