lhm 1.2.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rubocop.yml +256 -0
  4. data/.travis.yml +5 -1
  5. data/CHANGELOG.md +26 -0
  6. data/README.md +87 -8
  7. data/Rakefile +6 -4
  8. data/bin/lhm-config.sh +7 -0
  9. data/bin/lhm-kill-queue +13 -15
  10. data/bin/lhm-spec-clobber.sh +5 -4
  11. data/bin/lhm-spec-grants.sh +2 -2
  12. data/bin/lhm-spec-setup-cluster.sh +2 -3
  13. data/gemfiles/ar-2.3_mysql.gemfile +2 -1
  14. data/gemfiles/ar-3.2_mysql.gemfile +1 -1
  15. data/gemfiles/ar-3.2_mysql2.gemfile +1 -1
  16. data/gemfiles/dm_mysql.gemfile +1 -1
  17. data/lhm.gemspec +7 -8
  18. data/lib/lhm/atomic_switcher.rb +2 -1
  19. data/lib/lhm/chunker.rb +51 -39
  20. data/lib/lhm/command.rb +4 -2
  21. data/lib/lhm/connection.rb +14 -2
  22. data/lib/lhm/entangler.rb +5 -5
  23. data/lib/lhm/intersection.rb +29 -16
  24. data/lib/lhm/invoker.rb +31 -10
  25. data/lib/lhm/locked_switcher.rb +6 -6
  26. data/lib/lhm/migration.rb +7 -5
  27. data/lib/lhm/migrator.rb +57 -9
  28. data/lib/lhm/printer.rb +54 -0
  29. data/lib/lhm/sql_helper.rb +4 -4
  30. data/lib/lhm/table.rb +12 -12
  31. data/lib/lhm/throttler/time.rb +29 -0
  32. data/lib/lhm/throttler.rb +32 -0
  33. data/lib/lhm/version.rb +1 -1
  34. data/lib/lhm.rb +71 -6
  35. data/spec/.lhm.example +1 -1
  36. data/spec/README.md +20 -13
  37. data/spec/fixtures/lines.ddl +7 -0
  38. data/spec/fixtures/permissions.ddl +5 -0
  39. data/spec/fixtures/tracks.ddl +5 -0
  40. data/spec/fixtures/users.ddl +4 -2
  41. data/spec/integration/atomic_switcher_spec.rb +7 -7
  42. data/spec/integration/chunker_spec.rb +11 -5
  43. data/spec/integration/cleanup_spec.rb +72 -0
  44. data/spec/integration/entangler_spec.rb +11 -11
  45. data/spec/integration/integration_helper.rb +49 -17
  46. data/spec/integration/lhm_spec.rb +157 -37
  47. data/spec/integration/locked_switcher_spec.rb +7 -7
  48. data/spec/integration/table_spec.rb +15 -17
  49. data/spec/test_helper.rb +28 -0
  50. data/spec/unit/atomic_switcher_spec.rb +6 -6
  51. data/spec/unit/chunker_spec.rb +95 -73
  52. data/spec/unit/datamapper_connection_spec.rb +1 -0
  53. data/spec/unit/entangler_spec.rb +19 -19
  54. data/spec/unit/intersection_spec.rb +27 -15
  55. data/spec/unit/lhm_spec.rb +29 -0
  56. data/spec/unit/locked_switcher_spec.rb +14 -14
  57. data/spec/unit/migration_spec.rb +10 -5
  58. data/spec/unit/migrator_spec.rb +53 -41
  59. data/spec/unit/printer_spec.rb +79 -0
  60. data/spec/unit/sql_helper_spec.rb +10 -10
  61. data/spec/unit/table_spec.rb +11 -11
  62. data/spec/unit/throttler_spec.rb +73 -0
  63. data/spec/unit/unit_helper.rb +1 -13
  64. metadata +63 -24
  65. data/spec/bootstrap.rb +0 -13
@@ -1,19 +1,13 @@
1
1
  # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
-
4
- require File.expand_path(File.dirname(__FILE__)) + "/../bootstrap"
5
-
3
+ require 'test_helper'
4
+ require 'yaml'
6
5
  begin
