sbader-lhm 1.1.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.
- data/.gitignore +6 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +99 -0
- data/LICENSE +27 -0
- data/README.md +146 -0
- data/Rakefile +20 -0
- data/bin/lhm-kill-queue +172 -0
- data/bin/lhm-spec-clobber.sh +36 -0
- data/bin/lhm-spec-grants.sh +25 -0
- data/bin/lhm-spec-setup-cluster.sh +67 -0
- data/bin/lhm-test-all.sh +10 -0
- data/gemfiles/ar-2.3_mysql.gemfile +5 -0
- data/gemfiles/ar-3.2_mysql.gemfile +5 -0
- data/gemfiles/ar-3.2_mysql2.gemfile +5 -0
- data/lhm.gemspec +27 -0
- data/lib/lhm.rb +45 -0
- data/lib/lhm/atomic_switcher.rb +49 -0
- data/lib/lhm/chunker.rb +114 -0
- data/lib/lhm/command.rb +46 -0
- data/lib/lhm/entangler.rb +98 -0
- data/lib/lhm/intersection.rb +63 -0
- data/lib/lhm/invoker.rb +49 -0
- data/lib/lhm/locked_switcher.rb +71 -0
- data/lib/lhm/migration.rb +30 -0
- data/lib/lhm/migrator.rb +219 -0
- data/lib/lhm/sql_helper.rb +85 -0
- data/lib/lhm/table.rb +97 -0
- data/lib/lhm/version.rb +6 -0
- data/spec/.lhm.example +4 -0
- data/spec/README.md +51 -0
- data/spec/bootstrap.rb +13 -0
- data/spec/fixtures/destination.ddl +6 -0
- data/spec/fixtures/origin.ddl +6 -0
- data/spec/fixtures/small_table.ddl +4 -0
- data/spec/fixtures/users.ddl +12 -0
- data/spec/integration/atomic_switcher_spec.rb +42 -0
- data/spec/integration/chunker_spec.rb +32 -0
- data/spec/integration/entangler_spec.rb +66 -0
- data/spec/integration/integration_helper.rb +140 -0
- data/spec/integration/lhm_spec.rb +204 -0
- data/spec/integration/locked_switcher_spec.rb +42 -0
- data/spec/integration/table_spec.rb +48 -0
- data/spec/unit/atomic_switcher_spec.rb +31 -0
- data/spec/unit/chunker_spec.rb +111 -0
- data/spec/unit/entangler_spec.rb +76 -0
- data/spec/unit/intersection_spec.rb +39 -0
- data/spec/unit/locked_switcher_spec.rb +51 -0
- data/spec/unit/migration_spec.rb +23 -0
- data/spec/unit/migrator_spec.rb +134 -0
- data/spec/unit/sql_helper_spec.rb +32 -0
- data/spec/unit/table_spec.rb +34 -0
- data/spec/unit/unit_helper.rb +14 -0
- metadata +173 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# 1.1.0 (April 29, 2012)
|
2
|
+
|
3
|
+
* Add option to specify custom index name
|
4
|
+
* Add mysql2 compatibility
|
5
|
+
* Add AtomicSwitcher
|
6
|
+
|
7
|
+
# 1.0.3 (February 23, 2012)
|
8
|
+
|
9
|
+
* Improve change_column
|
10
|
+
|
11
|
+
# 1.0.2 (February 17, 2012)
|
12
|
+
|
13
|
+
* closes https://github.com/soundcloud/large-hadron-migrator/issues/11
|
14
|
+
this critical bug could cause data loss. table parser was replaced with
|
15
|
+
an implementation that reads directly from information_schema.
|
16
|
+
|
17
|
+
# 1.0.1 (February 09, 2012)
|
18
|
+
|
19
|
+
* released to rubygems
|
20
|
+
|
21
|
+
# 1.0.0 (February 09, 2012)
|
22
|
+
|
23
|
+
* added change_column
|
24
|
+
* final 1.0 release
|
25
|
+
|
26
|
+
# 1.0.0.rc8 (February 09, 2012)
|
27
|
+
|
28
|
+
* removed spec binaries from gem bins
|
29
|
+
|
30
|
+
# 1.0.0.rc7 (January 31, 2012)
|
31
|
+
|
32
|
+
* added SqlHelper.annotation into the middle of trigger statements. this
|
33
|
+
is for the benefit of the killer script which should not kill trigger
|
34
|
+
statements.
|
35
|
+
|
36
|
+
# 1.0.0.rc6 (January 30, 2012)
|
37
|
+
|
38
|
+
* added --confirm to kill script; fixes to kill script
|
39
|
+
|
40
|
+
# 1.0.0.rc5 (January 30, 2012)
|
41
|
+
|
42
|
+
* moved scripts into bin, renamed, added to gem binaries
|
43
|
+
|
44
|
+
# 1.0.0.rc4 (January 29, 2012)
|
45
|
+
|
46
|
+
* added '-- lhm' to the end of statements for more visibility
|
47
|
+
|
48
|
+
# 1.0.0.rc3 (January 19, 2012)
|
49
|
+
|
50
|
+
* Speedup migrations for tables with large minimum id
|
51
|
+
* Add a bit yard documentation
|
52
|
+
* Fix issues with index creation on reserved column names
|
53
|
+
* Improve error handling
|
54
|
+
* Add tests for replication
|
55
|
+
* Rename public API method from `hadron_change_table` to `change_table`
|
56
|
+
* Add tests for ActiveRecord 2.3 and 3.1 compatibility
|
57
|
+
|
58
|
+
# 1.0.0.rc2 (January 18, 2012)
|
59
|
+
|
60
|
+
* Speedup migrations for tables with large ids
|
61
|
+
* Fix conversion of milliseconds to seconds
|
62
|
+
* Fix handling of sql errors
|
63
|
+
* Add helper to create unique index
|
64
|
+
* Allow index creation on prefix of column
|
65
|
+
* Quote column names on index creation
|
66
|
+
* Remove ambiguous method signature
|
67
|
+
* Documentation fix
|
68
|
+
* 1.8.7 compatibility
|
69
|
+
|
70
|
+
# 1.0.0.rc1 (January 15, 2012)
|
71
|
+
|
72
|
+
* rewrite.
|
73
|
+
|
74
|
+
# 0.2.1 (November 26, 2011)
|
75
|
+
|
76
|
+
* Include changelog in gem
|
77
|
+
|
78
|
+
# 0.2.0 (November 26, 2011)
|
79
|
+
|
80
|
+
* Add Ruby 1.8 compatibility
|
81
|
+
* Setup travis continuous integration
|
82
|
+
* Fix record lose issue
|
83
|
+
* Fix and speed up specs
|
84
|
+
|
85
|
+
# 0.1.4
|
86
|
+
|
87
|
+
* Merged [Pullrequest #9](https://github.com/soundcloud/large-hadron-migrator/pull/9)
|
88
|
+
|
89
|
+
# 0.1.3
|
90
|
+
|
91
|
+
* code cleanup
|
92
|
+
* Merged [Pullrequest #8](https://github.com/soundcloud/large-hadron-migrator/pull/8)
|
93
|
+
* Merged [Pullrequest #7](https://github.com/soundcloud/large-hadron-migrator/pull/7)
|
94
|
+
* Merged [Pullrequest #4](https://github.com/soundcloud/large-hadron-migrator/pull/4)
|
95
|
+
* Merged [Pullrequest #1](https://github.com/soundcloud/large-hadron-migrator/pull/1)
|
96
|
+
|
97
|
+
# 0.1.2
|
98
|
+
|
99
|
+
* Initial Release
|
data/LICENSE
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Copyright (c) 2011, SoundCloud, Rany Keddo, Tobias Bielohlawek, Tobias Schmidt
|
2
|
+
|
3
|
+
All rights reserved.
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
7
|
+
|
8
|
+
- Redistributions of source code must retain the above copyright notice, this
|
9
|
+
list of conditions and the following disclaimer.
|
10
|
+
- Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
12
|
+
and/or other materials provided with the distribution.
|
13
|
+
- Neither the name of the SoundCloud nor the names of its contributors may be
|
14
|
+
used to endorse or promote products derived from this software without
|
15
|
+
specific prior written permission.
|
16
|
+
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
18
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
19
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
20
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
21
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
22
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
23
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
24
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
25
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
26
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
27
|
+
|
data/README.md
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# Large Hadron Migrator [][4]
|
2
|
+
|
3
|
+
Rails style database migrations are a useful way to evolve your data schema in
|
4
|
+
an agile manner. Most Rails projects start like this, and at first, making
|
5
|
+
changes is fast and easy.
|
6
|
+
|
7
|
+
That is until your tables grow to millions of records. At this point, the
|
8
|
+
locking nature of `ALTER TABLE` may take your site down for an hour or more
|
9
|
+
while critical tables are migrated. In order to avoid this, developers begin
|
10
|
+
to design around the problem by introducing join tables or moving the data
|
11
|
+
into another layer. Development gets less and less agile as tables grow and
|
12
|
+
grow. To make the problem worse, adding or changing indices to optimize data
|
13
|
+
access becomes just as difficult.
|
14
|
+
|
15
|
+
> Side effects may include black holes and universe implosion.
|
16
|
+
|
17
|
+
There are few things that can be done at the server or engine level. It is
|
18
|
+
possible to change default values in an `ALTER TABLE` without locking the
|
19
|
+
table. The InnoDB Plugin provides facilities for online index creation, which
|
20
|
+
is great if you are using this engine, but only solves half the problem.
|
21
|
+
|
22
|
+
At SoundCloud we started having migration pains quite a while ago, and after
|
23
|
+
looking around for third party solutions, we decided to create our
|
24
|
+
own. We called it Large Hadron Migrator, and it is a gem for online
|
25
|
+
ActiveRecord migrations.
|
26
|
+
|
27
|
+

|
28
|
+
|
29
|
+
[The Large Hadron collider at CERN](http://en.wikipedia.org/wiki/Large_Hadron_Collider)
|
30
|
+
|
31
|
+
## The idea
|
32
|
+
|
33
|
+
The basic idea is to perform the migration online while the system is live,
|
34
|
+
without locking the table. In contrast to [OAK][0] and the
|
35
|
+
[facebook tool][1], we only use a copy table and triggers.
|
36
|
+
|
37
|
+
The Large Hadron is a test driven Ruby solution which can easily be dropped
|
38
|
+
into an ActiveRecord migration. It presumes a single auto incremented
|
39
|
+
numerical primary key called id as per the Rails convention. Unlike the
|
40
|
+
[twitter solution][2], it does not require the presence of an indexed
|
41
|
+
`updated_at` column.
|
42
|
+
|
43
|
+
## Requirements
|
44
|
+
|
45
|
+
Lhm currently only works with MySQL databases and requires an established
|
46
|
+
ActiveRecord connection.
|
47
|
+
|
48
|
+
It is compatible and [continuously tested][4] with Ruby 1.8.7 and Ruby 1.9.x,
|
49
|
+
ActiveRecord 2.3.x and 3.x as well as mysql and mysql2 adapters.
|
50
|
+
|
51
|
+
## Installation
|
52
|
+
|
53
|
+
Install it via `gem install lhm` or add `gem "lhm"` to your Gemfile.
|
54
|
+
|
55
|
+
## Usage
|
56
|
+
|
57
|
+
You can invoke Lhm directly from a plain ruby file after connecting ActiveRecord
|
58
|
+
to your mysql instance:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
require 'lhm'
|
62
|
+
|
63
|
+
ActiveRecord::Base.establish_connection(
|
64
|
+
:adapter => 'mysql',
|
65
|
+
:host => '127.0.0.1',
|
66
|
+
:database => 'lhm'
|
67
|
+
)
|
68
|
+
|
69
|
+
Lhm.change_table :users do |m|
|
70
|
+
m.add_column :arbitrary, "INT(12)"
|
71
|
+
m.add_index [:arbitrary_id, :created_at]
|
72
|
+
m.ddl("alter table %s add column flag tinyint(1)" % m.name)
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
To use Lhm from an ActiveRecord::Migration in a Rails project, add it to your
|
77
|
+
Gemfile, then invoke as follows:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
require 'lhm'
|
81
|
+
|
82
|
+
class MigrateUsers < ActiveRecord::Migration
|
83
|
+
def self.up
|
84
|
+
Lhm.change_table :users do |m|
|
85
|
+
m.add_column :arbitrary, "INT(12)"
|
86
|
+
m.add_index [:arbitrary_id, :created_at]
|
87
|
+
m.ddl("alter table %s add column flag tinyint(1)" % m.name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.down
|
92
|
+
Lhm.change_table :users do |m|
|
93
|
+
m.remove_index [:arbitrary_id, :created_at]
|
94
|
+
m.remove_column :arbitrary)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
## Table rename strategies
|
101
|
+
|
102
|
+
There are two different table rename strategies available: LockedSwitcher and
|
103
|
+
AtomicSwitcher.
|
104
|
+
|
105
|
+
For all setups which use replication and a MySQL version
|
106
|
+
affected by the the [binlog bug #39675](http://bugs.mysql.com/bug.php?id=39675),
|
107
|
+
we recommend the LockedSwitcher strategy to avoid replication issues. This
|
108
|
+
strategy locks the table being migrated and issues two ALTER TABLE statements.
|
109
|
+
The AtomicSwitcher uses a single atomic RENAME TABLE query and should be favored
|
110
|
+
in setups which do not suffer from the mentioned replication bug.
|
111
|
+
|
112
|
+
Lhm chooses the strategy automatically based on the used MySQL server version,
|
113
|
+
but you can override the behavior with an option:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Lhm.change_table :users, :atomic_switch => true do |m|
|
117
|
+
# ...
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
## Contributing
|
122
|
+
|
123
|
+
We'll check out your contribution if you:
|
124
|
+
|
125
|
+
* Provide a comprehensive suite of tests for your fork.
|
126
|
+
* Have a clear and documented rationale for your changes.
|
127
|
+
* Package these up in a pull request.
|
128
|
+
|
129
|
+
We'll do our best to help you out with any contribution issues you may have.
|
130
|
+
|
131
|
+
## License
|
132
|
+
|
133
|
+
The license is included as LICENSE in this directory.
|
134
|
+
|
135
|
+
## Similar solutions
|
136
|
+
|
137
|
+
* [OAK: online alter table][0]
|
138
|
+
* [Facebook][1]
|
139
|
+
* [Twitter][2]
|
140
|
+
* [pt-online-schema-change][3]
|
141
|
+
|
142
|
+
[0]: http://openarkkit.googlecode.com
|
143
|
+
[1]: http://www.facebook.com/note.php?note\_id=430801045932
|
144
|
+
[2]: https://github.com/freels/table_migrator
|
145
|
+
[3]: http://www.percona.com/doc/percona-toolkit/2.1/pt-online-schema-change.html
|
146
|
+
[4]: http://travis-ci.org/soundcloud/large-hadron-migrator
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
Rake::TestTask.new("unit") do |t|
|
7
|
+
t.libs.push "lib"
|
8
|
+
t.test_files = FileList['spec/unit/*_spec.rb']
|
9
|
+
t.verbose = true
|
10
|
+
end
|
11
|
+
|
12
|
+
Rake::TestTask.new("integration") do |t|
|
13
|
+
t.libs.push "lib"
|
14
|
+
t.test_files = FileList['spec/integration/*_spec.rb']
|
15
|
+
t.verbose = true
|
16
|
+
end
|
17
|
+
|
18
|
+
task :specs => [:unit, :integration]
|
19
|
+
task :default => :specs
|
20
|
+
|
data/bin/lhm-kill-queue
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'lhm/sql_helper'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
module Lhm
|
8
|
+
class KillQueue
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@port = 3306
|
12
|
+
@grace = 10
|
13
|
+
@tiny = 0.1
|
14
|
+
@marker = "%#{ SqlHelper.annotation }%"
|
15
|
+
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
opts.on("-h", "--hostname HOSTNAME") { |v| @hostname = v }
|
18
|
+
opts.on("-u", "--username USERNAME") { |v| @username = v }
|
19
|
+
opts.on("-p", "--password PASSWORD") { |v| @password = v }
|
20
|
+
opts.on("-d", "--database DATABASE") { |v| @database = v }
|
21
|
+
opts.on("-m", "--mode MODE") { |v| @mode = v.to_sym }
|
22
|
+
opts.on("-y", "--confirm") { |v| @confirm = true }
|
23
|
+
end.parse!
|
24
|
+
|
25
|
+
unless(@hostname && @username && @password && @database)
|
26
|
+
abort usage
|
27
|
+
end
|
28
|
+
|
29
|
+
unless([:kill, :master, :slave].include?(@mode))
|
30
|
+
abort "specify -m kill OR -m master OR -m slave"
|
31
|
+
end
|
32
|
+
|
33
|
+
connect
|
34
|
+
end
|
35
|
+
|
36
|
+
def usage
|
37
|
+
<<-desc.gsub(/^ /, '')
|
38
|
+
kills queries on the given server after detecting 'lock table#{ @marker }'.
|
39
|
+
usage:
|
40
|
+
lhm-kill-queue -h hostname -u username -p password -d database \\
|
41
|
+
(-m kill | -m master | -m slave) [--confirm]
|
42
|
+
|
43
|
+
desc
|
44
|
+
end
|
45
|
+
|
46
|
+
def run
|
47
|
+
case @mode
|
48
|
+
when :kill then kill
|
49
|
+
when :master then master
|
50
|
+
when :slave then slave
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def kill
|
55
|
+
lock = trip
|
56
|
+
kill_process(lock)
|
57
|
+
end
|
58
|
+
|
59
|
+
def master
|
60
|
+
lock = trip
|
61
|
+
puts "starting to kill non lhm processes in #{ @grace } seconds"
|
62
|
+
sleep(@grace + @tiny)
|
63
|
+
|
64
|
+
[list_non_lhm].flatten.each do |process|
|
65
|
+
kill_process(process)
|
66
|
+
sleep(@tiny)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def slave
|
71
|
+
lock = trip
|
72
|
+
puts "starting to kill non lhm SELECT processes in #{ @grace } seconds"
|
73
|
+
sleep(@grace + @tiny)
|
74
|
+
|
75
|
+
[list_non_lhm].flatten.each do |process|
|
76
|
+
if(select?(process))
|
77
|
+
kill_process(process)
|
78
|
+
sleep(@tiny)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def connect
|
86
|
+
ActiveRecord::Base.establish_connection({
|
87
|
+
:adapter => 'mysql',
|
88
|
+
:host => @hostname,
|
89
|
+
:port => @port,
|
90
|
+
:username => @username,
|
91
|
+
:password => @password,
|
92
|
+
:database => @database
|
93
|
+
})
|
94
|
+
end
|
95
|
+
|
96
|
+
def connection
|
97
|
+
ActiveRecord::Base.connection
|
98
|
+
end
|
99
|
+
|
100
|
+
def list_non_lhm
|
101
|
+
select_processes %Q(
|
102
|
+
info not like '#{ @marker }' and time > #{ @grace } and command = 'Query'
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
def trip
|
107
|
+
until res = select_processes("info like 'lock table#{ @marker }'").first
|
108
|
+
sleep @tiny
|
109
|
+
print '.'
|
110
|
+
end
|
111
|
+
|
112
|
+
res
|
113
|
+
end
|
114
|
+
|
115
|
+
def kill_process(process_id)
|
116
|
+
puts "killing #{ select_statement(process_id) }"
|
117
|
+
|
118
|
+
if(@confirm)
|
119
|
+
print "confirm ('y' to confirm): "
|
120
|
+
|
121
|
+
if(gets.strip != 'y')
|
122
|
+
puts "skipped."
|
123
|
+
return
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
connection.execute("kill #{ process_id }")
|
128
|
+
puts "killed #{ process_id }"
|
129
|
+
end
|
130
|
+
|
131
|
+
def select?(process)
|
132
|
+
if statement = select_statement(process)
|
133
|
+
case statement
|
134
|
+
when /delete/i then false
|
135
|
+
when /update/i then false
|
136
|
+
when /insert/i then false
|
137
|
+
else
|
138
|
+
!!statement.match(/select/i)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def select_statement(process)
|
144
|
+
if process
|
145
|
+
value %Q(
|
146
|
+
select info from information_schema.processlist where id = #{ process }
|
147
|
+
)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def select_processes(predicate)
|
152
|
+
values %Q(
|
153
|
+
select id from information_schema.processlist
|
154
|
+
where db = '#{ @database }'
|
155
|
+
and user = '#{ @username }'
|
156
|
+
and #{ predicate }
|
157
|
+
)
|
158
|
+
end
|
159
|
+
|
160
|
+
def value(statement)
|
161
|
+
connection.select_value(statement)
|
162
|
+
end
|
163
|
+
|
164
|
+
def values(statement)
|
165
|
+
connection.select_values(statement)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
killer = Lhm::KillQueue.new
|
171
|
+
killer.run
|
172
|
+
|