lhm-shopify 3.4.2 → 3.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +8 -12
- data/.gitignore +1 -4
- data/README.md +3 -5
- data/Rakefile +10 -0
- data/dev.yml +11 -4
- data/docker-compose.yml +46 -0
- data/lib/lhm/atomic_switcher.rb +4 -11
- data/lib/lhm/chunk_insert.rb +2 -9
- data/lib/lhm/chunker.rb +6 -7
- data/lib/lhm/cleanup/current.rb +3 -10
- data/lib/lhm/connection.rb +61 -0
- data/lib/lhm/entangler.rb +4 -12
- data/lib/lhm/invoker.rb +1 -1
- data/lib/lhm/sql_retry.rb +12 -7
- data/lib/lhm/version.rb +1 -1
- data/lib/lhm.rb +2 -1
- 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 +3 -0
- data/scripts/proxysql/proxysql.cnf +117 -0
- data/spec/integration/atomic_switcher_spec.rb +16 -8
- data/spec/integration/database.yml +15 -0
- data/spec/integration/integration_helper.rb +3 -6
- data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +7 -4
- data/spec/unit/chunker_spec.rb +43 -42
- data/spec/unit/connection_spec.rb +68 -0
- data/spec/unit/entangler_spec.rb +19 -8
- metadata +11 -5
- data/bin/.gitkeep +0 -0
- data/dbdeployer/config.json +0 -32
- data/dbdeployer/install.sh +0 -64
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6028b7c6d8d1f2e2d8b786ae3e8f41c4525c7507529085c3427a8dc9cf11bd2e
|
4
|
+
data.tar.gz: de625eda2736c56cfa813ba73f8447f6e798c30080274d91164b895879a7f3a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6561aadaa26f6f28f4fd04987a7fca180e9130194cd756cc5a53a654e060b7bd93efaec78a6b067d39e2b31fd6b4f1826a4de9ac90da162af912b1dd101b8732
|
7
|
+
data.tar.gz: b221afa7215cf36f66fdd48ffd449a2d6034c8710c36d9ccdbf909b2078b83fc610fa1c1170214752371e50f41ed6504186975c80e5a8164ef6b25dce631fe89
|
data/.github/workflows/test.yml
CHANGED
@@ -17,18 +17,14 @@ jobs:
|
|
17
17
|
with:
|
18
18
|
ruby-version: 2.5.1
|
19
19
|
bundler-cache: true
|
20
|
-
- name: Cache MySQL
|
21
|
-
uses: actions/cache@v2
|
22
|
-
with:
|
23
|
-
path: dbdeployer/binaries
|
24
|
-
key: ${{ runner.os }}-dbdeployer-${{ hashFiles('**/dbdeployer/install.sh') }}
|
25
20
|
- name: Install Ruby packages
|
26
21
|
run: bundle install
|
27
22
|
- name: Install Ubuntu packages
|
28
|
-
run: sudo apt-get install numactl libaio-dev
|
29
|
-
- name: Setup MySQL
|
30
|
-
run:
|
31
|
-
- name:
|
32
|
-
run:
|
33
|
-
|
34
|
-
|
23
|
+
run: sudo apt-get update && sudo apt-get install numactl libaio-dev libmysqlclient-dev
|
24
|
+
- name: Setup MySQL and ProxySQL (docker-compose)
|
25
|
+
run: docker-compose up -d # Might have to change to docker compose up -d (i.e. Compose V2) when the Ubuntu image changes the docker-compose version
|
26
|
+
- name: Wait until DBs are alive
|
27
|
+
run: ./scripts/helpers/wait-for-dbs.sh
|
28
|
+
timeout-minutes: 2
|
29
|
+
- name: Run tests
|
30
|
+
run: bundle exec rake specs
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -259,11 +259,9 @@ COV=1 bundle exec rake unit && bundle exec rake integration
|
|
259
259
|
open coverage/index.html
|
260
260
|
```
|
261
261
|
|
262
|
-
###
|
263
|
-
|
264
|
-
|
265
|
-
[`dbdeployer`](https://github.com/datacharmer/dbdeployer) to manage the test nodes
|
266
|
-
required to set up these configurations.
|
262
|
+
### Docker Compose
|
263
|
+
The integration tests rely on a replication configuration for MySQL which is being proxied by an instance of ProxySQL.
|
264
|
+
It is important that every container is running to execute the integration test suite.
|
267
265
|
|
268
266
|
## License
|
269
267
|
|
data/Rakefile
CHANGED
@@ -16,6 +16,16 @@ Rake::TestTask.new('integration') do |t|
|
|
16
16
|
t.libs << 'spec'
|
17
17
|
t.test_files = FileList['spec/integration/**/*_spec.rb']
|
18
18
|
t.verbose = true
|
19
|
+
end
|
20
|
+
|
21
|
+
Rake::TestTask.new('dev') do |t|
|
22
|
+
t.libs << 'lib'
|
23
|
+
t.libs << 'spec'
|
24
|
+
t.test_files = FileList[
|
25
|
+
'spec/test_helper.rb',
|
26
|
+
# Add file to test individually
|
27
|
+
]
|
28
|
+
t.verbose = true
|
19
29
|
end
|
20
30
|
|
21
31
|
task :specs => [:unit, :integration]
|
data/dev.yml
CHANGED
@@ -7,14 +7,21 @@ up:
|
|
7
7
|
- wget
|
8
8
|
- ruby: 2.6.5
|
9
9
|
- bundler
|
10
|
+
- docker
|
10
11
|
- custom:
|
11
|
-
name:
|
12
|
-
met?:
|
13
|
-
meet:
|
14
|
-
|
12
|
+
name: Docker Compose
|
13
|
+
met?: docker compose ls | grep -ioE -q "lhm.*running\(3\)"
|
14
|
+
meet: docker compose up -d
|
15
|
+
- custom:
|
16
|
+
name: Waiting for DBs to be operational
|
17
|
+
met?: ./scripts/helpers/wait-for-dbs.sh
|
18
|
+
meet: ":"
|
15
19
|
|
16
20
|
commands:
|
17
21
|
unit: bundle exec rake unit
|
18
22
|
int: bundle exec rake integration
|
19
23
|
test: bundle exec rake unit && bundle exec rake integration
|
20
24
|
cov: rm -rf coverage; COV=1 bundle exec rake unit && bundle exec rake integration; open coverage/index.html
|
25
|
+
logs:
|
26
|
+
desc: "See the DB logs (ctrl-c + ctrl-c to exit)"
|
27
|
+
run: docker-compose logs -f
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
services:
|
2
|
+
# Writer
|
3
|
+
mysql-1:
|
4
|
+
container_name: mysql-1
|
5
|
+
image: percona:5.7
|
6
|
+
command:
|
7
|
+
--server-id=1
|
8
|
+
--log-slave-updates=ON
|
9
|
+
--gtid-mode=ON
|
10
|
+
--enforce-gtid-consistency=ON
|
11
|
+
--read-only=OFF
|
12
|
+
hostname: 'mysql-1'
|
13
|
+
volumes:
|
14
|
+
- ./scripts/mysql/writer:/docker-entrypoint-initdb.d
|
15
|
+
environment:
|
16
|
+
MYSQL_ROOT_PASSWORD: password
|
17
|
+
MYSQL_HOST: mysql-1
|
18
|
+
ports:
|
19
|
+
- "33006:3306"
|
20
|
+
# Reader
|
21
|
+
mysql-2:
|
22
|
+
container_name: mysql-2
|
23
|
+
image: percona:5.7
|
24
|
+
command:
|
25
|
+
--server-id=2
|
26
|
+
--log-slave-updates=ON
|
27
|
+
--gtid-mode=ON
|
28
|
+
--enforce-gtid-consistency=ON
|
29
|
+
--read-only=ON
|
30
|
+
hostname: 'mysql-2'
|
31
|
+
volumes:
|
32
|
+
- ./scripts/mysql/reader:/docker-entrypoint-initdb.d
|
33
|
+
environment:
|
34
|
+
MYSQL_ROOT_PASSWORD: password
|
35
|
+
MYSQL_HOST: mysql-2
|
36
|
+
ports:
|
37
|
+
- "33007:3306"
|
38
|
+
# Proxysql
|
39
|
+
proxysql:
|
40
|
+
container_name: proxysql
|
41
|
+
image: proxysql/proxysql:2.0.11
|
42
|
+
volumes:
|
43
|
+
- ./scripts/proxysql/proxysql.cnf:/etc/proxysql.cnf
|
44
|
+
command: "proxysql -c /etc/proxysql.cnf -f --idle-threads"
|
45
|
+
ports:
|
46
|
+
- "33005:3306"
|
data/lib/lhm/atomic_switcher.rb
CHANGED
@@ -16,17 +16,12 @@ module Lhm
|
|
16
16
|
|
17
17
|
attr_reader :connection
|
18
18
|
|
19
|
-
def initialize(migration, connection = nil, options
|
19
|
+
def initialize(migration, connection = nil, options={})
|
20
20
|
@migration = migration
|
21
21
|
@connection = connection
|
22
22
|
@origin = migration.origin
|
23
23
|
@destination = migration.destination
|
24
|
-
@
|
25
|
-
@connection,
|
26
|
-
{
|
27
|
-
log_prefix: "AtomicSwitcher"
|
28
|
-
}.merge!(options.fetch(:retriable, {}))
|
29
|
-
)
|
24
|
+
@retry_options = options[:retriable] || {}
|
30
25
|
end
|
31
26
|
|
32
27
|
def atomic_switch
|
@@ -36,7 +31,7 @@ module Lhm
|
|
36
31
|
|
37
32
|
def validate
|
38
33
|
unless @connection.data_source_exists?(@origin.name) &&
|
39
|
-
|
34
|
+
@connection.data_source_exists?(@destination.name)
|
40
35
|
error "`#{ @origin.name }` and `#{ @destination.name }` must exist"
|
41
36
|
end
|
42
37
|
end
|
@@ -44,9 +39,7 @@ module Lhm
|
|
44
39
|
private
|
45
40
|
|
46
41
|
def execute
|
47
|
-
@
|
48
|
-
retriable_connection.execute atomic_switch
|
49
|
-
end
|
42
|
+
@connection.execute(atomic_switch, @retry_options)
|
50
43
|
end
|
51
44
|
end
|
52
45
|
end
|
data/lib/lhm/chunk_insert.rb
CHANGED
@@ -7,18 +7,11 @@ module Lhm
|
|
7
7
|
@connection = connection
|
8
8
|
@lowest = lowest
|
9
9
|
@highest = highest
|
10
|
-
@
|
11
|
-
@connection,
|
12
|
-
{
|
13
|
-
log_prefix: "Chunker Insert"
|
14
|
-
}.merge!(options.fetch(:retriable, {}))
|
15
|
-
)
|
10
|
+
@retry_options = options[:retriable] || {}
|
16
11
|
end
|
17
12
|
|
18
13
|
def insert_and_return_count_of_rows_created
|
19
|
-
@
|
20
|
-
retriable_connection.update sql
|
21
|
-
end
|
14
|
+
@connection.update(sql, @retry_options)
|
22
15
|
end
|
23
16
|
|
24
17
|
def sql
|
data/lib/lhm/chunker.rb
CHANGED
@@ -28,11 +28,12 @@ module Lhm
|
|
28
28
|
@start = @chunk_finder.start
|
29
29
|
@limit = @chunk_finder.limit
|
30
30
|
@printer = options[:printer] || Printer::Percentage.new
|
31
|
+
@retry_options = options[:retriable] || {}
|
31
32
|
@retry_helper = SqlRetry.new(
|
32
33
|
@connection,
|
33
34
|
{
|
34
35
|
log_prefix: "Chunker"
|
35
|
-
}.merge!(
|
36
|
+
}.merge!(@retry_options)
|
36
37
|
)
|
37
38
|
end
|
38
39
|
|
@@ -46,7 +47,7 @@ module Lhm
|
|
46
47
|
top = upper_id(@next_to_insert, stride)
|
47
48
|
verify_can_run
|
48
49
|
|
49
|
-
affected_rows = ChunkInsert.new(@migration, @connection, bottom, top, @
|
50
|
+
affected_rows = ChunkInsert.new(@migration, @connection, bottom, top, @retry_options).insert_and_return_count_of_rows_created
|
50
51
|
expected_rows = top - bottom + 1
|
51
52
|
|
52
53
|
# Only log the chunker progress every 5 minutes instead of every iteration
|
@@ -78,7 +79,7 @@ module Lhm
|
|
78
79
|
private
|
79
80
|
|
80
81
|
def raise_on_non_pk_duplicate_warning
|
81
|
-
@connection.
|
82
|
+
@connection.execute("show warnings", @retry_options).each do |level, code, message|
|
82
83
|
unless message.match?(/Duplicate entry .+ for key 'PRIMARY'/)
|
83
84
|
m = "Unexpected warning found for inserted row: #{message}"
|
84
85
|
Lhm.logger.warn(m)
|
@@ -93,16 +94,14 @@ module Lhm
|
|
93
94
|
|
94
95
|
def verify_can_run
|
95
96
|
return unless @verifier
|
96
|
-
@retry_helper.with_retries do |retriable_connection|
|
97
|
+
@retry_helper.with_retries(@retry_options) do |retriable_connection|
|
97
98
|
raise "Verification failed, aborting early" if !@verifier.call(retriable_connection)
|
98
99
|
end
|
99
100
|
end
|
100
101
|
|
101
102
|
def upper_id(next_id, stride)
|
102
103
|
sql = "select id from `#{ @migration.origin_name }` where id >= #{ next_id } order by id limit 1 offset #{ stride - 1}"
|
103
|
-
top = @
|
104
|
-
retriable_connection.select_value(sql)
|
105
|
-
end
|
104
|
+
top = @connection.select_value(sql, @retry_options)
|
106
105
|
|
107
106
|
[top ? top.to_i : @limit, @limit].min
|
108
107
|
end
|
data/lib/lhm/cleanup/current.rb
CHANGED
@@ -4,17 +4,12 @@ require 'lhm/sql_retry'
|
|
4
4
|
module Lhm
|
5
5
|
module Cleanup
|
6
6
|
class Current
|
7
|
-
def initialize(run, origin_table_name, connection, options
|
7
|
+
def initialize(run, origin_table_name, connection, options={})
|
8
8
|
@run = run
|
9
9
|
@table_name = TableName.new(origin_table_name)
|
10
10
|
@connection = connection
|
11
11
|
@ddls = []
|
12
|
-
@
|
13
|
-
@connection,
|
14
|
-
{
|
15
|
-
log_prefix: "Cleanup::Current"
|
16
|
-
}.merge!(options.fetch(:retriable, {}))
|
17
|
-
)
|
12
|
+
@retry_config = options[:retriable] || {}
|
18
13
|
end
|
19
14
|
|
20
15
|
attr_reader :run, :connection, :ddls
|
@@ -59,9 +54,7 @@ module Lhm
|
|
59
54
|
|
60
55
|
def execute_ddls
|
61
56
|
ddls.each do |ddl|
|
62
|
-
|
63
|
-
retriable_connection.execute(ddl)
|
64
|
-
end
|
57
|
+
@connection.execute(ddl, @retry_config)
|
65
58
|
end
|
66
59
|
Lhm.logger.info("Dropped triggers on #{@lhm_triggers_for_origin.join(', ')}")
|
67
60
|
Lhm.logger.info("Dropped tables #{@lhm_triggers_for_origin.join(', ')}")
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Lhm
|
4
|
+
class Connection < SimpleDelegator
|
5
|
+
|
6
|
+
# Lhm::Connection inherits from SingleDelegator. It will forward any unknown method calls to the ActiveRecord
|
7
|
+
# connection.
|
8
|
+
alias connection __getobj__
|
9
|
+
alias connection= __setobj__
|
10
|
+
|
11
|
+
def initialize(connection:, default_log_prefix: nil, retry_options: {})
|
12
|
+
@default_log_prefix = default_log_prefix
|
13
|
+
@retry_options = retry_options || default_retry_config
|
14
|
+
@sql_retry = Lhm::SqlRetry.new(
|
15
|
+
connection,
|
16
|
+
retry_options,
|
17
|
+
)
|
18
|
+
|
19
|
+
# Creates delegation for the ActiveRecord Connection
|
20
|
+
super(connection)
|
21
|
+
end
|
22
|
+
|
23
|
+
def execute(query, retry_options = {})
|
24
|
+
exec_with_retries(:execute, query, retry_options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def update(query, retry_options = {})
|
28
|
+
exec_with_retries(:update, query, retry_options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def select_value(query, retry_options = {})
|
32
|
+
exec_with_retries(:select_value, query, retry_options)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def exec_with_retries(method, sql, retry_options = {})
|
38
|
+
retry_options[:log_prefix] ||= file
|
39
|
+
@sql_retry.with_retries(retry_options) do |conn|
|
40
|
+
conn.public_send(method, sql)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns camelized file name of caller (e.g. chunk_insert.rb -> ChunkInsert)
|
45
|
+
def file
|
46
|
+
# Find calling file and extract name
|
47
|
+
/[\/]*(\w+).rb:\d+:in/.match(relevant_caller)
|
48
|
+
name = $1&.camelize || "Connection"
|
49
|
+
"#{name}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def relevant_caller
|
53
|
+
lhm_stack = caller.select { |x| x.include?("/lhm") }
|
54
|
+
first_candidate_index = lhm_stack.find_index {|line| !line.include?(__FILE__)}
|
55
|
+
|
56
|
+
# Find the file that called the `#execute` (fallbacks to current file)
|
57
|
+
return lhm_stack.first unless first_candidate_index
|
58
|
+
lhm_stack.at(first_candidate_index)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/lhm/entangler.rb
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
require 'lhm/command'
|
5
5
|
require 'lhm/sql_helper'
|
6
6
|
require 'lhm/sql_retry'
|
7
|
+
require 'lhm/connection'
|
7
8
|
|
8
9
|
module Lhm
|
9
10
|
class Entangler
|
@@ -19,12 +20,7 @@ module Lhm
|
|
19
20
|
@origin = migration.origin
|
20
21
|
@destination = migration.destination
|
21
22
|
@connection = connection
|
22
|
-
@
|
23
|
-
@connection,
|
24
|
-
{
|
25
|
-
log_prefix: "Entangler"
|
26
|
-
}.merge!(options.fetch(:retriable, {}))
|
27
|
-
)
|
23
|
+
@retry_options = options[:retriable] || {}
|
28
24
|
end
|
29
25
|
|
30
26
|
def entangle
|
@@ -90,18 +86,14 @@ module Lhm
|
|
90
86
|
|
91
87
|
def before
|
92
88
|
entangle.each do |stmt|
|
93
|
-
@
|
94
|
-
retriable_connection.execute(stmt)
|
95
|
-
end
|
89
|
+
@connection.execute(stmt, @retry_options)
|
96
90
|
end
|
97
91
|
Lhm.logger.info("Created triggers on #{@origin.name}")
|
98
92
|
end
|
99
93
|
|
100
94
|
def after
|
101
95
|
untangle.each do |stmt|
|
102
|
-
@
|
103
|
-
retriable_connection.execute(stmt)
|
104
|
-
end
|
96
|
+
@connection.execute(stmt, @retry_options)
|
105
97
|
end
|
106
98
|
Lhm.logger.info("Dropped triggers on #{@origin.name}")
|
107
99
|
end
|
data/lib/lhm/invoker.rb
CHANGED
@@ -56,7 +56,7 @@ module Lhm
|
|
56
56
|
Chunker.new(migration, @connection, options).run
|
57
57
|
raise "Required triggers do not exist" unless triggers_still_exist?(@connection, entangler)
|
58
58
|
if options[:atomic_switch]
|
59
|
-
AtomicSwitcher.new(migration, @connection
|
59
|
+
AtomicSwitcher.new(migration, @connection).run
|
60
60
|
else
|
61
61
|
LockedSwitcher.new(migration, @connection).run
|
62
62
|
end
|
data/lib/lhm/sql_retry.rb
CHANGED
@@ -17,20 +17,26 @@ module Lhm
|
|
17
17
|
class SqlRetry
|
18
18
|
def initialize(connection, options = {})
|
19
19
|
@connection = connection
|
20
|
-
@
|
21
|
-
@retry_config = default_retry_config.dup.merge!(options)
|
20
|
+
@global_retry_config = default_retry_config.dup.merge!(options)
|
22
21
|
end
|
23
22
|
|
24
|
-
def with_retries
|
25
|
-
|
23
|
+
def with_retries(retry_config = {})
|
24
|
+
cnf = @global_retry_config.dup.merge!(retry_config)
|
25
|
+
@log_prefix = cnf.delete(:log_prefix) || "SQL Retry"
|
26
|
+
Retriable.retriable(cnf) do
|
26
27
|
yield(@connection)
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
30
|
-
attr_reader :
|
31
|
+
attr_reader :global_retry_config
|
31
32
|
|
32
33
|
private
|
33
34
|
|
35
|
+
def log_with_prefix(message, level = :info)
|
36
|
+
message.prepend("[#{@log_prefix}] ") if @log_prefix
|
37
|
+
Lhm.logger.send(level, message)
|
38
|
+
end
|
39
|
+
|
34
40
|
# For a full list of configuration options see https://github.com/kamui/retriable
|
35
41
|
def default_retry_config
|
36
42
|
{
|
@@ -52,8 +58,7 @@ module Lhm
|
|
52
58
|
max_elapsed_time: Float::INFINITY, # max total time in seconds that code is allowed to keep being retried
|
53
59
|
on_retry: Proc.new do |exception, try_number, total_elapsed_time, next_interval|
|
54
60
|
log = "#{exception.class}: '#{exception.message}' - #{try_number} tries in #{total_elapsed_time} seconds and #{next_interval} seconds until the next try."
|
55
|
-
log
|
56
|
-
Lhm.logger.info(log)
|
61
|
+
log_with_prefix(log, :info)
|
57
62
|
end
|
58
63
|
}.freeze
|
59
64
|
end
|
data/lib/lhm/version.rb
CHANGED
data/lib/lhm.rb
CHANGED
@@ -8,6 +8,7 @@ require 'lhm/throttler'
|
|
8
8
|
require 'lhm/version'
|
9
9
|
require 'lhm/cleanup/current'
|
10
10
|
require 'lhm/sql_retry'
|
11
|
+
require 'lhm/connection'
|
11
12
|
require 'lhm/test_support'
|
12
13
|
require 'lhm/railtie' if defined?(Rails::Railtie)
|
13
14
|
require 'logger'
|
@@ -82,7 +83,7 @@ module Lhm
|
|
82
83
|
end
|
83
84
|
|
84
85
|
def setup(connection)
|
85
|
-
@@connection = connection
|
86
|
+
@@connection = Lhm::Connection.new(connection: connection)
|
86
87
|
end
|
87
88
|
|
88
89
|
def connection
|
@@ -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 @@
|
|
1
|
+
CREATE DATABASE test;
|
@@ -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
|
+
)
|
@@ -6,6 +6,7 @@ require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
|
|
6
6
|
require 'lhm/table'
|
7
7
|
require 'lhm/migration'
|
8
8
|
require 'lhm/atomic_switcher'
|
9
|
+
require 'lhm/connection'
|
9
10
|
|
10
11
|
describe Lhm::AtomicSwitcher do
|
11
12
|
include IntegrationHelper
|
@@ -29,9 +30,11 @@ describe Lhm::AtomicSwitcher do
|
|
29
30
|
end
|
30
31
|
|
31
32
|
it 'should retry and log on lock wait timeouts' do
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
ar_connection = mock()
|
34
|
+
ar_connection.stubs(:data_source_exists?).returns(true)
|
35
|
+
ar_connection.stubs(:execute).raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.').then.returns(true)
|
36
|
+
|
37
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
35
38
|
|
36
39
|
switcher = Lhm::AtomicSwitcher.new(@migration, connection, retriable: {base_interval: 0})
|
37
40
|
|
@@ -40,13 +43,16 @@ describe Lhm::AtomicSwitcher do
|
|
40
43
|
log_messages = @logs.string.split("\n")
|
41
44
|
assert_equal(2, log_messages.length)
|
42
45
|
assert log_messages[0].include? "Starting run of class=Lhm::AtomicSwitcher"
|
46
|
+
# On failure of this assertion, check for Lhm::Connection#file
|
43
47
|
assert log_messages[1].include? "[AtomicSwitcher] ActiveRecord::StatementInvalid: 'Lock wait timeout exceeded; try restarting transaction.' - 1 tries"
|
44
48
|
end
|
45
49
|
|
46
50
|
it 'should give up on lock wait timeouts after a configured number of tries' do
|
47
|
-
|
48
|
-
|
49
|
-
|
51
|
+
ar_connection = mock()
|
52
|
+
ar_connection.stubs(:data_source_exists?).returns(true)
|
53
|
+
ar_connection.stubs(:execute).twice.raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
|
54
|
+
|
55
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
50
56
|
|
51
57
|
switcher = Lhm::AtomicSwitcher.new(@migration, connection, retriable: {tries: 2, base_interval: 0})
|
52
58
|
|
@@ -62,8 +68,10 @@ describe Lhm::AtomicSwitcher do
|
|
62
68
|
end
|
63
69
|
|
64
70
|
it "should raise when destination doesn't exist" do
|
65
|
-
|
66
|
-
|
71
|
+
ar_connection = mock()
|
72
|
+
ar_connection.stubs(:data_source_exists?).returns(false)
|
73
|
+
|
74
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
67
75
|
|
68
76
|
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
69
77
|
|
@@ -41,7 +41,6 @@ module IntegrationHelper
|
|
41
41
|
$db_config['master']['port'],
|
42
42
|
$db_config['master']['user'],
|
43
43
|
$db_config['master']['password'],
|
44
|
-
$db_config['master']['socket']
|
45
44
|
)
|
46
45
|
end
|
47
46
|
|
@@ -51,12 +50,11 @@ module IntegrationHelper
|
|
51
50
|
$db_config['slave']['port'],
|
52
51
|
$db_config['slave']['user'],
|
53
52
|
$db_config['slave']['password'],
|
54
|
-
$db_config['slave']['socket']
|
55
53
|
)
|
56
54
|
end
|
57
55
|
|
58
|
-
def connect!(hostname, port, user, password
|
59
|
-
adapter = ar_conn(hostname, port, user, password
|
56
|
+
def connect!(hostname, port, user, password)
|
57
|
+
adapter = Lhm::Connection.new(connection: ar_conn(hostname, port, user, password))
|
60
58
|
Lhm.setup(adapter)
|
61
59
|
unless defined?(@@cleaned_up)
|
62
60
|
Lhm.cleanup(true)
|
@@ -65,14 +63,13 @@ module IntegrationHelper
|
|
65
63
|
@connection = adapter
|
66
64
|
end
|
67
65
|
|
68
|
-
def ar_conn(host, port, user, password
|
66
|
+
def ar_conn(host, port, user, password)
|
69
67
|
ActiveRecord::Base.establish_connection(
|
70
68
|
:adapter => 'mysql2',
|
71
69
|
:host => host,
|
72
70
|
:username => user,
|
73
71
|
:port => port,
|
74
72
|
:password => password,
|
75
|
-
:socket => socket,
|
76
73
|
:database => $db_name
|
77
74
|
)
|
78
75
|
ActiveRecord::Base.connection
|
@@ -77,14 +77,17 @@ class LockWaitTimeoutTestHelper
|
|
77
77
|
attr_reader :main_conn, :lock_duration, :innodb_lock_wait_timeout
|
78
78
|
|
79
79
|
def new_mysql_connection
|
80
|
-
Mysql2::Client.new(
|
80
|
+
client = Mysql2::Client.new(
|
81
81
|
host: '127.0.0.1',
|
82
|
-
database: test_db_name,
|
83
82
|
username: db_config['master']['user'],
|
84
83
|
password: db_config['master']['password'],
|
85
|
-
port: db_config['master']['port']
|
86
|
-
socket: db_config['master']['socket']
|
84
|
+
port: db_config['master']['port']
|
87
85
|
)
|
86
|
+
|
87
|
+
# For some reasons sometimes the database does not exist
|
88
|
+
client.query("CREATE DATABASE IF NOT EXISTS #{test_db_name}")
|
89
|
+
client.select_db(test_db_name)
|
90
|
+
client
|
88
91
|
end
|
89
92
|
|
90
93
|
def test_db_name
|
data/spec/unit/chunker_spec.rb
CHANGED
@@ -7,6 +7,7 @@ require 'lhm/table'
|
|
7
7
|
require 'lhm/migration'
|
8
8
|
require 'lhm/chunker'
|
9
9
|
require 'lhm/throttler'
|
10
|
+
require 'lhm/connection'
|
10
11
|
|
11
12
|
describe Lhm::Chunker do
|
12
13
|
include UnitHelper
|
@@ -37,11 +38,11 @@ describe Lhm::Chunker do
|
|
37
38
|
5
|
38
39
|
end
|
39
40
|
|
40
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 4/)).returns(7)
|
41
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 4/)).returns(21)
|
42
|
-
@connection.expects(:update).with(regexp_matches(/between 1 and 7/)).returns(2)
|
43
|
-
@connection.expects(:update).with(regexp_matches(/between 8 and 10/)).returns(2)
|
44
|
-
@connection.expects(:
|
41
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 4/),{}).returns(7)
|
42
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 4/),{}).returns(21)
|
43
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 7/),{}).returns(2)
|
44
|
+
@connection.expects(:update).with(regexp_matches(/between 8 and 10/),{}).returns(2)
|
45
|
+
@connection.expects(:execute).twice.with(regexp_matches(/show warnings/),{}).returns([])
|
45
46
|
|
46
47
|
@chunker.run
|
47
48
|
end
|
@@ -52,17 +53,17 @@ describe Lhm::Chunker do
|
|
52
53
|
2
|
53
54
|
end
|
54
55
|
|
55
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/)).returns(2)
|
56
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 1/)).returns(4)
|
57
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 5 order by id limit 1 offset 1/)).returns(6)
|
58
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 7 order by id limit 1 offset 1/)).returns(8)
|
59
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 1/)).returns(10)
|
56
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),{}).returns(2)
|
57
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 1/),{}).returns(4)
|
58
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 5 order by id limit 1 offset 1/),{}).returns(6)
|
59
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 7 order by id limit 1 offset 1/),{}).returns(8)
|
60
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 1/),{}).returns(10)
|
60
61
|
|
61
|
-
@connection.expects(:update).with(regexp_matches(/between 1 and 2/)).returns(2)
|
62
|
-
@connection.expects(:update).with(regexp_matches(/between 3 and 4/)).returns(2)
|
63
|
-
@connection.expects(:update).with(regexp_matches(/between 5 and 6/)).returns(2)
|
64
|
-
@connection.expects(:update).with(regexp_matches(/between 7 and 8/)).returns(2)
|
65
|
-
@connection.expects(:update).with(regexp_matches(/between 9 and 10/)).returns(2)
|
62
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 2/),{}).returns(2)
|
63
|
+
@connection.expects(:update).with(regexp_matches(/between 3 and 4/),{}).returns(2)
|
64
|
+
@connection.expects(:update).with(regexp_matches(/between 5 and 6/),{}).returns(2)
|
65
|
+
@connection.expects(:update).with(regexp_matches(/between 7 and 8/),{}).returns(2)
|
66
|
+
@connection.expects(:update).with(regexp_matches(/between 9 and 10/),{}).returns(2)
|
66
67
|
|
67
68
|
@chunker.run
|
68
69
|
end
|
@@ -79,17 +80,17 @@ describe Lhm::Chunker do
|
|
79
80
|
end
|
80
81
|
end
|
81
82
|
|
82
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/)).returns(2)
|
83
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 2/)).returns(5)
|
84
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 2/)).returns(8)
|
85
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 2/)).returns(nil)
|
83
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),{}).returns(2)
|
84
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 2/),{}).returns(5)
|
85
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 2/),{}).returns(8)
|
86
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 2/),{}).returns(nil)
|
86
87
|
|
87
|
-
@connection.expects(:update).with(regexp_matches(/between 1 and 2/)).returns(2)
|
88
|
-
@connection.expects(:update).with(regexp_matches(/between 3 and 5/)).returns(2)
|
89
|
-
@connection.expects(:update).with(regexp_matches(/between 6 and 8/)).returns(2)
|
90
|
-
@connection.expects(:update).with(regexp_matches(/between 9 and 10/)).returns(2)
|
88
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 2/),{}).returns(2)
|
89
|
+
@connection.expects(:update).with(regexp_matches(/between 3 and 5/),{}).returns(2)
|
90
|
+
@connection.expects(:update).with(regexp_matches(/between 6 and 8/),{}).returns(2)
|
91
|
+
@connection.expects(:update).with(regexp_matches(/between 9 and 10/),{}).returns(2)
|
91
92
|
|
92
|
-
@connection.expects(:
|
93
|
+
@connection.expects(:execute).twice.with(regexp_matches(/show warnings/),{}).returns([])
|
93
94
|
|
94
95
|
@chunker.run
|
95
96
|
end
|
@@ -99,8 +100,8 @@ describe Lhm::Chunker do
|
|
99
100
|
:start => 1,
|
100
101
|
:limit => 1)
|
101
102
|
|
102
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 0/)).returns(nil)
|
103
|
-
@connection.expects(:update).with(regexp_matches(/between 1 and 1/)).returns(1)
|
103
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 0/),{}).returns(nil)
|
104
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 1/),{}).returns(1)
|
104
105
|
|
105
106
|
@chunker.run
|
106
107
|
end
|
@@ -113,17 +114,17 @@ describe Lhm::Chunker do
|
|
113
114
|
2
|
114
115
|
end
|
115
116
|
|
116
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 2 order by id limit 1 offset 1/)).returns(3)
|
117
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 4 order by id limit 1 offset 1/)).returns(5)
|
118
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 1/)).returns(7)
|
119
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 1/)).returns(9)
|
120
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 10 order by id limit 1 offset 1/)).returns(nil)
|
117
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 2 order by id limit 1 offset 1/),{}).returns(3)
|
118
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 4 order by id limit 1 offset 1/),{}).returns(5)
|
119
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 1/),{}).returns(7)
|
120
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 1/),{}).returns(9)
|
121
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 10 order by id limit 1 offset 1/),{}).returns(nil)
|
121
122
|
|
122
|
-
@connection.expects(:update).with(regexp_matches(/between 2 and 3/)).returns(2)
|
123
|
-
@connection.expects(:update).with(regexp_matches(/between 4 and 5/)).returns(2)
|
124
|
-
@connection.expects(:update).with(regexp_matches(/between 6 and 7/)).returns(2)
|
125
|
-
@connection.expects(:update).with(regexp_matches(/between 8 and 9/)).returns(2)
|
126
|
-
@connection.expects(:update).with(regexp_matches(/between 10 and 10/)).returns(1)
|
123
|
+
@connection.expects(:update).with(regexp_matches(/between 2 and 3/),{}).returns(2)
|
124
|
+
@connection.expects(:update).with(regexp_matches(/between 4 and 5/),{}).returns(2)
|
125
|
+
@connection.expects(:update).with(regexp_matches(/between 6 and 7/),{}).returns(2)
|
126
|
+
@connection.expects(:update).with(regexp_matches(/between 8 and 9/),{}).returns(2)
|
127
|
+
@connection.expects(:update).with(regexp_matches(/between 10 and 10/),{}).returns(1)
|
127
128
|
|
128
129
|
@chunker.run
|
129
130
|
end
|
@@ -137,9 +138,9 @@ describe Lhm::Chunker do
|
|
137
138
|
2
|
138
139
|
end
|
139
140
|
|
140
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/)).returns(2)
|
141
|
-
@connection.expects(:update).with(regexp_matches(/where \(foo.created_at > '2013-07-10' or foo.baz = 'quux'\) and `foo`/)).returns(1)
|
142
|
-
@connection.expects(:
|
141
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),{}).returns(2)
|
142
|
+
@connection.expects(:update).with(regexp_matches(/where \(foo.created_at > '2013-07-10' or foo.baz = 'quux'\) and `foo`/),{}).returns(1)
|
143
|
+
@connection.expects(:execute).with(regexp_matches(/show warnings/),{}).returns([])
|
143
144
|
|
144
145
|
def @migration.conditions
|
145
146
|
"where foo.created_at > '2013-07-10' or foo.baz = 'quux'"
|
@@ -157,9 +158,9 @@ describe Lhm::Chunker do
|
|
157
158
|
2
|
158
159
|
end
|
159
160
|
|
160
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/)).returns(2)
|
161
|
-
@connection.expects(:update).with(regexp_matches(/inner join bar on foo.id = bar.foo_id and/)).returns(1)
|
162
|
-
@connection.expects(:
|
161
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),{}).returns(2)
|
162
|
+
@connection.expects(:update).with(regexp_matches(/inner join bar on foo.id = bar.foo_id and/),{}).returns(1)
|
163
|
+
@connection.expects(:execute).with(regexp_matches(/show warnings/),{}).returns([])
|
163
164
|
|
164
165
|
def @migration.conditions
|
165
166
|
'inner join bar on foo.id = bar.foo_id'
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'lhm/connection'
|
2
|
+
|
3
|
+
describe Lhm::Connection do
|
4
|
+
|
5
|
+
LOCK_WAIT = ActiveRecord::StatementInvalid.new('Lock wait timeout exceeded; try restarting transaction.')
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
@logs = StringIO.new
|
9
|
+
Lhm.logger = Logger.new(@logs)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "Should find use calling file as prefix" do
|
13
|
+
ar_connection = mock()
|
14
|
+
ar_connection.stubs(:execute).raises(LOCK_WAIT).then.returns(true)
|
15
|
+
|
16
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
17
|
+
|
18
|
+
connection.execute("SHOW TABLES", { base_interval: 0 })
|
19
|
+
|
20
|
+
log_messages = @logs.string.split("\n")
|
21
|
+
assert_equal(1, log_messages.length)
|
22
|
+
assert log_messages.first.include?("[ConnectionSpec]")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "#execute should be retried" do
|
26
|
+
ar_connection = mock()
|
27
|
+
ar_connection.stubs(:execute).raises(LOCK_WAIT)
|
28
|
+
.then.raises(LOCK_WAIT)
|
29
|
+
.then.returns(true)
|
30
|
+
|
31
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
32
|
+
|
33
|
+
connection.execute("SHOW TABLES", { base_interval: 0, tries: 3 })
|
34
|
+
|
35
|
+
log_messages = @logs.string.split("\n")
|
36
|
+
assert_equal(2, log_messages.length)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "#update should be retried" do
|
40
|
+
ar_connection = mock()
|
41
|
+
ar_connection.stubs(:update).raises(LOCK_WAIT)
|
42
|
+
.then.raises(LOCK_WAIT)
|
43
|
+
.then.returns(1)
|
44
|
+
|
45
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
46
|
+
|
47
|
+
val = connection.update("SHOW TABLES", { base_interval: 0, tries: 3 })
|
48
|
+
|
49
|
+
log_messages = @logs.string.split("\n")
|
50
|
+
assert_equal val, 1
|
51
|
+
assert_equal(2, log_messages.length)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "#select_value should be retried" do
|
55
|
+
ar_connection = mock()
|
56
|
+
ar_connection.stubs(:select_value).raises(LOCK_WAIT)
|
57
|
+
.then.raises(LOCK_WAIT)
|
58
|
+
.then.returns("dummy")
|
59
|
+
|
60
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
61
|
+
|
62
|
+
val = connection.select_value("SHOW TABLES", { base_interval: 0, tries: 3 })
|
63
|
+
|
64
|
+
log_messages = @logs.string.split("\n")
|
65
|
+
assert_equal val, "dummy"
|
66
|
+
assert_equal(2, log_messages.length)
|
67
|
+
end
|
68
|
+
end
|
data/spec/unit/entangler_spec.rb
CHANGED
@@ -6,6 +6,7 @@ require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
6
6
|
require 'lhm/table'
|
7
7
|
require 'lhm/migration'
|
8
8
|
require 'lhm/entangler'
|
9
|
+
require 'lhm/connection'
|
9
10
|
|
10
11
|
describe Lhm::Entangler do
|
11
12
|
include UnitHelper
|
@@ -60,33 +61,43 @@ describe Lhm::Entangler do
|
|
60
61
|
end
|
61
62
|
|
62
63
|
it 'should retry trigger creation when it hits a lock wait timeout' do
|
63
|
-
connection = mock()
|
64
64
|
tries = 1
|
65
|
+
ar_connection = mock()
|
66
|
+
ar_connection.expects(:execute).times(tries).raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
67
|
+
|
68
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
69
|
+
|
65
70
|
@entangler = Lhm::Entangler.new(@migration, connection, retriable: {base_interval: 0, tries: tries})
|
66
|
-
connection.expects(:execute).times(tries).raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
67
71
|
|
68
72
|
assert_raises(Mysql2::Error) { @entangler.before }
|
69
73
|
end
|
70
74
|
|
71
75
|
it 'should not retry trigger creation with other mysql errors' do
|
72
|
-
|
73
|
-
|
76
|
+
ar_connection = mock()
|
77
|
+
ar_connection.expects(:execute).once.raises(Mysql2::Error, 'The MySQL server is running with the --read-only option so it cannot execute this statement.')
|
78
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
74
79
|
|
75
80
|
@entangler = Lhm::Entangler.new(@migration, connection, retriable: {base_interval: 0})
|
76
81
|
assert_raises(Mysql2::Error) { @entangler.before }
|
77
82
|
end
|
78
83
|
|
79
84
|
it 'should succesfully finish after retrying' do
|
80
|
-
|
81
|
-
|
85
|
+
ar_connection = mock()
|
86
|
+
ar_connection.stubs(:execute).raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction').then.returns(true)
|
87
|
+
|
88
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
89
|
+
|
82
90
|
@entangler = Lhm::Entangler.new(@migration, connection, retriable: {base_interval: 0})
|
83
91
|
|
84
92
|
assert @entangler.before
|
85
93
|
end
|
86
94
|
|
87
95
|
it 'should retry as many times as specified by configuration' do
|
88
|
-
|
89
|
-
|
96
|
+
ar_connection = mock()
|
97
|
+
ar_connection.expects(:execute).times(5).raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
98
|
+
|
99
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
100
|
+
|
90
101
|
@entangler = Lhm::Entangler.new(@migration, connection, retriable: {tries: 5, base_interval: 0})
|
91
102
|
|
92
103
|
assert_raises(Mysql2::Error) { @entangler.before }
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lhm-shopify
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- SoundCloud
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date: 2021-
|
15
|
+
date: 2021-12-07 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: retriable
|
@@ -129,10 +129,8 @@ files:
|
|
129
129
|
- LICENSE
|
130
130
|
- README.md
|
131
131
|
- Rakefile
|
132
|
-
- bin/.gitkeep
|
133
|
-
- dbdeployer/config.json
|
134
|
-
- dbdeployer/install.sh
|
135
132
|
- dev.yml
|
133
|
+
- docker-compose.yml
|
136
134
|
- gemfiles/ar-2.3_mysql.gemfile
|
137
135
|
- gemfiles/ar-3.2_mysql.gemfile
|
138
136
|
- gemfiles/ar-3.2_mysql2.gemfile
|
@@ -149,6 +147,7 @@ files:
|
|
149
147
|
- lib/lhm/chunker.rb
|
150
148
|
- lib/lhm/cleanup/current.rb
|
151
149
|
- lib/lhm/command.rb
|
150
|
+
- lib/lhm/connection.rb
|
152
151
|
- lib/lhm/entangler.rb
|
153
152
|
- lib/lhm/intersection.rb
|
154
153
|
- lib/lhm/invoker.rb
|
@@ -168,6 +167,11 @@ files:
|
|
168
167
|
- lib/lhm/throttler/time.rb
|
169
168
|
- lib/lhm/timestamp.rb
|
170
169
|
- lib/lhm/version.rb
|
170
|
+
- scripts/helpers/wait-for-dbs.sh
|
171
|
+
- scripts/mysql/reader/create_replication.sql
|
172
|
+
- scripts/mysql/writer/create_test_db.sql
|
173
|
+
- scripts/mysql/writer/create_users.sql
|
174
|
+
- scripts/proxysql/proxysql.cnf
|
171
175
|
- shipit.rubygems.yml
|
172
176
|
- spec/.lhm.example
|
173
177
|
- spec/README.md
|
@@ -188,6 +192,7 @@ files:
|
|
188
192
|
- spec/integration/chunk_insert_spec.rb
|
189
193
|
- spec/integration/chunker_spec.rb
|
190
194
|
- spec/integration/cleanup_spec.rb
|
195
|
+
- spec/integration/database.yml
|
191
196
|
- spec/integration/entangler_spec.rb
|
192
197
|
- spec/integration/integration_helper.rb
|
193
198
|
- spec/integration/invoker_spec.rb
|
@@ -202,6 +207,7 @@ files:
|
|
202
207
|
- spec/unit/chunk_finder_spec.rb
|
203
208
|
- spec/unit/chunk_insert_spec.rb
|
204
209
|
- spec/unit/chunker_spec.rb
|
210
|
+
- spec/unit/connection_spec.rb
|
205
211
|
- spec/unit/entangler_spec.rb
|
206
212
|
- spec/unit/intersection_spec.rb
|
207
213
|
- spec/unit/lhm_spec.rb
|
data/bin/.gitkeep
DELETED
File without changes
|
data/dbdeployer/config.json
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
{
|
2
|
-
"version": "1.8.0",
|
3
|
-
"sandbox-home": "./dbdeployer/sandboxes",
|
4
|
-
"sandbox-binary": "./dbdeployer/binaries",
|
5
|
-
"use-sandbox-catalog": true,
|
6
|
-
"master-slave-base-port": 11000,
|
7
|
-
"group-replication-base-port": 12000,
|
8
|
-
"group-replication-sp-base-port": 13000,
|
9
|
-
"fan-in-replication-base-port": 14000,
|
10
|
-
"all-masters-replication-base-port": 15000,
|
11
|
-
"multiple-base-port": 16000,
|
12
|
-
"group-port-delta": 125,
|
13
|
-
"mysqlx-port-delta": 10000,
|
14
|
-
"master-name": "master",
|
15
|
-
"master-abbr": "m",
|
16
|
-
"node-prefix": "node",
|
17
|
-
"slave-prefix": "slave",
|
18
|
-
"slave-abbr": "s",
|
19
|
-
"sandbox-prefix": "msb_",
|
20
|
-
"master-slave-prefix": "rsandbox_",
|
21
|
-
"group-prefix": "group_msb_",
|
22
|
-
"group-sp-prefix": "group_sp_msb_",
|
23
|
-
"multiple-prefix": "multi_msb_",
|
24
|
-
"fan-in-prefix": "fan_in_msb_",
|
25
|
-
"all-masters-prefix": "all_masters_msb_",
|
26
|
-
"reserved-ports": [
|
27
|
-
1186,
|
28
|
-
3306,
|
29
|
-
33060
|
30
|
-
],
|
31
|
-
"timestamp": "Mon Jul 16 17:36:55 AST 2018"
|
32
|
-
}
|
data/dbdeployer/install.sh
DELETED
@@ -1,64 +0,0 @@
|
|
1
|
-
set -e
|
2
|
-
mkdir -p ./dbdeployer/sandboxes
|
3
|
-
mkdir -p ./dbdeployer/binaries
|
4
|
-
|
5
|
-
if [ -z "$(uname | grep Darwin)" ]; then
|
6
|
-
OS=linux
|
7
|
-
set -x
|
8
|
-
else
|
9
|
-
OS=osx
|
10
|
-
fi
|
11
|
-
|
12
|
-
echo "Checking if dbdeployer is installed"
|
13
|
-
if ! [ -x "$(command -v ./bin/dbdeployer)" ]; then
|
14
|
-
echo "Not installed...starting install"
|
15
|
-
VERSION=1.56.0
|
16
|
-
origin=https://github.com/datacharmer/dbdeployer/releases/download/v$VERSION
|
17
|
-
filename=dbdeployer-$VERSION.$OS
|
18
|
-
wget -q $origin/$filename.tar.gz
|
19
|
-
tar -xzf $filename.tar.gz
|
20
|
-
chmod +x $filename
|
21
|
-
sudo mv $filename ./bin/dbdeployer
|
22
|
-
rm $filename.tar.gz
|
23
|
-
else
|
24
|
-
echo "Installation found!"
|
25
|
-
fi
|
26
|
-
|
27
|
-
|
28
|
-
echo "Checking if mysql 5.7.22 is available for dbdeployer"
|
29
|
-
if [ -z "$(./bin/dbdeployer --config ./dbdeployer/config.json --sandbox-binary "./dbdeployer/binaries" available | grep 5.7.22)" ]; then
|
30
|
-
echo "Not found..."
|
31
|
-
|
32
|
-
if [ "$OS" = "linux" ]; then
|
33
|
-
MYSQL_FILE=mysql-5.7.22-linux-glibc2.12-x86_64.tar.gz
|
34
|
-
else
|
35
|
-
MYSQL_FILE=mysql-5.7.22-macos10.13-x86_64.tar.gz
|
36
|
-
fi
|
37
|
-
|
38
|
-
if [ ! -f $MYSQL_FILE ]; then
|
39
|
-
echo "Downloading $MYSQL_FILE...(this may take a while)"
|
40
|
-
wget -q "https://dev.mysql.com/get/Downloads/MySQL-5.7/$MYSQL_FILE"
|
41
|
-
fi
|
42
|
-
|
43
|
-
echo "Setting up..."
|
44
|
-
./bin/dbdeployer unpack $MYSQL_FILE --verbosity 0 --config ./dbdeployer/config.json --sandbox-binary "./dbdeployer/binaries"
|
45
|
-
rm $MYSQL_FILE
|
46
|
-
else
|
47
|
-
echo "mysql 5.7.22 found!"
|
48
|
-
fi
|
49
|
-
|
50
|
-
echo "Forcing new replication setup..."
|
51
|
-
./bin/dbdeployer deploy replication 5.7.22 --nodes 2 --force --config ./dbdeployer/config.json --sandbox-binary "./dbdeployer/binaries" --sandbox-home "./dbdeployer/sandboxes"
|
52
|
-
./bin/dbdeployer global status --config ./dbdeployer/config.json --sandbox-binary "./dbdeployer/binaries" --sandbox-home "./dbdeployer/sandboxes"
|
53
|
-
|
54
|
-
echo "Setting up database.yml"
|
55
|
-
DATABASE_YML=spec/integration/database.yml
|
56
|
-
echo "master:" > $DATABASE_YML
|
57
|
-
cat ./dbdeployer/sandboxes/rsandbox_5_7_22/master/my.sandbox.cnf | grep -A 4 client | tail -n 4 | sed -e 's/ * = /: /' -e 's/^/ /' >> $DATABASE_YML
|
58
|
-
|
59
|
-
echo "slave:" >> $DATABASE_YML
|
60
|
-
cat ./dbdeployer/sandboxes/rsandbox_5_7_22/node1/my.sandbox.cnf | grep -A 4 client | tail -n 4 | sed -e 's/ * = /: /' -e 's/^/ /' >> $DATABASE_YML
|
61
|
-
|
62
|
-
cat $DATABASE_YML
|
63
|
-
|
64
|
-
echo "You are ready to run the integration test suite..."
|