7
- require 'active_record'
8
- begin
9
- require 'mysql2'
10
- rescue LoadError
11
- require 'mysql'
12
- end
6
+ require 'active_support'
13
7
  rescue LoadError
14
- require 'dm-core'
15
- require 'dm-mysql-adapter'
16
8
  end
9
+ $password = YAML.load_file(File.expand_path(File.dirname(__FILE__)) + '/database.yml')['password'] rescue nil
10
+
17
11
  require 'lhm/table'
18
12
  require 'lhm/sql_helper'
19
13
  require 'lhm/connection'
@@ -42,14 +36,19 @@ module IntegrationHelper
42
36
  :host => '127.0.0.1',
43
37
  :database => 'lhm',
44
38
  :username => 'root',
45
- :port => port
39
+ :port => port,
40
+ :password => $password
46
41
  )
47
42
  adapter = ActiveRecord::Base.connection
48
43
  elsif defined?(DataMapper)
49
- adapter = DataMapper.setup(:default, "mysql://root@localhost:#{port}/lhm")
44
+ adapter = DataMapper.setup(:default, "mysql://root:#{$password}@localhost:#{port}/lhm")
50
45
  end
51
46
 
52
47
  Lhm.setup(adapter)
48
+ unless defined?(@@cleaned_up)
49
+ Lhm.cleanup(true)
50
+ @@cleaned_up = true
51
+ end
53
52
  @connection = Lhm::Connection.new(adapter)
54
53
  end
55
54
 
@@ -87,7 +86,6 @@ module IntegrationHelper
87
86
  connect_master!
88
87
  end
89
88
 
90
-
91
89
  yield block
92
90
 
93
91
  if master_slave_mode?
@@ -109,6 +107,10 @@ module IntegrationHelper
109
107
  table_read(fixture_name)
110
108
  end
111
109
 
110
+ def table_rename(from_name, to_name)
111
+ execute "rename table `#{ from_name }` to `#{ to_name }`"
112
+ end
113
+
112
114
  def table_read(fixture_name)
113
115
  Lhm::Table.parse(fixture_name, @connection)
114
116
  end
@@ -127,7 +129,7 @@ module IntegrationHelper
127
129
  end
128
130
 
129
131
  def count_all(table)
130
- query = "select count(*) from #{ table }"
132
+ query = "select count(*) from `#{ table }`"
131
133
  select_value(query).to_i
132
134
  end
133
135
 
@@ -141,7 +143,7 @@ module IntegrationHelper
141
143
  non_unique = type == :non_unique ? 1 : 0
142
144
 
143
145
  !!select_one(%Q<
144
- show indexes in #{ table_name }
146
+ show indexes in `#{ table_name }`
145
147
  where key_name = '#{ key_name }'
146
148
  and non_unique = #{ non_unique }
147
149
  >)
@@ -152,6 +154,36 @@ module IntegrationHelper
152
154
  #
153
155
 
154
156
  def master_slave_mode?
155
- !!ENV["MASTER_SLAVE"]
157
+ !!ENV['MASTER_SLAVE']
158
+ end
159
+
160
+ #
161
+ # Misc
162
+ #
163
+
164
+ def capture_stdout
165
+ out = StringIO.new
166
+ $stdout = out
167
+ yield
168
+ return out.string
169
+ ensure
170
+ $stdout = ::STDOUT
171
+ end
172
+
173
+ def simulate_failed_migration
174
+ Lhm::Entangler.class_eval do
175
+ alias_method :old_after, :after
176
+ def after
177
+ true
178
+ end
179
+ end
180
+
181
+ yield
182
+ ensure
183
+ Lhm::Entangler.class_eval do
184
+ undef_method :after
185
+ alias_method :after, :old_after
186
+ undef_method :old_after
187
+ end
156
188
  end
157
189
  end
@@ -3,33 +3,71 @@
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
5
5
 
6
- require 'lhm'
7
-
8
6
  describe Lhm do
9
7
  include IntegrationHelper
10
8
 
11
9
  before(:each) { connect_master! }
12
10
 
13
- describe "changes" do
11
+ describe 'changes' do
14
12
  before(:each) do
