fluentd 1.8.1 → 1.9.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of fluentd might be problematic. Click here for more details.

Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -17
  3. data/CHANGELOG.md +23 -0
  4. data/Gemfile +1 -4
  5. data/README.md +2 -2
  6. data/fluentd.gemspec +2 -3
  7. data/lib/fluent/command/plugin_generator.rb +1 -1
  8. data/lib/fluent/config.rb +19 -0
  9. data/lib/fluent/config/literal_parser.rb +13 -8
  10. data/lib/fluent/engine.rb +60 -9
  11. data/lib/fluent/plugin/base.rb +5 -0
  12. data/lib/fluent/plugin/buf_file.rb +10 -6
  13. data/lib/fluent/plugin/buf_file_single.rb +10 -6
  14. data/lib/fluent/plugin/buffer.rb +35 -19
  15. data/lib/fluent/plugin/in_http.rb +9 -9
  16. data/lib/fluent/plugin/in_tail.rb +8 -6
  17. data/lib/fluent/plugin/out_http.rb +2 -2
  18. data/lib/fluent/plugin/output.rb +1 -1
  19. data/lib/fluent/plugin/parser.rb +6 -0
  20. data/lib/fluent/plugin_helper/http_server.rb +0 -1
  21. data/lib/fluent/plugin_helper/record_accessor.rb +0 -8
  22. data/lib/fluent/plugin_helper/server.rb +1 -16
  23. data/lib/fluent/plugin_id.rb +9 -4
  24. data/lib/fluent/static_config_analysis.rb +194 -0
  25. data/lib/fluent/supervisor.rb +101 -26
  26. data/lib/fluent/test/driver/base.rb +4 -3
  27. data/lib/fluent/variable_store.rb +40 -0
  28. data/lib/fluent/version.rb +1 -1
  29. data/test/config/test_system_config.rb +4 -4
  30. data/test/plugin/test_in_http.rb +35 -3
  31. data/test/plugin/test_out_http.rb +8 -2
  32. data/test/plugin/test_output.rb +3 -3
  33. data/test/plugin/test_output_as_buffered_secondary.rb +2 -2
  34. data/test/test_config.rb +27 -5
  35. data/test/test_engine.rb +203 -0
  36. data/test/test_output.rb +2 -2
  37. data/test/test_static_config_analysis.rb +177 -0
  38. data/test/test_supervisor.rb +4 -77
  39. data/test/test_variable_store.rb +65 -0
  40. metadata +15 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7df1551f31fc37657b4a757f55ca11bfa66c9a925662512d36ac87309c07bae6
4
- data.tar.gz: 3aec1319bee2aa7a07f5a2da46925880cd701a2f004eaff8616ee42077c3f437
3
+ metadata.gz: 05d0842de47c79ef5a898ea8777454fe9c9863b4d5b6e5161f05c9d5e30bf1c7
4
+ data.tar.gz: 59fc0341af8411eb6e5830005f097c3f03851df9117d64ea838c91d36e9a7ff6
5
5
  SHA512:
6
- metadata.gz: 76490ade18c60a06ead5f3626d94eccff81c356278841832b4007d232c31db72a1a89e8f4ef4ff12005ee3425ee75c5484bdd2b38ce347d21f22cd53331d6e86
7
- data.tar.gz: c273fe0a21377a313f5a29fbf68c15731bc8dc4fe3cb6330b6b9cf37a8921a716265ee1459d2ef322bd886e3db88411dc88efac7cc50dd498b7f797c404bda22
6
+ metadata.gz: aee7aaba37e954564671aa0790f6af069d583670bd02819578342d35b94061a225e5714fc221b1631d099000d36eb3df04e014edad39a5c7ac3156733cbf172b
7
+ data.tar.gz: 1400809d32318726727359177ecf25e6393154dc0a9e5659516b6f9a73c6f87665c138f7c9f71359047818efa5dcf19f1c620faf998bead64edea5d6f17af3db
@@ -7,12 +7,6 @@ cache: bundler
7
7
  # See here for osx_image -> OSX versions: https://docs.travis-ci.com/user/languages/objective-c
8
8
  matrix:
9
9
  include:
