lhm-shopify 3.5.1 → 3.5.5
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 +4 -4
- data/CHANGELOG.md +26 -0
- data/Gemfile.lock +1 -1
- data/README.md +14 -3
- data/Rakefile +6 -6
- data/dev.yml +5 -2
- data/gemfiles/activerecord_5.2.gemfile.lock +1 -1
- data/gemfiles/activerecord_6.0.gemfile.lock +1 -1
- data/gemfiles/activerecord_6.1.gemfile.lock +1 -1
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +1 -1
- data/lib/lhm/atomic_switcher.rb +4 -3
- data/lib/lhm/chunk_insert.rb +6 -3
- data/lib/lhm/chunker.rb +6 -6
- data/lib/lhm/cleanup/current.rb +4 -1
- data/lib/lhm/connection.rb +33 -25
- data/lib/lhm/entangler.rb +5 -4
- data/lib/lhm/invoker.rb +5 -3
- data/lib/lhm/locked_switcher.rb +2 -0
- data/lib/lhm/proxysql_helper.rb +1 -1
- data/lib/lhm/sql_retry.rb +56 -60
- data/lib/lhm/version.rb +1 -1
- data/lib/lhm.rb +30 -24
- data/spec/integration/atomic_switcher_spec.rb +28 -17
- data/spec/integration/chunker_spec.rb +7 -5
- data/spec/integration/integration_helper.rb +4 -6
- data/spec/integration/lhm_spec.rb +3 -4
- data/spec/integration/proxysql_spec.rb +1 -1
- data/spec/integration/sql_retry/lock_wait_spec.rb +2 -2
- data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +8 -7
- data/spec/test_helper.rb +3 -0
- data/spec/unit/chunker_spec.rb +44 -43
- data/spec/unit/connection_spec.rb +37 -12
- data/spec/unit/entangler_spec.rb +31 -9
- data/spec/unit/lhm_spec.rb +17 -0
- data/spec/unit/throttler/slave_lag_spec.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3893b743c675e62933e58a4f56c0ac2dba1b1081afabc779dc1d899406e7c42
|
4
|
+
data.tar.gz: 569175938b8069e0036f881400b9427180283ec61c29ca5d509a7090a7f8a9ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba3ebf953f97cd793c52cb83fee77dd4436877ca01a51ee6d718c7937916e391a32c659b9f403b62f65cf0a326dc755e6221e3311760738c9c6fb442746e1617
|
7
|
+
data.tar.gz: 2e617329a895d00a2541539526e941fc94dfda8285083d46d4179587a31786cbbb1062bd8dbdfffcc4d046801944ac96aa700c32618a4db9f4346345e9105c7c
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,29 @@
|
|
1
|
+
# 3.5.5 (Jan, 2022)
|
2
|
+
* Fix error where from Config shadowing which would cause LHM to abort on reconnect (https://github.com/Shopify/lhm/pull/128)
|
3
|
+
|
4
|
+
# 3.5.4 (Dec, 2021)
|
5
|
+
* Refactored the way options are handled internally. Code is now much clearer to understand
|
6
|
+
* Removed optional connection_options from `Lhm.setup` and `Lhm.connection`
|
7
|
+
* Option `reconnect_with_consistent_host` will now be provided with `options` for `Lhm.change_table`
|
8
|
+
|
9
|
+
# 3.5.3 (Dec, 2021)
|
10
|
+
* Adds ProxySQL comments at the end of query to accommodate for internal tool's requirements
|
11
|
+
|
12
|
+
# 3.5.2 (Dec, 2021)
|
13
|
+
* Fixed error on undefined connection, when calling `Lhm.connection` without calling `Lhm.setup` first
|
14
|
+
* Changed `Lhm.connection.connection` to `lhm.connection.ar_connection` for increased clarity and readability
|
15
|
+
|
16
|
+
# 3.5.1 (Dec , 2021)
|
17
|
+
* Add better logging to the LHM components (https://github.com/Shopify/lhm/pull/112)
|
18
|
+
* Slave lag throttler now supports ActiveRecord > 6.0
|
19
|
+
* [Dev] Add `Appraisals` to test against multiple version
|
20
|
+
|
21
|
+
# 3.5.0 (Dec , 2021)
|
22
|
+
* Duplicate of 3.4.2 (unfortunate mistake)
|
23
|
+
|
24
|
+
# 3.4.2 (Sept, 2021)
|
25
|
+
* Fixed Chunker's undefined name error (https://github.com/Shopify/lhm/pull/110)
|
26
|
+
|
1
27
|
# 3.4.1 (Sep 22, 2021)
|
2
28
|
|
3
29
|
* Add better logging to the LHM components (https://github.com/Shopify/lhm/pull/108)
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -113,7 +113,7 @@ tables must be cleaned up.
|
|
113
113
|
LHM can recover from connection loss. However, when used in conjunction with ProxySQL, there are multiple ways that
|
114
114
|
connection loss could induce data loss (if triggered by a failover). Therefore it will perform additional checks to
|
115
115
|
ensure that the MySQL host stays consistent across the schema migrations if the feature is enabled.
|
116
|
-
This is done by tagging every query with `/*maintenance:lhm*/`, which will be recognized by ProxySQL.
|
116
|
+
This is done by tagging every query with `/*maintenance:lhm*/`, which will be recognized by ProxySQL.
|
117
117
|
However, to get this feature working, a new ProxySQL query rule must be added.
|
118
118
|
```cnf
|
119
119
|
{
|
@@ -145,9 +145,11 @@ forwarded to the right target.
|
|
145
145
|
```
|
146
146
|
|
147
147
|
Once these changes are added to the ProxySQL configuration (either through `.cnf` or dynamically through the admin interface),
|
148
|
-
the feature can be enabled. This is done by adding this flag when
|
148
|
+
the feature can be enabled. This is done by adding this flag when providing options to the migration:
|
149
149
|
```ruby
|
150
|
-
Lhm.
|
150
|
+
Lhm.change_table(..., options: {reconnect_with_consistent_host: true}) do |t|
|
151
|
+
...
|
152
|
+
end
|
151
153
|
```
|
152
154
|
**Note**: This feature is disabled by default
|
153
155
|
|
@@ -301,6 +303,15 @@ COV=1 bundle exec rake unit && bundle exec rake integration
|
|
301
303
|
open coverage/index.html
|
302
304
|
```
|
303
305
|
|
306
|
+
### Merging for a new version
|
307
|
+
When creating a PR for a new version, make sure that th version has been bumped in `lib/lhm/version.rb`. Then run the following code snippet to ensure the everything is consistent, otherwise
|
308
|
+
the gem will not publish.
|
309
|
+
```bash
|
310
|
+
bundle install
|
311
|
+
bundle update
|
312
|
+
bundle exec appraisals install
|
313
|
+
```
|
314
|
+
|
304
315
|
### Docker Compose
|
305
316
|
The integration tests rely on a replication configuration for MySQL which is being proxied by an instance of ProxySQL.
|
306
317
|
It is important that every container is running to execute the integration test suite.
|
data/Rakefile
CHANGED
@@ -16,16 +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
|
-
|
19
|
+
end
|
20
20
|
|
21
21
|
Rake::TestTask.new('dev') do |t|
|
22
22
|
t.libs << 'lib'
|
23
23
|
t.libs << 'spec'
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
|
25
|
+
files = FileList.new('spec/test_helper.rb')
|
26
|
+
files.add(ENV["SINGLE_TEST"]) if ENV["SINGLE_TEST"]
|
27
|
+
t.test_files = files
|
28
|
+
|
29
29
|
t.verbose = true
|
30
30
|
end
|
31
31
|
|
data/dev.yml
CHANGED
@@ -5,7 +5,7 @@ up:
|
|
5
5
|
or: [mysql@5.7]
|
6
6
|
conflicts: [shopify/shopify/mysql-client, mysql-connector-c, mysql, mysql-client]
|
7
7
|
- wget
|
8
|
-
- ruby: 2.7.
|
8
|
+
- ruby: 2.7.5
|
9
9
|
- bundler
|
10
10
|
- custom:
|
11
11
|
name: Get Appraisal gems
|
@@ -31,7 +31,7 @@ commands:
|
|
31
31
|
if [[ $# -eq 0 ]]; then
|
32
32
|
bundle exec rake unit && bundle exec rake integration
|
33
33
|
else
|
34
|
-
bundle exec rake dev
|
34
|
+
SINGLE_TEST="$@" bundle exec rake dev
|
35
35
|
fi
|
36
36
|
appraisals: bundle exec appraisal rake specs
|
37
37
|
cov: rm -rf coverage; COV=1 bundle exec rake unit && bundle exec rake integration; open coverage/index.html
|
@@ -40,3 +40,6 @@ commands:
|
|
40
40
|
run: docker-compose logs -f
|
41
41
|
clear:
|
42
42
|
run: docker-compose rm -v -s -f && docker-compose up -d && ./scripts/helpers/wait-for-dbs.sh
|
43
|
+
pre-publish:
|
44
|
+
# Ensures all Gemfile.lock are sync with the new version in `lhm/version.rb` and runs appraisals
|
45
|
+
run: bundle install && bundle exec appraisal install && bundle exec appraisal rake specs
|
data/lib/lhm/atomic_switcher.rb
CHANGED
@@ -16,12 +16,13 @@ module Lhm
|
|
16
16
|
|
17
17
|
attr_reader :connection
|
18
18
|
|
19
|
-
|
19
|
+
LOG_PREFIX = "AtomicSwitcher"
|
20
|
+
|
21
|
+
def initialize(migration, connection = nil)
|
20
22
|
@migration = migration
|
21
23
|
@connection = connection
|
22
24
|
@origin = migration.origin
|
23
25
|
@destination = migration.destination
|
24
|
-
@retry_options = options[:retriable] || {}
|
25
26
|
end
|
26
27
|
|
27
28
|
def atomic_switch
|
@@ -39,7 +40,7 @@ module Lhm
|
|
39
40
|
private
|
40
41
|
|
41
42
|
def execute
|
42
|
-
@connection.execute(atomic_switch, should_retry: true,
|
43
|
+
@connection.execute(atomic_switch, should_retry: true, log_prefix: LOG_PREFIX)
|
43
44
|
end
|
44
45
|
end
|
45
46
|
end
|
data/lib/lhm/chunk_insert.rb
CHANGED
@@ -3,16 +3,19 @@ require 'lhm/proxysql_helper'
|
|
3
3
|
|
4
4
|
module Lhm
|
5
5
|
class ChunkInsert
|
6
|
-
|
6
|
+
|
7
|
+
LOG_PREFIX = "ChunkInsert"
|
8
|
+
|
9
|
+
def initialize(migration, connection, lowest, highest, retry_options = {})
|
7
10
|
@migration = migration
|
8
11
|
@connection = connection
|
9
12
|
@lowest = lowest
|
10
13
|
@highest = highest
|
11
|
-
@retry_options =
|
14
|
+
@retry_options = retry_options
|
12
15
|
end
|
13
16
|
|
14
17
|
def insert_and_return_count_of_rows_created
|
15
|
-
@connection.update(sql, should_retry: true,
|
18
|
+
@connection.update(sql, should_retry: true, log_prefix: LOG_PREFIX)
|
16
19
|
end
|
17
20
|
|
18
21
|
def sql
|
data/lib/lhm/chunker.rb
CHANGED
@@ -13,6 +13,8 @@ module Lhm
|
|
13
13
|
|
14
14
|
attr_reader :connection
|
15
15
|
|
16
|
+
LOG_PREFIX = "Chunker"
|
17
|
+
|
16
18
|
# Copy from origin to destination in chunks of size `stride`.
|
17
19
|
# Use the `throttler` class to sleep between each stride.
|
18
20
|
def initialize(migration, connection = nil, options = {})
|
@@ -31,9 +33,7 @@ module Lhm
|
|
31
33
|
@retry_options = options[:retriable] || {}
|
32
34
|
@retry_helper = SqlRetry.new(
|
33
35
|
@connection,
|
34
|
-
|
35
|
-
log_prefix: "Chunker"
|
36
|
-
}.merge!(@retry_options)
|
36
|
+
retry_options: @retry_options
|
37
37
|
)
|
38
38
|
end
|
39
39
|
|
@@ -79,7 +79,7 @@ module Lhm
|
|
79
79
|
private
|
80
80
|
|
81
81
|
def raise_on_non_pk_duplicate_warning
|
82
|
-
@connection.execute("show warnings", should_retry: true,
|
82
|
+
@connection.execute("show warnings", should_retry: true, log_prefix: LOG_PREFIX).each do |level, code, message|
|
83
83
|
unless message.match?(/Duplicate entry .+ for key 'PRIMARY'/)
|
84
84
|
m = "Unexpected warning found for inserted row: #{message}"
|
85
85
|
Lhm.logger.warn(m)
|
@@ -94,14 +94,14 @@ module Lhm
|
|
94
94
|
|
95
95
|
def verify_can_run
|
96
96
|
return unless @verifier
|
97
|
-
@retry_helper.with_retries(
|
97
|
+
@retry_helper.with_retries(log_prefix: LOG_PREFIX) do |retriable_connection|
|
98
98
|
raise "Verification failed, aborting early" if !@verifier.call(retriable_connection)
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
102
102
|
def upper_id(next_id, stride)
|
103
103
|
sql = "select id from `#{ @migration.origin_name }` where id >= #{ next_id } order by id limit 1 offset #{ stride - 1}"
|
104
|
-
top = @connection.select_value(sql, should_retry: true,
|
104
|
+
top = @connection.select_value(sql, should_retry: true, log_prefix: LOG_PREFIX)
|
105
105
|
|
106
106
|
[top ? top.to_i : @limit, @limit].min
|
107
107
|
end
|
data/lib/lhm/cleanup/current.rb
CHANGED
@@ -4,6 +4,9 @@ require 'lhm/sql_retry'
|
|
4
4
|
module Lhm
|
5
5
|
module Cleanup
|
6
6
|
class Current
|
7
|
+
|
8
|
+
LOG_PREFIX = "Current"
|
9
|
+
|
7
10
|
def initialize(run, origin_table_name, connection, options={})
|
8
11
|
@run = run
|
9
12
|
@table_name = TableName.new(origin_table_name)
|
@@ -54,7 +57,7 @@ module Lhm
|
|
54
57
|
|
55
58
|
def execute_ddls
|
56
59
|
ddls.each do |ddl|
|
57
|
-
@connection.execute(ddl, should_retry: true,
|
60
|
+
@connection.execute(ddl, should_retry: true, log_prefix: LOG_PREFIX)
|
58
61
|
end
|
59
62
|
Lhm.logger.info("Dropped triggers on #{@lhm_triggers_for_origin.join(', ')}")
|
60
63
|
Lhm.logger.info("Dropped tables #{@lhm_triggers_for_origin.join(', ')}")
|
data/lib/lhm/connection.rb
CHANGED
@@ -1,20 +1,23 @@
|
|
1
1
|
require 'delegate'
|
2
|
+
require 'forwardable'
|
2
3
|
require 'lhm/sql_retry'
|
3
4
|
|
4
5
|
module Lhm
|
5
|
-
class Connection < SimpleDelegator
|
6
|
-
|
7
6
|
# Lhm::Connection inherits from SingleDelegator. It will forward any unknown method calls to the ActiveRecord
|
8
7
|
# connection.
|
9
|
-
|
10
|
-
|
8
|
+
class Connection < SimpleDelegator
|
9
|
+
extend Forwardable
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
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: {})
|
15
18
|
@sql_retry = Lhm::SqlRetry.new(
|
16
19
|
connection,
|
17
|
-
options:
|
20
|
+
retry_options: options[:retriable] || {},
|
18
21
|
reconnect_with_consistent_host: options[:reconnect_with_consistent_host] || false
|
19
22
|
)
|
20
23
|
|
@@ -22,46 +25,51 @@ module Lhm
|
|
22
25
|
super(connection)
|
23
26
|
end
|
24
27
|
|
25
|
-
def
|
26
|
-
|
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)
|
28
34
|
end
|
29
35
|
|
30
|
-
|
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)
|
31
39
|
if should_retry
|
32
|
-
exec_with_retries(:execute, query,
|
40
|
+
exec_with_retries(:execute, query, log_prefix)
|
33
41
|
else
|
34
42
|
exec(:execute, query)
|
35
43
|
end
|
36
44
|
end
|
37
45
|
|
38
|
-
def update(query, should_retry: false,
|
46
|
+
def update(query, should_retry: false, log_prefix: nil)
|
39
47
|
if should_retry
|
40
|
-
exec_with_retries(:update, query,
|
48
|
+
exec_with_retries(:update, query, log_prefix)
|
41
49
|
else
|
42
50
|
exec(:update, query)
|
43
51
|
end
|
44
52
|
end
|
45
53
|
|
46
|
-
def select_value(query, should_retry: false,
|
54
|
+
def select_value(query, should_retry: false, log_prefix: nil)
|
47
55
|
if should_retry
|
48
|
-
exec_with_retries(:select_value, query,
|
56
|
+
exec_with_retries(:select_value, query, log_prefix)
|
49
57
|
else
|
50
58
|
exec(:select_value, query)
|
51
59
|
end
|
52
60
|
end
|
53
61
|
|
54
|
-
def select_values(query, should_retry: false,
|
62
|
+
def select_values(query, should_retry: false, log_prefix: nil)
|
55
63
|
if should_retry
|
56
|
-
exec_with_retries(:select_values, query,
|
64
|
+
exec_with_retries(:select_values, query, log_prefix)
|
57
65
|
else
|
58
66
|
exec(:select_values, query)
|
59
67
|
end
|
60
68
|
end
|
61
69
|
|
62
|
-
def select_one(query, should_retry: false,
|
70
|
+
def select_one(query, should_retry: false, log_prefix: nil)
|
63
71
|
if should_retry
|
64
|
-
exec_with_retries(:select_one, query,
|
72
|
+
exec_with_retries(:select_one, query, log_prefix)
|
65
73
|
else
|
66
74
|
exec(:select_one, query)
|
67
75
|
end
|
@@ -70,12 +78,12 @@ module Lhm
|
|
70
78
|
private
|
71
79
|
|
72
80
|
def exec(method, sql)
|
73
|
-
|
81
|
+
ar_connection.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
|
74
82
|
end
|
75
83
|
|
76
|
-
def exec_with_retries(method, sql,
|
77
|
-
|
78
|
-
@sql_retry.with_retries(
|
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|
|
79
87
|
conn.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
|
80
88
|
end
|
81
89
|
end
|
data/lib/lhm/entangler.rb
CHANGED
@@ -13,14 +13,15 @@ module Lhm
|
|
13
13
|
|
14
14
|
attr_reader :connection
|
15
15
|
|
16
|
+
LOG_PREFIX = "Entangler"
|
17
|
+
|
16
18
|
# Creates entanglement between two tables. All creates, updates and deletes
|
17
19
|
# to origin will be repeated on the destination table.
|
18
|
-
def initialize(migration, connection = nil
|
20
|
+
def initialize(migration, connection = nil)
|
19
21
|
@intersection = migration.intersection
|
20
22
|
@origin = migration.origin
|
21
23
|
@destination = migration.destination
|
22
24
|
@connection = connection
|
23
|
-
@retry_options = options[:retriable] || {}
|
24
25
|
end
|
25
26
|
|
26
27
|
def entangle
|
@@ -86,14 +87,14 @@ module Lhm
|
|
86
87
|
|
87
88
|
def before
|
88
89
|
entangle.each do |stmt|
|
89
|
-
@connection.execute(stmt, should_retry: true,
|
90
|
+
@connection.execute(stmt, should_retry: true, log_prefix: LOG_PREFIX)
|
90
91
|
end
|
91
92
|
Lhm.logger.info("Created triggers on #{@origin.name}")
|
92
93
|
end
|
93
94
|
|
94
95
|
def after
|
95
96
|
untangle.each do |stmt|
|
96
|
-
@connection.execute(stmt, should_retry: true,
|
97
|
+
@connection.execute(stmt, should_retry: true, log_prefix: LOG_PREFIX)
|
97
98
|
end
|
98
99
|
Lhm.logger.info("Dropped triggers on #{@origin.name}")
|
99
100
|
end
|
data/lib/lhm/invoker.rb
CHANGED
@@ -16,8 +16,8 @@ module Lhm
|
|
16
16
|
class Invoker
|
17
17
|
include SqlHelper
|
18
18
|
LOCK_WAIT_TIMEOUT_DELTA = 10
|
19
|
-
INNODB_LOCK_WAIT_TIMEOUT_MAX=1073741824.freeze # https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout
|
20
|
-
LOCK_WAIT_TIMEOUT_MAX=31536000.freeze # https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html
|
19
|
+
INNODB_LOCK_WAIT_TIMEOUT_MAX = 1073741824.freeze # https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout
|
20
|
+
LOCK_WAIT_TIMEOUT_MAX = 31536000.freeze # https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html
|
21
21
|
|
22
22
|
attr_reader :migrator, :connection
|
23
23
|
|
@@ -49,7 +49,7 @@ module Lhm
|
|
49
49
|
normalize_options(options)
|
50
50
|
set_session_lock_wait_timeouts
|
51
51
|
migration = @migrator.run
|
52
|
-
entangler = Entangler.new(migration, @connection
|
52
|
+
entangler = Entangler.new(migration, @connection)
|
53
53
|
|
54
54
|
entangler.run do
|
55
55
|
options[:verifier] ||= Proc.new { |conn| triggers_still_exist?(conn, entangler) }
|
@@ -90,6 +90,8 @@ module Lhm
|
|
90
90
|
options[:throttler] = Lhm.throttler
|
91
91
|
end
|
92
92
|
|
93
|
+
Lhm.connection.retry_config = options[:retriable] || {}
|
94
|
+
|
93
95
|
rescue => e
|
94
96
|
Lhm.logger.error "LHM run failed with exception=#{e.class} message=#{e.message}"
|
95
97
|
raise
|
data/lib/lhm/locked_switcher.rb
CHANGED