15
13
  table_create(:users)
14
+ table_create(:tracks)
15
+ table_create(:permissions)
16
+ end
17
+
18
+ describe 'when providing a subset of data to copy' do
19
+
20
+ before do
21
+ execute('insert into tracks set id = 13, public = 0')
22
+ 11.times { |n| execute("insert into tracks set id = #{n + 1}, public = 1") }
23
+ 11.times { |n| execute("insert into permissions set track_id = #{n + 1}") }
24
+
25
+ Lhm.change_table(:permissions, :atomic_switch => false) do |t|
26
+ t.filter('inner join tracks on tracks.`id` = permissions.`track_id` and tracks.`public` = 1')
27
+ end
28
+ end
29
+
30
+ describe 'when no additional data is inserted into the table' do
31
+
32
+ it 'migrates the existing data' do
33
+ slave do
34
+ count_all(:permissions).must_equal(11)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe 'when additional data is inserted' do
40
+
41
+ before do
42
+ execute('insert into tracks set id = 14, public = 0')
43
+ execute('insert into tracks set id = 15, public = 1')
44
+ execute('insert into permissions set track_id = 14')
45
+ execute('insert into permissions set track_id = 15')
46
+ end
47
+
48
+ it 'migrates all data' do
49
+ slave do
50
+ count_all(:permissions).must_equal(13)
51
+ end
52
+ end
53
+ end
16
54
  end
17
55
 
18
- it "should add a column" do
56
+ it 'should add a column' do
19
57
  Lhm.change_table(:users, :atomic_switch => false) do |t|
20
58
  t.add_column(:logins, "INT(12) DEFAULT '0'")
21
59
  end
22
60
 
23
61
  slave do