10
- - rvm: 2.1.10
11
- os: linux
12
- - rvm: 2.2.10
13
- os: linux
14
- - rvm: 2.2.10
15
- os: linux-ppc64le
16
10
  - rvm: 2.4.9
17
11
  os: linux
18
12
  - rvm: 2.4.9
@@ -25,13 +19,12 @@ matrix:
25
19
  dist: xenial
26
20
  - rvm: 2.6.5
27
21
  os: linux
22
+ - rvm: 2.7.0
23
+ os: linux
28
24
  - rvm: ruby-head
29
25
  os: linux
30
26
  - rvm: ruby-head
31
27
  os: linux-ppc64le
32
- - rvm: 2.1.10
33
- os: osx
34
- osx_image: xcode8.3 # OSX 10.12
35
28
  - rvm: 2.4.6
36
29
  os: osx
37
30
  osx_image: xcode8.3 # OSX 10.12
@@ -39,9 +32,6 @@ matrix:
39
32
  os: osx
40
33
  osx_image: xcode8.3 # OSX 10.12
41
34
  allow_failures:
42
- - rvm: 2.1.10
43
- os: osx
44
- osx_image: xcode8.3
45
35
  - rvm: 2.4.6
46
36
  os: osx
47
37
  osx_image: xcode8.3
@@ -54,11 +44,6 @@ matrix:
54
44
  branches:
55
45
  only:
56
46
  - master
57
- - v0.12
58
- - v0.14
59
-
60
- before_install:
61
- - gem update --system=2.7.8
62
47
 
63
48
  sudo: false
64
49
  dist: trusty # for TLSv1.2 support
@@ -1,3 +1,26 @@
1
+ # v1.9
2
+
3
+ ## Unreleased
4
+
5
+ ### New feature
6
+
7
+ * New light-weight config reload mechanizm
8
+ https://github.com/fluent/fluentd/pull/2716
9
+ * Drop ruby 2.1/2.2/2.3 support
10
+ https://github.com/fluent/fluentd/pull/2750
11
+
12
+ ### Enhancement
13
+
14
+ * output: Show better message for secondary warning
15
+ https://github.com/fluent/fluentd/pull/2751
16
+ * Clean up code for ruby 2.7
17
+ https://github.com/fluent/fluentd/pull/2753
18
+
19
+ ### Bug fixes
20
+
21
+ * outut/buffer: Fix stage size computation
22
+ https://github.com/fluent/fluentd/pull/2734
23
+
1
24
  # v1.8
2
25
 
3
26
  ## Release v1.8.1 - 2019/12/26
data/Gemfile CHANGED
@@ -2,10 +2,7 @@ source 'https://rubygems.org/'
2
2
 
3
3
  gemspec
4
4
 
5
- # https://github.com/socketry/async-io/blob/v1.23.1/async-io.gemspec#L21
6
- if Gem::Version.create(RUBY_VERSION) >= Gem::Version.create('2.3.0')
7
- gem 'async-http', '~> 0.42'
8
- end
5
+ gem 'async-http', '~> 0.42'
9
6
 
10
7
  local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
11
8
  if File.exist?(local_gemfile)
data/README.md CHANGED
@@ -42,11 +42,11 @@ Mobile/Web Application Logging | Fluentd can function as middleware to enable as
42
42
  ### Branch
43
43
 
44
44
  - master: For v1 development.
