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.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +43 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +183 -0
- data/.travis.yml +21 -0
- data/Appraisals +24 -0
- data/CHANGELOG.md +254 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +27 -0
- data/README.md +335 -0
- data/Rakefile +33 -0
- data/dev.yml +45 -0
- data/docker-compose.yml +60 -0
- data/gemfiles/activerecord_5.2.gemfile +9 -0
- data/gemfiles/activerecord_5.2.gemfile.lock +66 -0
- data/gemfiles/activerecord_6.0.gemfile +7 -0
- data/gemfiles/activerecord_6.0.gemfile.lock +68 -0
- data/gemfiles/activerecord_6.1.gemfile +7 -0
- data/gemfiles/activerecord_6.1.gemfile.lock +67 -0
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile +7 -0
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +65 -0
- data/lhm.gemspec +38 -0
- data/lib/lhm/atomic_switcher.rb +46 -0
- data/lib/lhm/chunk_finder.rb +62 -0
- data/lib/lhm/chunk_insert.rb +61 -0
- data/lib/lhm/chunker.rb +95 -0
- data/lib/lhm/cleanup/current.rb +71 -0
- data/lib/lhm/command.rb +48 -0
- data/lib/lhm/connection.rb +108 -0
- data/lib/lhm/entangler.rb +112 -0
- data/lib/lhm/intersection.rb +51 -0
- data/lib/lhm/invoker.rb +100 -0
- data/lib/lhm/locked_switcher.rb +76 -0
- data/lib/lhm/migration.rb +51 -0
- data/lib/lhm/migrator.rb +244 -0
- data/lib/lhm/printer.rb +63 -0
- data/lib/lhm/proxysql_helper.rb +10 -0
- data/lib/lhm/railtie.rb +9 -0
- data/lib/lhm/sql_helper.rb +77 -0
- data/lib/lhm/sql_retry.rb +180 -0
- data/lib/lhm/table.rb +121 -0
- data/lib/lhm/table_name.rb +23 -0
- data/lib/lhm/test_support.rb +35 -0
- data/lib/lhm/throttler/slave_lag.rb +162 -0
- data/lib/lhm/throttler/threads_running.rb +53 -0
- data/lib/lhm/throttler/time.rb +29 -0
- data/lib/lhm/throttler.rb +36 -0
- data/lib/lhm/timestamp.rb +11 -0
- data/lib/lhm/version.rb +6 -0
- data/lib/lhm-shopify.rb +1 -0
- data/lib/lhm.rb +156 -0
- data/scripts/helpers/wait-for-dbs.sh +21 -0
- data/scripts/mysql/reader/create_replication.sql +10 -0
- data/scripts/mysql/writer/create_test_db.sql +1 -0
- data/scripts/mysql/writer/create_users.sql +6 -0
- data/scripts/proxysql/proxysql.cnf +117 -0
- data/shipit.rubygems.yml +0 -0
- data/spec/.lhm.example +4 -0
- data/spec/README.md +58 -0
- data/spec/fixtures/bigint_table.ddl +4 -0
- data/spec/fixtures/composite_primary_key.ddl +6 -0
- data/spec/fixtures/composite_primary_key_dest.ddl +6 -0
- data/spec/fixtures/custom_primary_key.ddl +6 -0
- data/spec/fixtures/custom_primary_key_dest.ddl +6 -0
- data/spec/fixtures/destination.ddl +6 -0
- data/spec/fixtures/lines.ddl +7 -0
- data/spec/fixtures/origin.ddl +6 -0
- data/spec/fixtures/permissions.ddl +5 -0
- data/spec/fixtures/small_table.ddl +4 -0
- data/spec/fixtures/tracks.ddl +5 -0
- data/spec/fixtures/users.ddl +14 -0
- data/spec/fixtures/wo_id_int_column.ddl +6 -0
- data/spec/integration/atomic_switcher_spec.rb +129 -0
- data/spec/integration/chunk_insert_spec.rb +30 -0
- data/spec/integration/chunker_spec.rb +269 -0
- data/spec/integration/cleanup_spec.rb +147 -0
- data/spec/integration/database.yml +25 -0
- data/spec/integration/entangler_spec.rb +68 -0
- data/spec/integration/integration_helper.rb +252 -0
- data/spec/integration/invoker_spec.rb +33 -0
- data/spec/integration/lhm_spec.rb +659 -0
- data/spec/integration/lock_wait_timeout_spec.rb +30 -0
- data/spec/integration/locked_switcher_spec.rb +50 -0
- data/spec/integration/proxysql_spec.rb +34 -0
- data/spec/integration/sql_retry/db_connection_helper.rb +52 -0
- data/spec/integration/sql_retry/lock_wait_spec.rb +127 -0
- data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +114 -0
- data/spec/integration/sql_retry/proxysql_helper.rb +22 -0
- data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +109 -0
- data/spec/integration/table_spec.rb +83 -0
- data/spec/integration/toxiproxy_helper.rb +40 -0
- data/spec/test_helper.rb +69 -0
- data/spec/unit/atomic_switcher_spec.rb +29 -0
- data/spec/unit/chunk_finder_spec.rb +73 -0
- data/spec/unit/chunk_insert_spec.rb +67 -0
- data/spec/unit/chunker_spec.rb +176 -0
- data/spec/unit/connection_spec.rb +111 -0
- data/spec/unit/entangler_spec.rb +187 -0
- data/spec/unit/intersection_spec.rb +51 -0
- data/spec/unit/lhm_spec.rb +46 -0
- data/spec/unit/locked_switcher_spec.rb +46 -0
- data/spec/unit/migrator_spec.rb +144 -0
- data/spec/unit/printer_spec.rb +85 -0
- data/spec/unit/sql_helper_spec.rb +28 -0
- data/spec/unit/table_name_spec.rb +39 -0
- data/spec/unit/table_spec.rb +47 -0
- data/spec/unit/throttler/slave_lag_spec.rb +322 -0
- data/spec/unit/throttler/threads_running_spec.rb +64 -0
- data/spec/unit/throttler_spec.rb +124 -0
- data/spec/unit/unit_helper.rb +26 -0
- metadata +366 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: ..
|
|
3
|
+
specs:
|
|
4
|
+
lhm-shopify (3.5.5)
|
|
5
|
+
retriable (>= 3.0.0)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
activemodel (7.0.0.alpha2)
|
|
11
|
+
activesupport (= 7.0.0.alpha2)
|
|
12
|
+
activerecord (7.0.0.alpha2)
|
|
13
|
+
activemodel (= 7.0.0.alpha2)
|
|
14
|
+
activesupport (= 7.0.0.alpha2)
|
|
15
|
+
activesupport (7.0.0.alpha2)
|
|
16
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
17
|
+
i18n (>= 1.6, < 2)
|
|
18
|
+
minitest (>= 5.1)
|
|
19
|
+
tzinfo (~> 2.0)
|
|
20
|
+
after_do (0.4.0)
|
|
21
|
+
appraisal (2.4.1)
|
|
22
|
+
bundler
|
|
23
|
+
rake
|
|
24
|
+
thor (>= 0.14.0)
|
|
25
|
+
byebug (11.1.3)
|
|
26
|
+
concurrent-ruby (1.1.9)
|
|
27
|
+
docile (1.4.0)
|
|
28
|
+
i18n (1.8.11)
|
|
29
|
+
concurrent-ruby (~> 1.0)
|
|
30
|
+
minitest (5.14.4)
|
|
31
|
+
mocha (1.13.0)
|
|
32
|
+
mysql2 (0.5.3)
|
|
33
|
+
rake (13.0.6)
|
|
34
|
+
retriable (3.1.2)
|
|
35
|
+
simplecov (0.21.2)
|
|
36
|
+
docile (~> 1.1)
|
|
37
|
+
simplecov-html (~> 0.11)
|
|
38
|
+
simplecov_json_formatter (~> 0.1)
|
|
39
|
+
simplecov-html (0.12.3)
|
|
40
|
+
simplecov_json_formatter (0.1.3)
|
|
41
|
+
thor (1.1.0)
|
|
42
|
+
toxiproxy (2.0.0)
|
|
43
|
+
tzinfo (2.0.4)
|
|
44
|
+
concurrent-ruby (~> 1.0)
|
|
45
|
+
|
|
46
|
+
PLATFORMS
|
|
47
|
+
arm64-darwin-21
|
|
48
|
+
x86_64-darwin-20
|
|
49
|
+
x86_64-linux
|
|
50
|
+
|
|
51
|
+
DEPENDENCIES
|
|
52
|
+
activerecord (= 7.0.0.alpha2)
|
|
53
|
+
after_do
|
|
54
|
+
appraisal
|
|
55
|
+
byebug
|
|
56
|
+
lhm-shopify!
|
|
57
|
+
minitest
|
|
58
|
+
mocha
|
|
59
|
+
mysql2
|
|
60
|
+
rake
|
|
61
|
+
simplecov
|
|
62
|
+
toxiproxy
|
|
63
|
+
|
|
64
|
+
BUNDLED WITH
|
|
65
|
+
2.2.22
|
data/lhm.gemspec
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
4
|
+
$:.unshift(lib) unless $:.include?(lib)
|
|
5
|
+
|
|
6
|
+
require 'lhm/version'
|
|
7
|
+
|
|
8
|
+
Gem::Specification.new do |s|
|
|
9
|
+
s.name = 'lhm-teak'
|
|
10
|
+
s.version = Lhm::VERSION
|
|
11
|
+
s.licenses = ['BSD-3-Clause']
|
|
12
|
+
s.platform = Gem::Platform::RUBY
|
|
13
|
+
s.authors = ['SoundCloud', 'Shopify', 'Rany Keddo', 'Tobias Bielohlawek', 'Tobias Schmidt', 'Teak.io']
|
|
14
|
+
s.email = %q{team@teak.io}
|
|
15
|
+
s.summary = %q{online schema changer for mysql}
|
|
16
|
+
s.description = %q{Migrate large tables without downtime by copying to a temporary table in chunks. The old table is not dropped. Instead, it is moved to timestamp_table_name for verification.}
|
|
17
|
+
s.homepage = %q{http://github.com/GoCarrot/lhm}
|
|
18
|
+
s.files = `git ls-files`.split("\n")
|
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
20
|
+
s.require_paths = ['lib']
|
|
21
|
+
s.executables = []
|
|
22
|
+
s.metadata['allowed_push_host'] = "https://rubygems.org"
|
|
23
|
+
|
|
24
|
+
s.required_ruby_version = '>= 2.3.0'
|
|
25
|
+
|
|
26
|
+
s.add_dependency 'retriable', '>= 3.0.0'
|
|
27
|
+
|
|
28
|
+
s.add_development_dependency 'activerecord'
|
|
29
|
+
s.add_development_dependency 'minitest'
|
|
30
|
+
s.add_development_dependency 'mocha'
|
|
31
|
+
s.add_development_dependency 'after_do'
|
|
32
|
+
s.add_development_dependency 'rake'
|
|
33
|
+
s.add_development_dependency 'mysql2'
|
|
34
|
+
s.add_development_dependency 'simplecov'
|
|
35
|
+
s.add_development_dependency 'toxiproxy'
|
|
36
|
+
s.add_development_dependency 'appraisal'
|
|
37
|
+
s.add_development_dependency 'byebug'
|
|
38
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
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_retry'
|
|
7
|
+
|
|
8
|
+
module Lhm
|
|
9
|
+
# Switches origin with destination table using an atomic rename.
|
|
10
|
+
#
|
|
11
|
+
# It should only be used if the MySQL server version is not affected by the
|
|
12
|
+
# bin log affecting bug #39675. This can be verified using
|
|
13
|
+
# Lhm::SqlHelper.supports_atomic_switch?.
|
|
14
|
+
class AtomicSwitcher
|
|
15
|
+
include Command
|
|
16
|
+
|
|
17
|
+
attr_reader :connection
|
|
18
|
+
|
|
19
|
+
LOG_PREFIX = "AtomicSwitcher"
|
|
20
|
+
|
|
21
|
+
def initialize(migration, connection = nil)
|
|
22
|
+
@migration = migration
|
|
23
|
+
@connection = connection
|
|
24
|
+
@origin = migration.origin
|
|
25
|
+
@destination = migration.destination
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def atomic_switch
|
|
29
|
+
"rename table `#{ @origin.name }` to `#{ @migration.archive_name }`, " \
|
|
30
|
+
"`#{ @destination.name }` to `#{ @origin.name }`"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate
|
|
34
|
+
unless @connection.data_source_exists?(@origin.name) &&
|
|
35
|
+
@connection.data_source_exists?(@destination.name)
|
|
36
|
+
error "`#{ @origin.name }` and `#{ @destination.name }` must exist"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def execute
|
|
43
|
+
@connection.execute(atomic_switch, should_retry: true, log_prefix: LOG_PREFIX)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Lhm
|
|
2
|
+
class ChunkFinder
|
|
3
|
+
LOG_PREFIX = "Chunker"
|
|
4
|
+
|
|
5
|
+
def initialize(migration, connection = nil, options = {})
|
|
6
|
+
@migration = migration
|
|
7
|
+
@connection = connection
|
|
8
|
+
@start = options[:start] || select_start_from_db
|
|
9
|
+
@limit = options[:limit] || select_limit_from_db
|
|
10
|
+
@throttler = options[:throttler]
|
|
11
|
+
@processed_rows = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def table_empty?
|
|
15
|
+
start.nil? && limit.nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate
|
|
19
|
+
if start > limit
|
|
20
|
+
raise ArgumentError, "impossible chunk options (limit (#{limit.inspect} must be greater than start (#{start.inspect})"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def each_chunk
|
|
25
|
+
next_id = @start
|
|
26
|
+
@processed_rows = 0
|
|
27
|
+
while next_id <= @limit
|
|
28
|
+
top = upper_id(next_id)
|
|
29
|
+
@processed_rows += @throttler.stride
|
|
30
|
+
yield ChunkInsert.new(@migration, @connection, next_id, top)
|
|
31
|
+
next_id = top + 1
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def max_rows
|
|
36
|
+
@limit - @start + 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def processed_rows
|
|
40
|
+
@processed_rows
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
attr_reader :start, :limit
|
|
46
|
+
|
|
47
|
+
def select_start_from_db
|
|
48
|
+
@connection.select_value("select min(id) from `#{ @migration.origin_name }`")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def select_limit_from_db
|
|
52
|
+
@connection.select_value("select max(id) from `#{ @migration.origin_name }`")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def upper_id(next_id)
|
|
56
|
+
sql = "select id from `#{ @migration.origin_name }` where id >= #{ next_id } order by id limit 1 offset #{ @throttler.stride - 1}"
|
|
57
|
+
top = @connection.select_value(sql, should_retry: true, log_prefix: LOG_PREFIX)
|
|
58
|
+
|
|
59
|
+
[top ? top.to_i : @limit, @limit].min
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'lhm/sql_retry'
|
|
2
|
+
require 'lhm/proxysql_helper'
|
|
3
|
+
|
|
4
|
+
module Lhm
|
|
5
|
+
class ChunkInsert
|
|
6
|
+
|
|
7
|
+
LOG_PREFIX = "ChunkInsert"
|
|
8
|
+
|
|
9
|
+
def initialize(migration, connection, lowest, highest, retry_options = {})
|
|
10
|
+
@migration = migration
|
|
11
|
+
@connection = connection
|
|
12
|
+
@lowest = lowest
|
|
13
|
+
@highest = highest
|
|
14
|
+
@retry_options = retry_options
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def insert_and_return_count_of_rows_created
|
|
18
|
+
@connection.update(sql, should_retry: true, log_prefix: LOG_PREFIX)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def bottom
|
|
22
|
+
@lowest
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def top
|
|
26
|
+
@highest
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def expected_rows
|
|
30
|
+
top - bottom + 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def sql
|
|
36
|
+
"insert ignore into `#{ @migration.destination_name }` (#{ @migration.destination_columns }) " \
|
|
37
|
+
"select #{ @migration.origin_columns } from `#{ @migration.origin_name }` " \
|
|
38
|
+
"#{ conditions } `#{ @migration.origin_name }`.`id` between #{ @lowest } and #{ @highest }"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# XXX this is extremely brittle and doesn't work when filter contains more
|
|
42
|
+
# than one SQL clause, e.g. "where ... group by foo". Before making any
|
|
43
|
+
# more changes here, please consider either:
|
|
44
|
+
#
|
|
45
|
+
# 1. Letting users only specify part of defined clauses (i.e. don't allow
|
|
46
|
+
# `filter` on Migrator to accept both WHERE and INNER JOIN
|
|
47
|
+
# 2. Changing query building so that it uses structured data rather than
|
|
48
|
+
# strings until the last possible moment.
|
|
49
|
+
def conditions
|
|
50
|
+
if @migration.conditions
|
|
51
|
+
@migration.conditions.
|
|
52
|
+
# strip ending paren
|
|
53
|
+
sub(/\)\Z/, '').
|
|
54
|
+
# put any where conditions in parens
|
|
55
|
+
sub(/where\s(\w.*)\Z/, 'where (\\1)') + ' and'
|
|
56
|
+
else
|
|
57
|
+
'where'
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/lhm/chunker.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
require 'lhm/command'
|
|
4
|
+
require 'lhm/sql_helper'
|
|
5
|
+
require 'lhm/printer'
|
|
6
|
+
require 'lhm/chunk_insert'
|
|
7
|
+
require 'lhm/chunk_finder'
|
|
8
|
+
|
|
9
|
+
module Lhm
|
|
10
|
+
class Chunker
|
|
11
|
+
include Command
|
|
12
|
+
include SqlHelper
|
|
13
|
+
|
|
14
|
+
attr_reader :connection
|
|
15
|
+
|
|
16
|
+
LOG_PREFIX = "Chunker"
|
|
17
|
+
|
|
18
|
+
# Copy from origin to destination in chunks of size `stride`.
|
|
19
|
+
# Use the `throttler` class to sleep between each stride.
|
|
20
|
+
def initialize(migration, connection = nil, options = {})
|
|
21
|
+
@migration = migration
|
|
22
|
+
@connection = connection
|
|
23
|
+
@chunk_finder = options.fetch(:chuck_finder, ChunkFinder).new(migration, connection, options)
|
|
24
|
+
@options = options
|
|
25
|
+
@raise_on_warnings = options.fetch(:raise_on_warnings, false)
|
|
26
|
+
@verifier = options[:verifier]
|
|
27
|
+
if @throttler = options[:throttler]
|
|
28
|
+
@throttler.connection = @connection if @throttler.respond_to?(:connection=)
|
|
29
|
+
end
|
|
30
|
+
@printer = options[:printer] || Printer::Percentage.new
|
|
31
|
+
@retry_options = options[:retriable] || {}
|
|
32
|
+
@retry_helper = SqlRetry.new(
|
|
33
|
+
@connection,
|
|
34
|
+
retry_options: @retry_options
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def execute
|
|
39
|
+
@start_time = Time.now
|
|
40
|
+
|
|
41
|
+
return if @chunk_finder.table_empty?
|
|
42
|
+
@chunk_finder.each_chunk do |chunk|
|
|
43
|
+
verify_can_run
|
|
44
|
+
|
|
45
|
+
affected_rows = chunk.insert_and_return_count_of_rows_created
|
|
46
|
+
|
|
47
|
+
# Only log the chunker progress every 5 minutes instead of every iteration
|
|
48
|
+
current_time = Time.now
|
|
49
|
+
if current_time - @start_time > (5 * 60)
|
|
50
|
+
Lhm.logger.info("Inserted #{affected_rows} rows into the destination table from #{chunk.bottom} to #{chunk.top}")
|
|
51
|
+
@start_time = current_time
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if affected_rows < chunk.expected_rows
|
|
55
|
+
raise_on_non_pk_duplicate_warning
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if @throttler && affected_rows > 0
|
|
59
|
+
@throttler.run
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@printer.notify(@chunk_finder.processed_rows, @chunk_finder.max_rows)
|
|
63
|
+
end
|
|
64
|
+
@printer.end
|
|
65
|
+
rescue => e
|
|
66
|
+
@printer.exception(e) if @printer.respond_to?(:exception)
|
|
67
|
+
raise
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def raise_on_non_pk_duplicate_warning
|
|
73
|
+
@connection.execute("show warnings", should_retry: true, log_prefix: LOG_PREFIX).each do |level, code, message|
|
|
74
|
+
unless message.match?(/Duplicate entry .+ for key 'PRIMARY'/)
|
|
75
|
+
m = "Unexpected warning found for inserted row: #{message}"
|
|
76
|
+
Lhm.logger.warn(m)
|
|
77
|
+
raise Error.new(m) if @raise_on_warnings
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def verify_can_run
|
|
83
|
+
return unless @verifier
|
|
84
|
+
@retry_helper.with_retries(log_prefix: LOG_PREFIX) do |retriable_connection|
|
|
85
|
+
raise "Verification failed, aborting early" if !@verifier.call(retriable_connection)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate
|
|
90
|
+
return if @chunk_finder.table_empty?
|
|
91
|
+
@chunk_finder.validate
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require 'lhm/timestamp'
|
|
2
|
+
require 'lhm/sql_retry'
|
|
3
|
+
|
|
4
|
+
module Lhm
|
|
5
|
+
module Cleanup
|
|
6
|
+
class Current
|
|
7
|
+
|
|
8
|
+
LOG_PREFIX = "Current"
|
|
9
|
+
|
|
10
|
+
def initialize(run, origin_table_name, connection, options={})
|
|
11
|
+
@run = run
|
|
12
|
+
@table_name = TableName.new(origin_table_name)
|
|
13
|
+
@connection = connection
|
|
14
|
+
@ddls = []
|
|
15
|
+
@retry_config = options[:retriable] || {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :run, :connection, :ddls
|
|
19
|
+
|
|
20
|
+
def execute
|
|
21
|
+
build_statements_for_drop_lhm_triggers_for_origin
|
|
22
|
+
build_statements_for_rename_lhmn_tables_for_origin
|
|
23
|
+
if run
|
|
24
|
+
execute_ddls
|
|
25
|
+
else
|
|
26
|
+
report_ddls
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def build_statements_for_drop_lhm_triggers_for_origin
|
|
33
|
+
lhm_triggers_for_origin.each do |trigger|
|
|
34
|
+
@ddls << "drop trigger if exists #{trigger}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def lhm_triggers_for_origin
|
|
39
|
+
@lhm_triggers_for_origin ||= all_triggers_for_origin.select { |name| name =~ /^lhmt/ }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def all_triggers_for_origin
|
|
43
|
+
@all_triggers_for_origin ||= connection.select_values("show triggers like '%#{@table_name.original}'").collect do |trigger|
|
|
44
|
+
trigger.respond_to?(:trigger) ? trigger.trigger : trigger
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_statements_for_rename_lhmn_tables_for_origin
|
|
49
|
+
lhmn_tables_for_origin.each do |table|
|
|
50
|
+
@ddls << "rename table #{table} to #{@table_name.failed}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def lhmn_tables_for_origin
|
|
55
|
+
@lhmn_tables_for_origin ||= connection.select_values("show tables like '#{@table_name.new}'")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def execute_ddls
|
|
59
|
+
ddls.each do |ddl|
|
|
60
|
+
@connection.execute(ddl, should_retry: true, log_prefix: LOG_PREFIX)
|
|
61
|
+
end
|
|
62
|
+
Lhm.logger.info("Dropped triggers on #{@lhm_triggers_for_origin.join(', ')}")
|
|
63
|
+
Lhm.logger.info("Dropped tables #{@lhm_triggers_for_origin.join(', ')}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def report_ddls
|
|
67
|
+
Lhm.logger.info("The following DDLs would be executed: #{ddls}")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/lhm/command.rb
ADDED
|
@@ -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,108 @@
|
|
|
1
|
+
require 'delegate'
|
|
2
|
+
require 'forwardable'
|
|
3
|
+
require 'lhm/sql_retry'
|
|
4
|
+
|
|
5
|
+
module Lhm
|
|
6
|
+
# Lhm::Connection inherits from SingleDelegator. It will forward any unknown method calls to the ActiveRecord
|
|
7
|
+
# connection.
|
|
8
|
+
class Connection < SimpleDelegator
|
|
9
|
+
extend Forwardable
|
|
10
|
+
|
|
11
|
+
# Will delegate the following function to @sql_retry object, while leaving them accessible from the Lhm::Connection
|
|
12
|
+
# object
|
|
13
|
+
def_delegators :@sql_retry, :reconnect_with_consistent_host, :reconnect_with_consistent_host=, :retry_config=
|
|
14
|
+
|
|
15
|
+
alias ar_connection __getobj__
|
|
16
|
+
|
|
17
|
+
def initialize(connection:, options: {})
|
|
18
|
+
@sql_retry = Lhm::SqlRetry.new(
|
|
19
|
+
connection,
|
|
20
|
+
retry_options: options[:retriable] || {},
|
|
21
|
+
reconnect_with_consistent_host: options[:reconnect_with_consistent_host] || false
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Creates delegation for the ActiveRecord Connection
|
|
25
|
+
super(connection)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ar_connection=(connection)
|
|
29
|
+
raise Lhm::Error.new("Lhm::Connection requires an active record connection to operate") if connection.nil?
|
|
30
|
+
|
|
31
|
+
@sql_retry.connection = connection
|
|
32
|
+
# Sets connection as the delegated object
|
|
33
|
+
__setobj__(connection)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ActiveRecord::Base overridden methods to incorporate custom retry logic
|
|
37
|
+
# All other methods will be delegated
|
|
38
|
+
def execute(query, should_retry: false, log_prefix: nil)
|
|
39
|
+
if should_retry
|
|
40
|
+
exec_with_retries(:execute, query, log_prefix)
|
|
41
|
+
else
|
|
42
|
+
exec(:execute, query)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def update(query, should_retry: false, log_prefix: nil)
|
|
47
|
+
if should_retry
|
|
48
|
+
exec_with_retries(:update, query, log_prefix)
|
|
49
|
+
else
|
|
50
|
+
exec(:update, query)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def select_value(query, should_retry: false, log_prefix: nil)
|
|
55
|
+
if should_retry
|
|
56
|
+
exec_with_retries(:select_value, query, log_prefix)
|
|
57
|
+
else
|
|
58
|
+
exec(:select_value, query)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def select_values(query, should_retry: false, log_prefix: nil)
|
|
63
|
+
if should_retry
|
|
64
|
+
exec_with_retries(:select_values, query, log_prefix)
|
|
65
|
+
else
|
|
66
|
+
exec(:select_values, query)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def select_one(query, should_retry: false, log_prefix: nil)
|
|
71
|
+
if should_retry
|
|
72
|
+
exec_with_retries(:select_one, query, log_prefix)
|
|
73
|
+
else
|
|
74
|
+
exec(:select_one, query)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def exec(method, sql)
|
|
81
|
+
ar_connection.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def exec_with_retries(method, sql, log_prefix=nil)
|
|
85
|
+
effective_log_prefix = log_prefix || file
|
|
86
|
+
@sql_retry.with_retries(log_prefix: effective_log_prefix) do |conn|
|
|
87
|
+
conn.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns camelized file name of caller (e.g. chunk_insert.rb -> ChunkInsert)
|
|
92
|
+
def file
|
|
93
|
+
# Find calling file and extract name
|
|
94
|
+
/[\/]*(\w+).rb:\d+:in/.match(relevant_caller)
|
|
95
|
+
name = $1&.camelize || "Connection"
|
|
96
|
+
"#{name}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def relevant_caller
|
|
100
|
+
lhm_stack = caller.select { |x| x.include?("/lhm") }
|
|
101
|
+
first_candidate_index = lhm_stack.find_index { |line| !line.include?(__FILE__) }
|
|
102
|
+
|
|
103
|
+
# Find the file that called the `#execute` (fallbacks to current file)
|
|
104
|
+
return lhm_stack.first unless first_candidate_index
|
|
105
|
+
lhm_stack.at(first_candidate_index)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|