24
- table_read(:users).columns["logins"].must_equal({
25
- :type => "int(12)",
26
- :is_nullable => "YES",
62
+ table_read(:users).columns['logins'].must_equal({
63
+ :type => 'int(12)',
64
+ :is_nullable => 'YES',
27
65
  :column_default => '0'
28
66
  })
29
67
  end
30
68
  end
31
69
 
32
- it "should copy all rows" do
70
+ it 'should copy all rows' do
33
71
  23.times { |n| execute("insert into users set reference = '#{ n }'") }
34
72
 
35
73
  Lhm.change_table(:users, :atomic_switch => false) do |t|
@@ -41,17 +79,17 @@ describe Lhm do
41
79
  end
42
80
  end
43
81
 
44
- it "should remove a column" do
82
+ it 'should remove a column' do
45
83
  Lhm.change_table(:users, :atomic_switch => false) do |t|
46
84
  t.remove_column(:comment)
47
85
  end
48
86
 
49
87
  slave do
50
- table_read(:users).columns["comment"].must_equal nil
88
+ table_read(:users).columns['comment'].must_equal nil
51
89
  end
52
90
  end
53
91
 
54
- it "should add an index" do
92
+ it 'should add an index' do
55
93
  Lhm.change_table(:users, :atomic_switch => false) do |t|
56
94
  t.add_index([:comment, :created_at])
57
95
  end
@@ -61,7 +99,7 @@ describe Lhm do
61
99
  end
62
100
  end
63
101
 
64
- it "should add an index with a custom name" do
102
+ it 'should add an index with a custom name' do
65
103
  Lhm.change_table(:users, :atomic_switch => false) do |t|
66
104
  t.add_index([:comment, :created_at], :my_index_name)
67
105
  end
@@ -71,7 +109,7 @@ describe Lhm do
71
109
  end
72
110
  end
73
111
 
74
- it "should add an index on a column with a reserved name" do
112
+ it 'should add an index on a column with a reserved name' do
75
113
  Lhm.change_table(:users, :atomic_switch => false) do |t|
76
114
  t.add_index(:group)
77
115
  end
@@ -81,7 +119,7 @@ describe Lhm do
81
119
  end
82
120
  end
83
121
 
84
- it "should add a unqiue index" do
122
+ it 'should add a unqiue index' do
85
123
  Lhm.change_table(:users, :atomic_switch => false) do |t|
86
124
  t.add_unique_index(:comment)
87
125
  end
@@ -91,7 +129,7 @@ describe Lhm do
91
129
  end
92
130
  end
93
131
 
94
- it "should remove an index" do
132
+ it 'should remove an index' do
95
133
  Lhm.change_table(:users, :atomic_switch => false) do |t|
96
134
  t.remove_index([:username, :created_at])
97
135
  end
@@ -101,62 +139,144 @@ describe Lhm do
101
139
  end
102
140
  end
103
141
 
104
- it "should remove an index with a custom name" do
142
+ it 'should remove an index with a custom name' do
143
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
144
+ t.remove_index([:username, :group])
145
+ end
146
+
147
+ slave do
148
+ index?(:users, :index_with_a_custom_name).must_equal(false)
149
+ end
150
+ end
151
+
152
+ it 'should remove an index with a custom name by name' do
105
153
  Lhm.change_table(:users, :atomic_switch => false) do |t|
106
- t.remove_index(:reference, :index_users_on_reference)
154
+ t.remove_index(:irrelevant_column_name, :index_with_a_custom_name)
107
155
  end
108
156
 
109
157
  slave do
110
- index?(:users, :index_users_on_reference).must_equal(false)
158
+ index?(:users, :index_with_a_custom_name).must_equal(false)
111
159
  end
112
160
  end
113
161
 
114
- it "should apply a ddl statement" do
162
+ it 'should apply a ddl statement' do
115
163
  Lhm.change_table(:users, :atomic_switch => false) do |t|
116
- t.ddl("alter table %s add column flag tinyint(1)" % t.name)
164
+ t.ddl('alter table %s add column flag tinyint(1)' % t.name)
117
165
  end
118
166
 
119
167
  slave do
120
- table_read(:users).columns["flag"].must_equal({
121
- :type => "tinyint(1)",
122
- :is_nullable => "YES",
168
+ table_read(:users).columns['flag'].must_equal({
169
+ :type => 'tinyint(1)',
170
+ :is_nullable => 'YES',
123
171
  :column_default => nil
124
172
  })
125
173
  end
126
174
  end
127
175
 
128
- it "should change a column" do
176
+ it 'should change a column' do
129
177
  Lhm.change_table(:users, :atomic_switch => false) do |t|
130
178
  t.change_column(:comment, "varchar(20) DEFAULT 'none' NOT NULL")
131
179
  end
132
180
 
133
181
  slave do
134
- table_read(:users).columns["comment"].must_equal({
135
- :type => "varchar(20)",
136
- :is_nullable => "NO",
137
- :column_default => "none"
182
+ table_read(:users).columns['comment'].must_equal({
183
+ :type => 'varchar(20)',
184
+ :is_nullable => 'NO',
185
+ :column_default => 'none'
138
186
  })
139
187
  end
140
188
  end
141
189
 
142
- it "should change the last column in a table" do
190
+ it 'should change the last column in a table' do
143
191
  table_create(:small_table)
144
192
 
145
193
  Lhm.change_table(:small_table, :atomic_switch => false) do |t|
146
- t.change_column(:id, "int(5)")
194
+ t.change_column(:id, 'int(5)')
195
+ end
196
+
197
+ slave do
198
+ table_read(:small_table).columns['id'].must_equal({
199
+ :type => 'int(5)',
200
+ :is_nullable => 'NO',
201
+ :column_default => '0'
202
+ })
203
+ end
204
+ end
205
+
206
+ it 'should rename a column' do
207
+ table_create(:users)
208
+
209
+ execute("INSERT INTO users (username) VALUES ('a user')")
210
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
211
+ t.rename_column(:username, :login)
212
+ end
213
+
214
+ slave do
215
+ table_data = table_read(:users)
216
+ table_data.columns['username'].must_equal(nil)
217
+ table_read(:users).columns['login'].must_equal({
218
+ :type => 'varchar(255)',
219
+ :is_nullable => 'YES',
220
+ :column_default => nil
221
+ })
222
+
223
+ # DM & AR versions of select_one return different structures. The
224
+ # real test is whether the data was copied
225
+ result = select_one('SELECT login from users')
226
+ result = result['login'] if result.respond_to?(:has_key?)
227
+ result.must_equal('a user')
228
+ end
229
+ end
230
+
231
+ it 'should rename a column with a default' do
232
+ table_create(:users)
233
+
234
+ execute("INSERT INTO users (username) VALUES ('a user')")
235
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
236
+ t.rename_column(:group, :fnord)
147
237
  end
148
238
 
149
239
  slave do
150
- table_read(:small_table).columns["id"].must_equal({
151
- :type => "int(5)",
152
- :is_nullable => "NO",
153
- :column_default => "0"
240
+ table_data = table_read(:users)
241
+ table_data.columns['group'].must_equal(nil)
242
+ table_read(:users).columns['fnord'].must_equal({
243
+ :type => 'varchar(255)',
244
+ :is_nullable => 'YES',
245
+ :column_default => 'Superfriends'
154
246
  })
247
+
248
+ # DM & AR versions of select_one return different structures. The
249
+ # real test is whether the data was copied
250
+ result = select_one('SELECT `fnord` from users')
251
+ result = result['fnord'] if result.respond_to?(:has_key?)
252
+ result.must_equal('Superfriends')
253
+ end
254
+ end
255
+
256
+ it 'works when mysql reserved words are used' do
257
+ table_create(:lines)
258
+ execute("insert into `lines` set id = 1, `between` = 'foo'")
259
+ execute("insert into `lines` set id = 2, `between` = 'bar'")
260
+
261
+ Lhm.change_table(:lines) do |t|
262
+ t.add_column('by', 'varchar(10)')
263
+ t.remove_column('lines')
264
+ t.add_index('by')
265
+ t.add_unique_index('between')
266
+ t.remove_index('by')
267
+ end
268
+
269
+ slave do
270
+ table_read(:lines).columns.must_include 'by'
271
+ table_read(:lines).columns.wont_include 'lines'
272
+ index_on_columns?(:lines, ['between'], :unique).must_equal true
273
+ index_on_columns?(:lines, ['by']).must_equal false
274
+ count_all(:lines).must_equal(2)
155
275
  end
156
276
  end
157
277
 
158
- describe "parallel" do
159
- it "should perserve inserts during migration" do
278
+ describe 'parallel' do
279
+ it 'should perserve inserts during migration' do
160
280
  50.times { |n| execute("insert into users set reference = '#{ n }'") }
161
281
 
162
282
  insert = Thread.new do
@@ -180,7 +300,7 @@ describe Lhm do
180
300
  end
181
301
  end
182
302
 
183
- it "should perserve deletes during migration" do
303
+ it 'should perserve deletes during migration' do
184
304
  50.times { |n| execute("insert into users set reference = '#{ n }'") }
185
305
 
186
306
  delete = Thread.new do
@@ -12,30 +12,30 @@ describe Lhm::LockedSwitcher do
12
12
 
13
13
  before(:each) { connect_master! }
14
14
 
15
- describe "switching" do
15
+ describe 'switching' do
16
16
  before(:each) do
17
- @origin = table_create("origin")
18
- @destination = table_create("destination")
17
+ @origin = table_create('origin')
18
+ @destination = table_create('destination')
19
19
  @migration = Lhm::Migration.new(@origin, @destination)
20
20
  end
21
21
 
22
- it "rename origin to archive" do
22
+ it 'rename origin to archive' do
23
23
  switcher = Lhm::LockedSwitcher.new(@migration, connection)
24
24
  switcher.run
25
25
 
26
26
  slave do
27
27
  table_exists?(@origin).must_equal true
28
- table_read(@migration.archive_name).columns.keys.must_include "origin"
28
+ table_read(@migration.archive_name).columns.keys.must_include 'origin'
29
29
  end
30
30
  end
31
31
 
32
- it "rename destination to origin" do
32
+ it 'rename destination to origin' do
33
33
  switcher = Lhm::LockedSwitcher.new(@migration, connection)
34
34
  switcher.run
35
35
 
36
36
  slave do
37
37
  table_exists?(@destination).must_equal false
38
- table_read(@origin.name).columns.keys.must_include "destination"
38
+ table_read(@origin.name).columns.keys.must_include 'destination'
39
39
  end
40
40
  end
41
41
  end
@@ -2,46 +2,44 @@
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
5
-
6
- require 'lhm'
7
5
  require 'lhm/table'
8
6
 
9
7
  describe Lhm::Table do
10
8
  include IntegrationHelper
11
9
 
12
10
  describe Lhm::Table::Parser do
13
- describe "create table parsing" do
11
+ describe 'create table parsing' do
14
12
  before(:each) do
15
13
  connect_master!
16
14
  @table = table_create(:users)
17
15
  end
18
16
 
19
- it "should parse table name in show create table" do
20
- @table.name.must_equal("users")
17
+ it 'should parse table name in show create table' do
18
+ @table.name.must_equal('users')
21
19
  end
22
20
 
23
- it "should parse primary key" do
24
- @table.pk.must_equal("id")
21
+ it 'should parse primary key' do
22
+ @table.pk.must_equal('id')
25
23
  end
26
24
 
27
- it "should parse column type in show create table" do
28
- @table.columns["username"][:type].must_equal("varchar(255)")
25
+ it 'should parse column type in show create table' do
26
+ @table.columns['username'][:type].must_equal('varchar(255)')
29
27
  end
30
28
 
31
- it "should parse column metadata" do
32
- @table.columns["username"][:column_default].must_equal nil
29
+ it 'should parse column metadata' do
30
+ @table.columns['username'][:column_default].must_equal nil
33
31
  end
34
32
 
35
- it "should parse indices" do
33
+ it 'should parse indices' do
36
34
  @table.
37
- indices["index_users_on_username_and_created_at"].
38
- must_equal(["username", "created_at"])
35
+ indices['index_users_on_username_and_created_at'].
36
+ must_equal(['username', 'created_at'])
39
37
  end
40
38
 
41
- it "should parse index" do
39
+ it 'should parse index' do
42
40
  @table.
43
- indices["index_users_on_reference"].
44
- must_equal(["reference"])
41
+ indices['index_users_on_reference'].
42
+ must_equal(['reference'])
45
43
  end
46
44
  end
47
45
  end
@@ -0,0 +1,28 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ require 'minitest/autorun'
5
+ require 'minitest/spec'
6
+ require 'minitest/mock'
7
+ require 'pathname'
8
+ require 'lhm'
9
+
10
+ $project = Pathname.new(File.dirname(__FILE__) + '/..').cleanpath
11
+ $spec = $project.join('spec')
12
+ $fixtures = $spec.join('fixtures')
13
+
14
+ begin
15
+ require 'active_record'
16
+ begin
17
+ require 'mysql2'
18
+ rescue LoadError
19
+ require 'mysql'
20
+ end
21
+ rescue LoadError
22
+ require 'dm-core'
23
+ require 'dm-mysql-adapter'
24
+ end
25
+
26
+ logger = Logger.new STDOUT
27
+ logger.level = Logger::WARN
28
+ Lhm.logger = logger
@@ -12,19 +12,19 @@ describe Lhm::AtomicSwitcher do
12
12
 
13
13
  before(:each) do
14
14
  @start = Time.now
15
- @origin = Lhm::Table.new("origin")
16
- @destination = Lhm::Table.new("destination")
15
+ @origin = Lhm::Table.new('origin')
16
+ @destination = Lhm::Table.new('destination')
17
17
  @migration = Lhm::Migration.new(@origin, @destination, @start)
18
18
  @switcher = Lhm::AtomicSwitcher.new(@migration, nil)
19
19
  end
20
20
 
21
- describe "atomic switch" do
22
- it "should perform a single atomic rename" do
21
+ describe 'atomic switch' do
22
+ it 'should perform a single atomic rename' do
23
23
  @switcher.
24
24
  statements.
25
25
  must_equal([
26
- "rename table `origin` to `#{ @migration.archive_name }`, " +
27
- "`destination` to `origin`"
26
+ "rename table `origin` to `#{ @migration.archive_name }`, " \
27
+ '`destination` to `origin`'
28
28
  ])
29
29
  end
30
30
  end