45
- - v0.12: For v0.12. This is security maintenance mode. Only security fix is accepted.
45
+ - v0.12: For v0.12. This is deprecated version. we already stopped supporting (See https://www.fluentd.org/blog/drop-schedule-announcement-in-2019).
46
46
 
47
47
  ### Prerequisites
48
48
 
49
- - Ruby 2.1 or later
49
+ - Ruby 2.4 or later
50
50
  - git
51
51
 
52
52
  `git` should be in `PATH`. On Windows, you can use `Github for Windows` and `GitShell` for easy setup.
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ["lib"]
17
17
  gem.license = "Apache-2.0"
18
18
 
19
- gem.required_ruby_version = '>= 2.1'
19
+ gem.required_ruby_version = '>= 2.4'
20
20
 
21
21
  gem.add_runtime_dependency("msgpack", [">= 1.2.0", "< 2.0.0"])
22
22
  gem.add_runtime_dependency("yajl-ruby", ["~> 1.0"])
@@ -27,7 +27,6 @@ Gem::Specification.new do |gem|
27
27
  gem.add_runtime_dependency("tzinfo", [">= 1.0", "< 3.0"])
28
28
  gem.add_runtime_dependency("tzinfo-data", ["~> 1.0"])
29
29
  gem.add_runtime_dependency("strptime", [">= 0.2.2", "< 1.0.0"])
30
- gem.add_runtime_dependency("dig_rb", ["~> 1.0.0"])
31
30
 
32
31
  # build gem for a certain platform. see also Rakefile
33
32
  fake_platform = ENV['GEM_BUILD_FAKE_PLATFORM'].to_s
@@ -40,7 +39,7 @@ Gem::Specification.new do |gem|
40
39
  gem.add_runtime_dependency("certstore_c", ["~> 0.1.2"])
41
40
  end
42
41
 
43
- gem.add_development_dependency("rake", ["~> 11.0"])
42
+ gem.add_development_dependency("rake", ["~> 12.0"])
44
43
  gem.add_development_dependency("flexmock", ["~> 2.0"])
45
44
  gem.add_development_dependency("parallel_tests", ["~> 0.15.3"])
46
45
  gem.add_development_dependency("simplecov", ["~> 0.7"])
@@ -281,7 +281,7 @@ HELP
281
281
  @text = ""
282
282
  @preamble_source = ""
283
283
  @preamble = nil
284
- open(LICENSE_URL) do |io|
284
+ URI.open(LICENSE_URL) do |io|
285
285
  @text = io.read
286
286
  end
287
287
  @preamble_source = @text[/^(\s*Copyright.+)/m, 1]
@@ -20,6 +20,25 @@ require 'fluent/configurable'
20
20
 
21
21
  module Fluent
22
22
  module Config
23
+ # @param config_path [String] config file path
24
+ # @param encoding [String] encoding of config file
25
+ # @param additional_config [String] config which is added to last of config body
26
+ # @param use_v1_config [Bool] config is formatted with v1 or not
27
+ # @return [Fluent::Config]
28
+ def self.build(config_path:, encoding: 'utf-8', additional_config: nil, use_v1_config: true)
29
+ config_fname = File.basename(config_path)
30
+ config_basedir = File.dirname(config_path)
31
+ config_data = File.open(config_path, "r:#{encoding}:utf-8") do |f|
32
+ s = f.read
33
+ if additional_config
34
+ c = additional_config.gsub("\\n", "\n")
35
+ s += "\n#{c}"
36
+ end
37
+ s
38
+ end
39
+ Fluent::Config.parse(config_data, config_fname, config_basedir, use_v1_config)
40
+ end
41
+
23
42
  def self.parse(str, fname, basepath = Dir.pwd, v1_config = nil, syntax: :v1)
24
43
  parser = if fname =~ /\.rb$/ || syntax == :ruby
25
44
  :ruby
@@ -19,7 +19,7 @@ require 'stringio'
19
19
  require 'json'
20
20
  require 'yajl'
21
21
  require 'socket'
22
- require 'irb/ruby-lex' # RubyLex
22
+ require 'ripper'
23
23
 
24
24
  require 'fluent/config/basic_parser'
25
25
 
@@ -155,15 +155,20 @@ module Fluent
155
155
  end
156
156
 
157
157
  def scan_embedded_code
158
- rlex = RubyLex.new
159
- src = '"#{'+@ss.rest+"\n=end\n}"
158
+ src = '"#{'+@ss.rest+"\n=begin\n=end\n}"
160
159
 
161
- input = StringIO.new(src)
162
- input.define_singleton_method(:encoding) { external_encoding }
163
- rlex.set_input(input)
160
+ seek = -1
161
+ while (seek = src.index('}', seek + 1))
162
+ unless Ripper.sexp(src[0..seek] + '"').nil? # eager parsing until valid expression
163
+ break
164
+ end
165
+ end
166
+
167
+ unless seek
168
+ raise Fluent::ConfigParseError, @ss.rest
169
+ end
164
170
 
165
- tk = rlex.token
166
- code = src[3,tk.seek-3]
171
+ code = src[3, seek-3]
167
172
 
168
173
  if @ss.rest.length < code.length
169
174
  @ss.pos += @ss.rest.length
@@ -23,6 +23,7 @@ require 'fluent/time'
23
23
  require 'fluent/system_config'
24
24
  require 'fluent/plugin'
25
25
  require 'fluent/fluent_log_event_router'
26
+ require 'fluent/static_config_analysis'
26
27
 
27
28
  module Fluent
28
29
  class EngineClass
@@ -157,14 +158,47 @@ module Fluent
157
158
  raise
158
159
  end
159
160
 
160
- unless @log_event_verbose
161
- $log.enable_event(false)
162
- @fluent_log_event_router.graceful_stop
161
+ stop_phase(@root_agent)
162
+ end
163
+
164
+ # @param conf [Fluent::Config]
165
+ # @param supervisor [Bool]
166
+ # @reutrn nil
167
+ def reload_config(conf, supervisor: false)
168
+ # configure first to reduce down time while restarting
169
+ new_agent = RootAgent.new(log: log, system_config: @system_config)
170
+ ret = Fluent::StaticConfigAnalysis.call(conf, workers: system_config.workers)
171
+
172
+ ret.all_plugins.each do |plugin|
173
+ if plugin.respond_to?(:reloadable_plugin?) && !plugin.reloadable_plugin?
174
+ raise Fluent::ConfigError, "Unreloadable plugin plugin: #{Fluent::Plugin.lookup_type_from_class(plugin.class)}, plugin_id: #{plugin.plugin_id}, class_name: #{plugin.class})"
175
+ end
163
176
  end
164
- $log.info "shutting down fluentd worker", worker: worker_id
165
- shutdown
166
177
 
167
- @fluent_log_event_router.stop
178
+ # Assign @root_agent to new root_agent
179
+ # for https://github.com/fluent/fluentd/blob/fcef949ce40472547fde295ddd2cfe297e1eddd6/lib/fluent/plugin_helper/event_emitter.rb#L50
180
+ old_agent, @root_agent = @root_agent, new_agent
181
+ begin
182
+ @root_agent.configure(conf)
183
+ rescue
184
+ @root_agent = old_agent
185
+ raise
186
+ end
187
+
188
+ unless @suppress_config_dump
189
+ $log.info :supervisor, "using configuration file: #{conf.to_s.rstrip}"
190
+ end
191
+
192
+ # supervisor doesn't handle actual data. so the following code is unnecessary.
193
+ if supervisor
194
+ old_agent.shutdown # to close thread created in #configure
195
+ return
196
+ end
197
+
198
+ stop_phase(old_agent)
199
+
200
+ $log.info 'restart fluentd worker', worker: worker_id
201
+ start_phase(new_agent)
168
202
  end
169
203
 
170
204
  def stop
@@ -189,12 +223,29 @@ module Fluent
189
223
  end
190
224
 
191
225
  private
192
- def start
226
+
227
+ def stop_phase(root_agent)
228
+ unless @log_event_verbose
229
+ $log.enable_event(false)
230
+ @fluent_log_event_router.graceful_stop
231
+ end
232
+ $log.info 'shutting down fluentd worker', worker: worker_id
233
+ root_agent.shutdown
234
+
235
+ @fluent_log_event_router.stop
236
+ end
237
+
238
+ def start_phase(root_agent)
239
+ @fluent_log_event_router = FluentLogEventRouter.build(root_agent)
240
+ if @fluent_log_event_router.emittable?
241
+ $log.enable_event(true)
242
+ end
243
+
193
244
  @root_agent.start
194
245
  end
195
246
 
196
- def shutdown
197
- @root_agent.shutdown
247
+ def start
248
+ @root_agent.start
198
249
  end
199
250
  end
200
251
 
@@ -187,6 +187,11 @@ module Fluent
187
187
  # https://github.com/ruby/ruby/blob/trunk/gc.c#L788
188
188
  "#<%s:%014x>" % [self.class.name, '0x%014x' % (__id__ << 1)]
189
189
  end
190
+
191
+ def reloadable_plugin?
192
+ # Engine can't capture all class variables. so it's forbbiden to use class variables in each plugins if enabling reload.
193
+ self.class.class_variables.empty?
194
+ end
190
195
  end
191
196
  end
192
197
  end
@@ -19,6 +19,7 @@ require 'fileutils'
19
19
  require 'fluent/plugin/buffer'
20
20
  require 'fluent/plugin/buffer/file_chunk'
21
21
  require 'fluent/system_config'
22
+ require 'fluent/variable_store'
22
23
 
23
24
  module Fluent
24
25
  module Plugin
@@ -43,19 +44,20 @@ module Fluent
43
44
  config_param :file_permission, :string, default: nil # '0644'
44
45
  config_param :dir_permission, :string, default: nil # '0755'
45
46
 
46
- @@buffer_paths = {}
47
-
48
47
  def initialize
49
48
  super
50
49
  @symlink_path = nil
51
50
  @multi_workers_available = false
52
51
  @additional_resume_path = nil
53
52
  @buffer_path = nil
53
+ @variable_store = nil
54
54
  end
55
55
 
56
56
  def configure(conf)
57
57
  super
58
58
 
59
+ @variable_store = Fluent::VariableStore.fetch_or_build(:buf_file)
60
+
59
61
  multi_workers_configured = owner.system_config.workers > 1 ? true : false
60
62
 
61
63
  using_plugin_root_dir = false
@@ -69,13 +71,13 @@ module Fluent
69
71
  end
70
72
 
71
73
  type_of_owner = Plugin.lookup_type_from_class(@_owner.class)
72
- if @@buffer_paths.has_key?(@path) && !called_in_test?
73
- type_using_this_path = @@buffer_paths[@path]
74
+ if @variable_store.has_key?(@path) && !called_in_test?
75
+ type_using_this_path = @variable_store[@path]
74
76
  raise ConfigError, "Other '#{type_using_this_path}' plugin already use same buffer path: type = #{type_of_owner}, buffer path = #{@path}"
75
77
  end
76
78
 
77
79
  @buffer_path = @path
78
- @@buffer_paths[@buffer_path] = type_of_owner
80
+ @variable_store[@buffer_path] = type_of_owner
79
81
 
80
82
  specified_directory_exists = File.exist?(@path) && File.directory?(@path)
81
83
  unexisting_path_for_directory = !File.exist?(@path) && !@path.include?('.*')
@@ -125,7 +127,9 @@ module Fluent
125
127
  end
126
128
 
127
129
  def stop
128
- @@buffer_paths.delete(@buffer_path)
130
+ if @variable_store
131
+ @variable_store.delete(@buffer_path)
132
+ end
129
133
 
130
134
  super
131
135
  end
@@ -19,6 +19,7 @@ require 'fileutils'
19
19
  require 'fluent/plugin/buffer'
20
20
  require 'fluent/plugin/buffer/file_single_chunk'
21
21
  require 'fluent/system_config'
22
+ require 'fluent/variable_store'
22
23
 
23
24
  module Fluent
24
25
  module Plugin
@@ -48,18 +49,19 @@ module Fluent
48
49
  desc 'The permission of chunk directory. If no specified, <system> setting or 0755 is used'
49
50
  config_param :dir_permission, :string, default: nil
50
51
 
51
- @@buffer_paths = {}
52
-
53
52
  def initialize
54
53
  super
55
54
 
56
55
  @multi_workers_available = false
57
56
  @additional_resume_path = nil
57
+ @variable_store = nil
58
58
  end
59
59
 
60
60
  def configure(conf)
61
61
  super
62
62
 
63
+ @variable_store = Fluent::VariableStore.fetch_or_build(:buf_file_single)
64
+
63
65
  if @chunk_format == :auto
64
66
  @chunk_format = owner.formatted_to_msgpack_binary? ? :msgpack : :text
65
67
  end
@@ -117,12 +119,12 @@ module Fluent
117
119
  end
118
120
 
119
121
  type_of_owner = Plugin.lookup_type_from_class(@_owner.class)
120
- if @@buffer_paths.has_key?(@path) && !called_in_test?
121
- type_using_this_path = @@buffer_paths[@path]
122
+ if @variable_store.has_key?(@path) && !called_in_test?
123
+ type_using_this_path = @variable_store[@path]
122
124
  raise Fluent::ConfigError, "Other '#{type_using_this_path}' plugin already uses same buffer path: type = #{type_of_owner}, buffer path = #{@path}"
123
125
  end
124
126
 
125
- @@buffer_paths[@path] = type_of_owner
127
+ @variable_store[@path] = type_of_owner
126
128
  @dir_permission = if @dir_permission
127
129
  @dir_permission.to_i(8)
128
130
  else
@@ -145,7 +147,9 @@ module Fluent
145
147
  end
146
148
 
147
149
  def stop
148
- @@buffer_paths.delete(@path)
150
+ if @variable_store
151
+ @variable_store.delete(@path)
152
+ end
149
153
 
150
154
  super
151
155
  end
@@ -266,10 +266,10 @@ module Fluent
266
266
 
267
267
  log.on_trace { log.trace "writing events into buffer", instance: self.object_id, metadata_size: metadata_and_data.size }
268
268
 
269
- staged_bytesize = 0
270
269
  operated_chunks = []
271
270
  unstaged_chunks = {} # metadata => [chunk, chunk, ...]
272
271
  chunks_to_enqueue = []
272
+ staged_bytesizes_by_chunk = {}
273
273
 
274
274
  begin
275
275
  # sort metadata to get lock of chunks in same order with other threads
@@ -279,7 +279,13 @@ module Fluent
279
279
  chunk.mon_enter # add lock to prevent to be committed/rollbacked from other threads
280
280
  operated_chunks << chunk
281
281
  if chunk.staged?
282
- staged_bytesize += adding_bytesize
282
+ #
283
+ # https://github.com/fluent/fluentd/issues/2712
284
+ # write_once is supposed to write to a chunk only once
285
+ # but this block **may** run multiple times from write_step_by_step and previous write may be rollbacked
286
+ # So we should be counting the stage_size only for the last successful write
287
+ #
288
+ staged_bytesizes_by_chunk[chunk] = adding_bytesize
283
289
  elsif chunk.unstaged?
284
290
  unstaged_chunks[metadata] ||= []
285
291
  unstaged_chunks[metadata] << chunk
@@ -326,27 +332,37 @@ module Fluent
326
332
 
327
333
  # All locks about chunks are released.
328
334
 
329
- synchronize do
330
- # At here, staged chunks may be enqueued by other threads.
331
- @stage_size += staged_bytesize
332
-
333
- chunks_to_enqueue.each do |c|
334
- if c.staged? && (enqueue || chunk_size_full?(c))
335
- m = c.metadata
336
- enqueue_chunk(m)
337
- if unstaged_chunks[m]
338
- u = unstaged_chunks[m].pop
335
+ #
336
+ # Now update the stage, stage_size with proper locking
337
+ # FIX FOR stage_size miscomputation - https://github.com/fluent/fluentd/issues/2712
338
+ #
339
+ staged_bytesizes_by_chunk.each do |chunk, bytesize|
340
+ chunk.synchronize do
341
+ synchronize { @stage_size += bytesize }
342
+ log.on_trace { log.trace { "chunk #{chunk.path} size_added: #{bytesize} new_size: #{chunk.bytesize}" } }
343
+ end
344
+ end
345
+
346
+ chunks_to_enqueue.each do |c|
347
+ if c.staged? && (enqueue || chunk_size_full?(c))
348
+ m = c.metadata
349
+ enqueue_chunk(m)
350
+ if unstaged_chunks[m]
351
+ u = unstaged_chunks[m].pop
352
+ u.synchronize do
339
353
  if u.unstaged? && !chunk_size_full?(u)
340
- @stage[m] = u.staged!
341
- @stage_size += u.bytesize
354
+ synchronize {
355
+ @stage[m] = u.staged!
356
+ @stage_size += u.bytesize
357
+ }
342
358
  end
343
359
  end
344
- elsif c.unstaged?
345
- enqueue_unstaged_chunk(c)
346
- else
347
- # previously staged chunk is already enqueued, closed or purged.
348
- # no problem.
349
360
  end
361
+ elsif c.unstaged?
362
+ enqueue_unstaged_chunk(c)
363
+ else
364
+ # previously staged chunk is already enqueued, closed or purged.
365
+ # no problem.
350
366
  end
351
367
  end
352
368