lhm-shopify 3.3.5 → 3.4.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c187c9f0e9bd183d5eb227f83ba9cce6a57ee5b03e4732312470a737f2d8a13
4
- data.tar.gz: 54797e75f27f2f174ffd0eb07cd6d1fad57a9d16542e5baa0a56cb1130fcd88a
3
+ metadata.gz: cfb1802db81b2673ea5e52acec84b4e457a8d5debb74f5391fcb282bf1fc8484
4
+ data.tar.gz: 88abd0ae0484efa69777b0284c253788996b2148c0cb61fc035c0ccd7d49627c
5
5
  SHA512:
6
- metadata.gz: 9f11850e292435adb700695686887945d729ec8ec3b2d4b658fd1d6dd297f198e17711b26f2f1533cca0a58cdcb5f45a87bdbb1741f0cd988b57dfb47bd90d5b
7
- data.tar.gz: ba2e873e51070ac3658644f90c7f54a1680707e8aa48db2d844fdcbfd2de83a5a96abbc0521eae9e55894d5e93ee399f9327df4c56f6b95945e381894e93ff6a
6
+ metadata.gz: 4b98a803678e1ab543997d4e6f9031b02511324293c2d97b3f94938ae6b701e55c122633ab6044920363c56d9746e35f9b131433eb65108437c4ffeb8ca2e5ea
7
+ data.tar.gz: '018c3a179dfdf90f9b5fd4bd3f67636644fc47eda7b0b825aee00330db0f83a59cf6855a5e18105493a89ac0a372ac26fd2c356148282449ca2de84fc56c5179'
data/CHANGELOG.md CHANGED
@@ -1,4 +1,16 @@
1
- # 3.3.5 (Jul 5, 2021)
1
+ # 3.4.1 (Sep 22, 2021)
2
+
3
+ * Add better logging to the LHM components (https://github.com/Shopify/lhm/pull/108)
4
+
5
+ # 3.4.0 (Jul 19, 2021)
6
+
7
+ * Log or raise on unexpected duplicated entry warnings during INSERT IGNORE (https://github.com/Shopify/lhm/pull/100)
8
+
9
+ # 3.3.6 (Jul 7, 2021)
10
+
11
+ * Add lhm-shopify.rb to require lhm
12
+
13
+ # 3.3.5 (Jul 5, 2021)
2
14
 
3
15
  * Add comment and collate copying to rename_column
4
16
  * Publish to rubygems
data/lib/lhm/chunker.rb CHANGED
@@ -20,6 +20,7 @@ module Lhm
20
20
  @connection = connection
21
21
  @chunk_finder = ChunkFinder.new(migration, connection, options)
22
22
  @options = options
23
+ @raise_on_warnings = options.fetch(:raise_on_warnings, false)
23
24
  @verifier = options[:verifier]
24
25
  if @throttler = options[:throttler]
25
26
  @throttler.connection = @connection if @throttler.respond_to?(:connection=)
@@ -36,6 +37,8 @@ module Lhm
36
37
  end
37
38
 
38
39
  def execute
40
+ @start_time = Time.now
41
+
39
42
  return if @chunk_finder.table_empty?
40
43
  @next_to_insert = @start
41
44
  while @next_to_insert <= @limit || (@start == @limit)
@@ -44,11 +47,26 @@ module Lhm
44
47
  verify_can_run
45
48
 
46
49
  affected_rows = ChunkInsert.new(@migration, @connection, bottom, top, @options).insert_and_return_count_of_rows_created
50
+ expected_rows = top - bottom + 1
51
+
52
+ # Only log the chunker progress every 5 minutes instead of every iteration
53
+ current_time = Time.now
54
+ if current_time - @start_time > (5 * 60)
55
+ Lhm.logger.info("Inserted #{affected_rows} rows into the destination table from #{bottom} to #{top}")
56
+ @start_time = current_time
57
+ end
58
+
59
+ if affected_rows < expected_rows
60
+ raise_on_non_pk_duplicate_warning
61
+ end
62
+
47
63
  if @throttler && affected_rows > 0
48
64
  @throttler.run
49
65
  end
50
- @printer.notify(bottom, @limit)
66
+
51
67
  @next_to_insert = top + 1
68
+ @printer.notify(bottom, @limit)
69
+
52
70
  break if @start == @limit
53
71
  end
54
72
  @printer.end
@@ -59,6 +77,16 @@ module Lhm
59
77
 
60
78
  private
61
79
 
80
+ def raise_on_non_pk_duplicate_warning
81
+ @connection.query("show warnings").each do |level, code, message|
82
+ unless message.match?(/Duplicate entry .+ for key 'PRIMARY'/)
83
+ m = "Unexpected warning found for inserted row: #{message}"
84
+ Lhm.logger.warn(m)
85
+ raise Error.new(m) if @raise_on_warnings
86
+ end
87
+ end
88
+ end
89
+
62
90
  def bottom
63
91
  @next_to_insert
64
92
  end
@@ -83,5 +111,6 @@ module Lhm
83
111
  return if @chunk_finder.table_empty?
84
112
  @chunk_finder.validate
85
113
  end
114
+
86
115
  end
87
116
  end
@@ -63,11 +63,12 @@ module Lhm
63
63
  retriable_connection.execute(ddl)
64
64
  end
65
65
  end
66
+ Lhm.logger.info("Dropped triggers on #{@lhm_triggers_for_origin.join(', ')}")
67
+ Lhm.logger.info("Dropped tables #{@lhm_triggers_for_origin.join(', ')}")
66
68
  end
67
69
 
68
70
  def report_ddls
69
- puts "The following DDLs would be executed:"
70
- ddls.each { |ddl| puts ddl }
71
+ Lhm.logger.info("The following DDLs would be executed: #{ddls}")
71
72
  end
72
73
  end
73
74
  end
data/lib/lhm/entangler.rb CHANGED
@@ -94,6 +94,7 @@ module Lhm
94
94
  retriable_connection.execute(stmt)
95
95
  end
96
96
  end
97
+ Lhm.logger.info("Created triggers on #{@origin.name}")
97
98
  end
98
99
 
99
100
  def after
@@ -102,6 +103,7 @@ module Lhm
102
103
  retriable_connection.execute(stmt)
103
104
  end
104
105
  end
106
+ Lhm.logger.info("Dropped triggers on #{@origin.name}")
105
107
  end
106
108
 
107
109
  def revert
data/lib/lhm/migrator.rb CHANGED
@@ -214,6 +214,8 @@ module Lhm
214
214
  replacement = %{CREATE TABLE `#{ @origin.destination_name }`}
215
215
  stmt = @origin.ddl.gsub(original, replacement)
216
216
  @connection.execute(tagged(stmt))
217
+
218
+ Lhm.logger.info("Created destination table #{@origin.destination_name}")
217
219
  end
218
220
 
219
221
  def destination_read
data/lib/lhm/printer.rb CHANGED
@@ -12,26 +12,30 @@ module Lhm
12
12
  end
13
13
  end
14
14
 
15
- class Percentage < Base
15
+ class Percentage
16
16
  def initialize
17
- super
18
17
  @max_length = 0
19
18
  end
20
19
 
21
20
  def notify(lowest, highest)
22
21
  return if !highest || highest == 0
22
+
23
+ # The argument lowest represents the next_to_insert row id, and highest represents the
24
+ # maximum id upto which chunker has to copy the data.
25
+ # If all the rows are inserted upto highest, then lowest passed here from chunker was
26
+ # highest + 1, which leads to the printer printing the progress > 100%.
27
+ return if lowest >= highest
28
+
23
29
  message = "%.2f%% (#{lowest}/#{highest}) complete" % (lowest.to_f / highest * 100.0)
24
30
  write(message)
25
31
  end
26
32
 
27
33
  def end
28
34
  write('100% complete')
29
- @output.write "\n"
30
35
  end
31
36
 
32
37
  def exception(e)
33
- write("failed: #{e}")
34
- @output.write "\n"
38
+ Lhm.logger.error("failed: #{e}")
35
39
  end
36
40
 
37
41
  private
@@ -42,7 +46,7 @@ module Lhm
42
46
  extra = 0
43
47
  end
44
48
 
45
- @output.write "\r#{message}" + (' ' * extra)
49
+ Lhm.logger.info(message)
46
50
  end
47
51
  end
48
52
 
data/lib/lhm/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Schmidt
3
3
 
4
4
  module Lhm
5
- VERSION = '3.3.5'
5
+ VERSION = '3.4.2'
6
6
  end
@@ -0,0 +1 @@
1
+ require "lhm"
data/lib/lhm.rb CHANGED
@@ -114,17 +114,21 @@ module Lhm
114
114
  triggers.each do |trigger|
115
115
  connection.execute("drop trigger if exists #{trigger}")
116
116
  end
117
+ logger.info("Dropped triggers #{triggers.join(', ')}")
118
+
117
119
  tables.each do |table|
118
120
  connection.execute("drop table if exists #{table}")
119
121
  end
122
+ logger.info("Dropped tables #{tables.join(', ')}")
123
+
120
124
  true
121
125
  elsif tables.empty? && triggers.empty?
122
- puts 'Everything is clean. Nothing to do.'
126
+ logger.info('Everything is clean. Nothing to do.')
123
127
  true
124
128
  else
125
- puts "Would drop LHM backup tables: #{tables.join(', ')}."
126
- puts "Would drop LHM triggers: #{triggers.join(', ')}."
127
- puts 'Run with Lhm.cleanup(true) to drop all LHM triggers and tables, or Lhm.cleanup_current_run(true, table_name) to clean up a specific LHM.'
129
+ logger.info("Would drop LHM backup tables: #{tables.join(', ')}.")
130
+ logger.info("Would drop LHM triggers: #{triggers.join(', ')}.")
131
+ logger.info('Run with Lhm.cleanup(true) to drop all LHM triggers and tables, or Lhm.cleanup_current_run(true, table_name) to clean up a specific LHM.')
128
132
  false
129
133
  end
130
134
  end
@@ -1,7 +1,6 @@
1
-
2
1
  CREATE TABLE `composite_primary_key` (
3
2
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
4
3
  `shop_id` bigint(20) NOT NULL,
5
- PRIMARY KEY (`shop_id`,`id`),
4
+ CONSTRAINT `pk_composite` PRIMARY KEY (`shop_id`,`id`),
6
5
  INDEX `index_key_id` (`id`)
7
6
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `composite_primary_key_dest` (
2
+ `id` bigint(20) NOT NULL AUTO_INCREMENT,
3
+ `shop_id` bigint(20) NOT NULL,
4
+ CONSTRAINT `pk_composite` PRIMARY KEY (`shop_id`,`id`),
5
+ INDEX `index_key_id` (`id`)
6
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `custom_primary_key_dest` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `pk` varchar(255),
4
+ PRIMARY KEY (`pk`),
5
+ UNIQUE KEY `index_custom_primary_key_on_id` (`id`)
6
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -58,7 +58,7 @@ describe Lhm::AtomicSwitcher do
58
58
  switcher.send :define_singleton_method, :atomic_switch do
59
59
  'SELECT * FROM nonexistent'
60
60
  end
61
- -> { switcher.run }.must_raise(ActiveRecord::StatementInvalid)
61
+ value(-> { switcher.run }).must_raise(ActiveRecord::StatementInvalid)
62
62
  end
63
63
 
64
64
  it "should raise when destination doesn't exist" do
@@ -75,8 +75,8 @@ describe Lhm::AtomicSwitcher do
75
75
  switcher.run
76
76
 
77
77
  slave do
78
- data_source_exists?(@origin).must_equal true
79
- table_read(@migration.archive_name).columns.keys.must_include 'origin'
78
+ value(data_source_exists?(@origin)).must_equal true
79
+ value(table_read(@migration.archive_name).columns.keys).must_include 'origin'
80
80
  end
81
81
  end
82
82
 
@@ -85,8 +85,8 @@ describe Lhm::AtomicSwitcher do
85
85
  switcher.run
86
86
 
87
87
  slave do
88
- data_source_exists?(@destination).must_equal false
89
- table_read(@origin.name).columns.keys.must_include 'destination'
88
+ value(data_source_exists?(@destination)).must_equal false
89
+ value(table_read(@origin.name).columns.keys).must_include 'destination'
90
90
  end
91
91
  end
92
92
  end
@@ -22,7 +22,7 @@ describe Lhm::ChunkInsert do
22
22
  @instance.insert_and_return_count_of_rows_created
23
23
 
24
24
  slave do
25
- count_all(@destination.name).must_equal(1)
25
+ value(count_all(@destination.name)).must_equal(1)
26
26
  end
27
27
  end
28
28
  end
@@ -15,34 +15,120 @@ describe Lhm::Chunker do
15
15
  @origin = table_create(:origin)
16
16
  @destination = table_create(:destination)
17
17
  @migration = Lhm::Migration.new(@origin, @destination)
18
+ @logs = StringIO.new
19
+ Lhm.logger = Logger.new(@logs)
20
+ end
21
+
22
+ def log_messages
23
+ @logs.string.split("\n")
18
24
  end
19
25
 
20
26
  it 'should copy 1 row from origin to destination even if the id of the single row does not start at 1' do
21
27
  execute("insert into origin set id = 1001 ")
22
- printer = Lhm::Printer::Base.new
23
28
 
24
- def printer.notify(*) ;end
25
- def printer.end(*) [] ;end
29
+ Lhm::Chunker.new(@migration, connection, {throttler: throttler, printer: printer} ).run
30
+
31
+ slave do
32
+ value(count_all(@destination.name)).must_equal(1)
33
+ end
34
+
35
+ end
26
36
 
27
- Lhm::Chunker.new(@migration, connection, {:throttler => Lhm::Throttler::Time.new(:stride => 100), :printer => printer} ).run
37
+ it 'should copy and ignore duplicate primary key' do
38
+ execute("insert into origin set id = 1001 ")
39
+ execute("insert into origin set id = 1002 ")
40
+ execute("insert into destination set id = 1002 ")
41
+
42
+ Lhm::Chunker.new(@migration, connection, {throttler: throttler, printer: printer} ).run
28
43
 
29
44
  slave do
30
- count_all(@destination.name).must_equal(1)
45
+ value(count_all(@destination.name)).must_equal(2)
46
+ end
47
+ end
48
+
49
+ it 'should copy and ignore duplicate composite primary key' do
50
+ origin = table_create(:composite_primary_key)
51
+ destination = table_create(:composite_primary_key_dest)
52
+ migration = Lhm::Migration.new(origin, destination)
53
+
54
+ execute("insert into composite_primary_key set id = 1001, shop_id = 1")
55
+ execute("insert into composite_primary_key set id = 1002, shop_id = 1")
56
+ execute("insert into composite_primary_key_dest set id = 1002, shop_id = 1")
57
+
58
+ Lhm::Chunker.new(migration, connection, {throttler: throttler, printer: printer} ).run
59
+
60
+ slave do
61
+ value(count_all(destination.name)).must_equal(2)
62
+ end
63
+ end
64
+
65
+ it 'should copy and raise on unexpected warnings' do
66
+ origin = table_create(:custom_primary_key)
67
+ destination = table_create(:custom_primary_key_dest)
68
+ migration = Lhm::Migration.new(origin, destination)
69
+
70
+ execute("insert into custom_primary_key set id = 1001, pk = 1")
71
+ execute("insert into custom_primary_key_dest set id = 1001, pk = 2")
72
+
73
+ exception = assert_raises(Lhm::Error) do
74
+ Lhm::Chunker.new(migration, connection, {raise_on_warnings: true, throttler: throttler, printer: printer} ).run
31
75
  end
32
76
 
77
+ assert_match "Unexpected warning found for inserted row: Duplicate entry '1001' for key 'index_custom_primary_key_on_id'", exception.message
78
+ end
79
+
80
+ it 'should copy and warn on unexpected warnings by default' do
81
+ origin = table_create(:custom_primary_key)
82
+ destination = table_create(:custom_primary_key_dest)
83
+ migration = Lhm::Migration.new(origin, destination)
84
+
85
+ execute("insert into custom_primary_key set id = 1001, pk = 1")
86
+ execute("insert into custom_primary_key_dest set id = 1001, pk = 2")
87
+
88
+ Lhm::Chunker.new(migration, connection, {throttler: throttler, printer: printer} ).run
89
+
90
+ assert_equal 2, log_messages.length
91
+ assert log_messages[1].include?("Unexpected warning found for inserted row: Duplicate entry '1001' for key 'index_custom_primary_key_on_id'"), log_messages
92
+ end
93
+
94
+ it 'should log two times for two unexpected warnings' do
95
+ origin = table_create(:custom_primary_key)
96
+ destination = table_create(:custom_primary_key_dest)
97
+ migration = Lhm::Migration.new(origin, destination)
98
+
99
+ execute("insert into custom_primary_key set id = 1001, pk = 1")
100
+ execute("insert into custom_primary_key set id = 1002, pk = 2")
101
+ execute("insert into custom_primary_key_dest set id = 1001, pk = 3")
102
+ execute("insert into custom_primary_key_dest set id = 1002, pk = 4")
103
+
104
+ Lhm::Chunker.new(migration, connection, {throttler: throttler, printer: printer} ).run
105
+
106
+ assert_equal 3, log_messages.length
107
+ assert log_messages[1].include?("Unexpected warning found for inserted row: Duplicate entry '1001' for key 'index_custom_primary_key_on_id'"), log_messages
108
+ assert log_messages[2].include?("Unexpected warning found for inserted row: Duplicate entry '1002' for key 'index_custom_primary_key_on_id'"), log_messages
109
+ end
110
+
111
+ it 'should copy and warn on unexpected warnings' do
112
+ origin = table_create(:custom_primary_key)
113
+ destination = table_create(:custom_primary_key_dest)
114
+ migration = Lhm::Migration.new(origin, destination)
115
+
116
+ execute("insert into custom_primary_key set id = 1001, pk = 1")
117
+ execute("insert into custom_primary_key_dest set id = 1001, pk = 2")
118
+
119
+ Lhm::Chunker.new(migration, connection, {raise_on_warnings: false, throttler: throttler, printer: printer} ).run
120
+
121
+ assert_equal 2, log_messages.length
122
+ assert log_messages[1].include?("Unexpected warning found for inserted row: Duplicate entry '1001' for key 'index_custom_primary_key_on_id'"), log_messages
33
123
  end
34
124
 
35
125
  it 'should create the modified destination, even if the source is empty' do
36
126
  execute("truncate origin ")
37
- printer = Lhm::Printer::Base.new
38
127
 
39
- def printer.notify(*) ;end
40
- def printer.end(*) [] ;end
41
-
42
- Lhm::Chunker.new(@migration, connection, {:throttler => Lhm::Throttler::Time.new(:stride => 100), :printer => printer} ).run
128
+ Lhm::Chunker.new(@migration, connection, {throttler: throttler, printer: printer} ).run
43
129
 
44
130
  slave do
45
- count_all(@destination.name).must_equal(0)
131
+ value(count_all(@destination.name)).must_equal(0)
46
132
  end
47
133
 
48
134
  end
@@ -55,11 +141,11 @@ describe Lhm::Chunker do
55
141
  printer.expect(:end, :return_value, [])
56
142
 
57
143
  Lhm::Chunker.new(
58
- @migration, connection, { :throttler => Lhm::Throttler::Time.new(:stride => 100), :printer => printer }
144
+ @migration, connection, { throttler: throttler, printer: printer }
59
145
  ).run
60
146
 
61
147
  slave do
62
- count_all(@destination.name).must_equal(23)
148
+ value(count_all(@destination.name)).must_equal(23)
63
149
  end
64
150
 
65
151
  printer.verify
@@ -69,17 +155,13 @@ describe Lhm::Chunker do
69
155
  it 'should copy all the records of a table, even if the last chunk starts with the last record of it.' do
70
156
  11.times { |n| execute("insert into origin set id = '#{ n + 1 }'") }
71
157
 
72
- printer = Lhm::Printer::Base.new
73
-
74
- def printer.notify(*) ;end
75
- def printer.end(*) [] ;end
76
158
 
77
159
  Lhm::Chunker.new(
78
- @migration, connection, { :throttler => Lhm::Throttler::Time.new(:stride => 10), :printer => printer }
160
+ @migration, connection, { throttler: Lhm::Throttler::Time.new(stride: 10), printer: printer }
79
161
  ).run
80
162
 
81
163
  slave do
82
- count_all(@destination.name).must_equal(11)
164
+ value(count_all(@destination.name)).must_equal(11)
83
165
  end
84
166
 
85
167
  end
@@ -92,11 +174,11 @@ describe Lhm::Chunker do
92
174
  printer.expect(:end, :return_value, [])
93
175
 
94
176
  Lhm::Chunker.new(
95
- @migration, connection, { :throttler => Lhm::Throttler::SlaveLag.new(:stride => 100), :printer => printer }
177
+ @migration, connection, { throttler: Lhm::Throttler::SlaveLag.new(stride: 100), printer: printer }
96
178
  ).run
97
179
 
98
180
  slave do
99
- count_all(@destination.name).must_equal(23)
181
+ value(count_all(@destination.name)).must_equal(23)
100
182
  end
101
183
 
102
184
  printer.verify
@@ -109,19 +191,19 @@ describe Lhm::Chunker do
109
191
  printer.expects(:notify).with(instance_of(Integer), instance_of(Integer)).twice
110
192
  printer.expects(:end)
111
193
 
112
- throttler = Lhm::Throttler::SlaveLag.new(:stride => 10, :allowed_lag => 0)
194
+ throttler = Lhm::Throttler::SlaveLag.new(stride: 10, allowed_lag: 0)
113
195
  def throttler.max_current_slave_lag
114
196
  1
115
197
  end
116
198
 
117
199
  Lhm::Chunker.new(
118
- @migration, connection, { :throttler => throttler, :printer => printer }
200
+ @migration, connection, { throttler: throttler, printer: printer }
119
201
  ).run
120
202
 
121
203
  assert_equal(Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT * 2 * 2, throttler.timeout_seconds)
122
204
 
123
205
  slave do
124
- count_all(@destination.name).must_equal(15)
206
+ value(count_all(@destination.name)).must_equal(15)
125
207
  end
126
208
  end
127
209
 
@@ -133,7 +215,7 @@ describe Lhm::Chunker do
133
215
  printer.expects(:verify)
134
216
  printer.expects(:end)
135
217
 
136
- throttler = Lhm::Throttler::SlaveLag.new(:stride => 10, :allowed_lag => 0)
218
+ throttler = Lhm::Throttler::SlaveLag.new(stride: 10, allowed_lag: 0)
137
219
 
138
220
  def throttler.slave_hosts
139
221
  ['127.0.0.1']
@@ -149,14 +231,14 @@ describe Lhm::Chunker do
149
231
  end
150
232
 
151
233
  Lhm::Chunker.new(
152
- @migration, connection, { :throttler => throttler, :printer => printer }
234
+ @migration, connection, { throttler: throttler, printer: printer }
153
235
  ).run
154
236
 
155
237
  assert_equal(Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT, throttler.timeout_seconds)
156
238
  assert_equal(0, throttler.send(:max_current_slave_lag))
157
239
 
158
240
  slave do
159
- count_all(@destination.name).must_equal(15)
241
+ value(count_all(@destination.name)).must_equal(15)
160
242
  end
161
243
 
162
244
  printer.verify
@@ -171,14 +253,14 @@ describe Lhm::Chunker do
171
253
 
172
254
  exception = assert_raises do
173
255
  Lhm::Chunker.new(
174
- @migration, connection, { :verifier => failer, :printer => printer, :throttler => Lhm::Throttler::Time.new(:stride => 100) }
256
+ @migration, connection, { verifier: failer, printer: printer, throttler: throttler }
175
257
  ).run
176
258
  end
177
259
 
178
260
  assert_match "Verification failed, aborting early", exception.message
179
261
 
180
262
  slave do
181
- count_all(@destination.name).must_equal(0)
263
+ value(count_all(@destination.name)).must_equal(0)
182
264
  end
183
265
  end
184
266
  end