fluentd 0.14.10-x64-mingw32 → 0.14.11-x64-mingw32

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +14 -6
  3. data/ChangeLog +28 -2
  4. data/appveyor.yml +1 -0
  5. data/lib/fluent/engine.rb +4 -7
  6. data/lib/fluent/error.rb +30 -0
  7. data/lib/fluent/log.rb +0 -7
  8. data/lib/fluent/plugin/base.rb +11 -0
  9. data/lib/fluent/plugin/buf_file.rb +9 -7
  10. data/lib/fluent/plugin/formatter_csv.rb +4 -2
  11. data/lib/fluent/plugin/in_forward.rb +46 -17
  12. data/lib/fluent/plugin/in_http.rb +2 -0
  13. data/lib/fluent/plugin/in_monitor_agent.rb +27 -2
  14. data/lib/fluent/plugin/in_syslog.rb +52 -36
  15. data/lib/fluent/plugin/in_tail.rb +1 -0
  16. data/lib/fluent/plugin/out_forward.rb +39 -29
  17. data/lib/fluent/plugin/output.rb +17 -0
  18. data/lib/fluent/plugin/storage_local.rb +16 -13
  19. data/lib/fluent/plugin_helper/storage.rb +21 -9
  20. data/lib/fluent/plugin_id.rb +17 -0
  21. data/lib/fluent/supervisor.rb +73 -45
  22. data/lib/fluent/system_config.rb +24 -21
  23. data/lib/fluent/version.rb +1 -1
  24. data/test/command/test_fluentd.rb +348 -0
  25. data/test/config/test_system_config.rb +39 -31
  26. data/test/plugin/test_base.rb +20 -0
  27. data/test/plugin/test_buf_file.rb +40 -0
  28. data/test/plugin/test_formatter_csv.rb +8 -0
  29. data/test/plugin/test_in_forward.rb +56 -21
  30. data/test/plugin/test_in_monitor_agent.rb +80 -8
  31. data/test/plugin/test_in_syslog.rb +75 -45
  32. data/test/plugin/test_out_file.rb +0 -1
  33. data/test/plugin/test_out_forward.rb +19 -11
  34. data/test/plugin/test_output.rb +44 -0
  35. data/test/plugin/test_storage_local.rb +290 -2
  36. data/test/plugin_helper/test_child_process.rb +40 -39
  37. data/test/plugin_helper/test_storage.rb +4 -3
  38. data/test/test_log.rb +1 -1
  39. data/test/test_output.rb +3 -0
  40. data/test/test_plugin_id.rb +101 -0
  41. data/test/test_supervisor.rb +3 -0
  42. metadata +7 -2
@@ -35,6 +35,7 @@ module Fluent::Plugin
35
35
  config_param :tag, :string, default: nil
36
36
  config_param :emit_interval, :time, default: 60
37
37
  config_param :include_config, :bool, default: true
38
+ config_param :include_retry, :bool, default: true
38
39
 
39
40
  class MonitorServlet < WEBrick::HTTPServlet::AbstractServlet
40
41
  def initialize(server, agent)
@@ -78,7 +79,7 @@ module Fluent::Plugin
78
79
 
79
80
  # if ?debug=1 is set, set :with_debug_info for get_monitor_info
80
81
  # and :pretty_json for render_json_error
81
- opts = {with_config: @agent.include_config}
82
+ opts = {with_config: @agent.include_config, with_retry: @agent.include_retry}
82
83
  if s = qs['debug'] and s[0]
83
84
  opts[:with_debug_info] = true
84
85
  opts[:pretty_json] = true
@@ -88,6 +89,10 @@ module Fluent::Plugin
88
89
  opts[:with_config] = Fluent::Config.bool_value(with_config)
89
90
  end
90
91
 
92
+ if with_retry = get_search_parameter(qs, 'with_retry'.freeze)
93
+ opts[:with_retry] = Fluent::Config.bool_value(with_retry)
94
+ end
95
+
91
96
  if tag = get_search_parameter(qs, 'tag'.freeze)
92
97
  # ?tag= to search an output plugin by match pattern
93
98
  if obj = @agent.plugin_info_by_tag(tag, opts)
@@ -231,7 +236,7 @@ module Fluent::Plugin
231
236
  if @tag
232
237
  log.debug "tag parameter is specified. Emit plugins info to '#{@tag}'"
233
238
 
234
- opts = {with_config: false}
239
+ opts = {with_config: false, with_retry: false}
235
240
  timer_execute(:in_monitor_agent_emit, @emit_interval, repeat: true) {
236
241
  es = Fluent::MultiEventStream.new
237
242
  now = Fluent::Engine.now
@@ -346,6 +351,8 @@ module Fluent::Plugin
346
351
  end
347
352
  }
348
353
 
354
+ obj['retry'] = get_retry_info(pe.retry) if opts[:with_retry] and pe.instance_variable_defined?(:@retry)
355
+
349
356
  # include all instance variables if :with_debug_info is set
350
357
  if opts[:with_debug_info]
351
358
  iv = {}
@@ -362,6 +369,24 @@ module Fluent::Plugin
362
369
  obj
363
370
  end
364
371
 
372
+ RETRY_INFO = {
373
+ 'start' => '@start',
374
+ 'steps' => '@steps',
375
+ 'next_time' => '@next_time',
376
+ }
377
+
378
+ def get_retry_info(pe_retry)
379
+ retry_variables = {}
380
+
381
+ if pe_retry
382
+ RETRY_INFO.each_pair { |key, param|
383
+ retry_variables[key] = pe_retry.instance_variable_get(param)
384
+ }
385
+ end
386
+
387
+ retry_variables
388
+ end
389
+
365
390
  def plugin_category(pe)
366
391
  case pe
367
392
  when Fluent::Plugin::Input
@@ -14,18 +14,17 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
- require 'cool.io'
18
- require 'yajl'
19
-
20
17
  require 'fluent/plugin/input'
21
18
  require 'fluent/config/error'
22
19
  require 'fluent/plugin/parser'
23
20
 
21
+ require 'yajl'
22
+
24
23
  module Fluent::Plugin
25
24
  class SyslogInput < Input
26
25
  Fluent::Plugin.register_input('syslog', self)
27
26
 
28
- helpers :parser, :compat_parameters, :event_loop
27
+ helpers :parser, :compat_parameters, :server
29
28
 
30
29
  DEFAULT_PARSER = 'syslog'
31
30
  SYSLOG_REGEXP = /^\<([0-9]+)\>(.*)/
@@ -68,11 +67,6 @@ module Fluent::Plugin
68
67
  7 => 'debug'
69
68
  }
70
69
 
71
- def initialize
72
- super
73
- require 'fluent/plugin/socket_util'
74
- end
75
-
76
70
  desc 'The port to listen to.'
77
71
  config_param :port, :integer, default: 5140
78
72
  desc 'The bind address to listen to.'
@@ -81,14 +75,22 @@ module Fluent::Plugin
81
75
  config_param :tag, :string
82
76
  desc 'The transport protocol used to receive logs.(udp, tcp)'
83
77
  config_param :protocol_type, :enum, list: [:tcp, :udp], default: :udp
78
+
84
79
  desc 'If true, add source host to event record.'
85
- config_param :include_source_host, :bool, default: false
80
+ config_param :include_source_host, :bool, default: false, deprecated: 'use "source_hostname_key" or "source_address_key" instead.'
86
81
  desc 'Specify key of source host when include_source_host is true.'
87
82
  config_param :source_host_key, :string, default: 'source_host'.freeze
83
+
84
+ desc 'The field name of hostname of sender.'
85
+ config_param :source_hostname_key, :string, default: nil
86
+ desc 'The field name of source address of sender.'
87
+ config_param :source_address_key, :string, default: nil
88
+
88
89
  desc 'The field name of the priority.'
89
90
  config_param :priority_key, :string, default: nil
90
91
  desc 'The field name of the facility.'
91
92
  config_param :facility_key, :string, default: nil
93
+
92
94
  config_param :blocking_timeout, :time, default: 0.5
93
95
  config_param :message_length_limit, :size, default: 2048
94
96
 
@@ -107,25 +109,57 @@ module Fluent::Plugin
107
109
  @parser = parser_create
108
110
  @parser_parse_priority = @parser.respond_to?(:with_priority) && @parser.with_priority
109
111
 
112
+ if @include_source_host
113
+ if @source_address_key
114
+ raise Fluent::ConfigError, "specify either source_address_key or include_source_host"
115
+ end
116
+ @source_address_key = @source_host_key
117
+ end
118
+ @resolve_name = !!@source_hostname_key
119
+
110
120
  @_event_loop_run_timeout = @blocking_timeout
111
121
  end
112
122
 
113
123
  def start
114
124
  super
115
125
 
116
- @handler = listen(method(:message_handler))
117
- event_loop_attach(@handler)
126
+ log.info "listening syslog socket on #{@bind}:#{@port} with #{@protocol_type}"
127
+ case @protocol_type
128
+ when :udp then start_udp_server
129
+ when :tcp then start_tcp_server
130
+ else
131
+ raise "BUG: invalid protocol_type value:#{@protocol_type}"
132
+ end
118
133
  end
119
134
 
120
- def shutdown
121
- @handler.close
135
+ def start_udp_server
136
+ server_create_udp(:in_syslog_udp_server, @port, bind: @bind, max_bytes: @message_length_limit, resolve_name: @resolve_name) do |data, sock|
137
+ message_handler(data.chomp, sock)
138
+ end
139
+ end
122
140
 
123
- super
141
+ def start_tcp_server
142
+ # syslog family add "\n" to each message and this seems only way to split messages in tcp stream
143
+ delimiter = "\n"
144
+ delimiter_size = delimiter.size
145
+ server_create_connection(:in_syslog_tcp_server, @port, bind: @bind, resolve_name: @resolve_name) do |conn|
146
+ buffer = ""
147
+ conn.data do |data|
148
+ buffer << data
149
+ pos = 0
150
+ while idx = buffer.index(delimiter, pos)
151
+ msg = buffer[pos...idx]
152
+ pos = idx + delimiter_size
153
+ message_handler(msg, conn)
154
+ end
155
+ buffer.slice!(0, pos) if pos > 0
156
+ end
157
+ end
124
158
  end
125
159
 
126
160
  private
127
161
 
128
- def message_handler(data, addr)
162
+ def message_handler(data, sock)
129
163
  pri = nil
130
164
  text = data
131
165
  unless @parser_parse_priority
@@ -150,7 +184,8 @@ module Fluent::Plugin
150
184
 
151
185
  record[@priority_key] = priority if @priority_key
152
186
  record[@facility_key] = facility if @facility_key
153
- record[@source_host_key] = addr[2] if @include_source_host
187
+ record[@source_address_key] = sock.remote_addr if @source_address_key
188
+ record[@source_hostname_key] = sock.remote_host if @source_hostname_key
154
189
 
155
190
  tag = "#{@tag}.#{facility}.#{priority}"
156
191
  emit(tag, time, record)
@@ -160,25 +195,6 @@ module Fluent::Plugin
160
195
  log.error_backtrace
161
196
  end
162
197
 
163
- private
164
-
165
- def listen(callback)
166
- log.info "listening syslog socket on #{@bind}:#{@port} with #{@protocol_type}"
167
- socket_manager_path = ENV['SERVERENGINE_SOCKETMANAGER_PATH']
168
- if Fluent.windows?
169
- socket_manager_path = socket_manager_path.to_i
170
- end
171
- client = ServerEngine::SocketManager::Client.new(socket_manager_path)
172
- if @protocol_type == :udp
173
- @usock = client.listen_udp(@bind, @port)
174
- Fluent::SocketUtil::UdpHandler.new(@usock, log, @message_length_limit, callback)
175
- else
176
- # syslog family add "\n" to each message and this seems only way to split messages in tcp stream
177
- lsock = client.listen_tcp(@bind, @port)
178
- Coolio::TCPServer.new(lsock, nil, Fluent::SocketUtil::TcpHandler, log, "\n", callback)
179
- end
180
- end
181
-
182
198
  def emit(tag, time, record)
183
199
  router.emit(tag, time, record)
184
200
  rescue => e
@@ -97,6 +97,7 @@ module Fluent::Plugin
97
97
  raise Fluent::ConfigError, "tail: 'path' parameter is required on tail input"
98
98
  end
99
99
 
100
+ # TODO: Use plugin_root_dir and storage plugin to store positions if available
100
101
  unless @pos_file
101
102
  $log.warn "'pos_file PATH' parameter is not set to a 'tail' source."
102
103
  $log.warn "this parameter is highly recommended to save the position to resume tailing."
@@ -178,7 +178,7 @@ module Fluent::Plugin
178
178
  # But it should be overwritten by ack_response_timeout to rollback chunks after timeout
179
179
  if @ack_response_timeout && @delayed_commit_timeout != @ack_response_timeout
180
180
  log.info "delayed_commit_timeout is overwritten by ack_response_timeout"
181
- @delayed_commit_timeout = @ack_response_timeout
181
+ @delayed_commit_timeout = @ack_response_timeout + 2 # minimum ack_reader IO.select interval is 1s
182
182
  end
183
183
 
184
184
  @rand_seed = Random.new.seed
@@ -214,22 +214,23 @@ module Fluent::Plugin
214
214
  select_a_healthy_node{|node| node.send_data(tag, chunk) }
215
215
  end
216
216
 
217
- ACKWaitingSockInfo = Struct.new(:sock, :chunk_id, :node, :time, :timeout) do
217
+ ACKWaitingSockInfo = Struct.new(:sock, :chunk_id, :chunk_id_base64, :node, :time, :timeout) do
218
218
  def expired?(now)
219
219
  time + timeout < now
220
220
  end
221
221
  end
222
222
 
223
223
  def try_write(chunk)
224
+ log.trace "writing a chunk to destination", chunk_id: dump_unique_id_hex(chunk.unique_id)
224
225
  if chunk.empty?
225
226
  commit_write(chunk.unique_id)
226
227
  return
227
228
  end
228
229
  tag = chunk.metadata.tag
229
230
  sock, node = select_a_healthy_node{|n| n.send_data(tag, chunk) }
230
- chunk_id = Base64.encode64(chunk.unique_id)
231
+ chunk_id_base64 = Base64.encode64(chunk.unique_id)
231
232
  current_time = Process.clock_gettime(PROCESS_CLOCK_ID)
232
- info = ACKWaitingSockInfo.new(sock, chunk_id, node, current_time, @ack_response_timeout)
233
+ info = ACKWaitingSockInfo.new(sock, chunk.unique_id, chunk_id_base64, node, current_time, @ack_response_timeout)
233
234
  @sock_ack_waiting_mutex.synchronize do
234
235
  @sock_ack_waiting << info
235
236
  end
@@ -341,7 +342,7 @@ module Fluent::Plugin
341
342
  end
342
343
  end
343
344
 
344
- # return chunk id when succeeded for tests
345
+ # return chunk id to be committed
345
346
  def read_ack_from_sock(sock, unpacker)
346
347
  begin
347
348
  raw_data = sock.recv(@read_length)
@@ -359,11 +360,14 @@ module Fluent::Plugin
359
360
  else
360
361
  unpacker.feed(raw_data)
361
362
  res = unpacker.read
362
- if res['ack'] != info.chunk_id
363
+ log.trace "getting response from destination", host: info.node.host, port: info.node.port, chunk_id: dump_unique_id_hex(info.chunk_id), response: res
364
+ if res['ack'] != info.chunk_id_base64
363
365
  # Some errors may have occured when ack and chunk id is different, so send the chunk again.
364
- log.warn "ack in response and chunk id in sent data are different", chunk_id: info.chunk_id, ack: res['ack']
366
+ log.warn "ack in response and chunk id in sent data are different", chunk_id: dump_unique_id_hex(info.chunk_id), ack: res['ack']
365
367
  rollback_write(info.chunk_id)
366
368
  return nil
369
+ else
370
+ log.trace "got a correct ack response", chunk_id: dump_unique_id_hex(info.chunk_id)
367
371
  end
368
372
  return info.chunk_id
369
373
  end
@@ -378,9 +382,9 @@ module Fluent::Plugin
378
382
 
379
383
  def ack_reader
380
384
  select_interval = if @delayed_commit_timeout > 3
381
- 2
385
+ 1
382
386
  else
383
- @delayed_commit_timeout / 2.0
387
+ @delayed_commit_timeout / 3.0
384
388
  end
385
389
 
386
390
  unpacker = Fluent::Engine.msgpack_unpacker
@@ -388,30 +392,36 @@ module Fluent::Plugin
388
392
  while thread_current_running?
389
393
  now = Process.clock_gettime(PROCESS_CLOCK_ID)
390
394
  sockets = []
391
- @sock_ack_waiting_mutex.synchronize do
392
- new_list = []
393
- @sock_ack_waiting.each do |info|
394
- if info.expired?(now)
395
- # There are 2 types of cases when no response has been received from socket:
396
- # (1) the node does not support sending responses
397
- # (2) the node does support sending response but responses have not arrived for some reasons.
398
- log.warn "no response from node. regard it as unavailable.", host: info.node.host, port: info.node.port
399
- info.node.disable!
400
- info.sock.close rescue nil
401
- rollback_write(info.chunk_id)
402
- else
403
- sockets << info.sock
404
- new_list << info
395
+ begin
396
+ @sock_ack_waiting_mutex.synchronize do
397
+ new_list = []
398
+ @sock_ack_waiting.each do |info|
399
+ if info.expired?(now)
400
+ # There are 2 types of cases when no response has been received from socket:
401
+ # (1) the node does not support sending responses
402
+ # (2) the node does support sending response but responses have not arrived for some reasons.
403
+ log.warn "no response from node. regard it as unavailable.", host: info.node.host, port: info.node.port
404
+ info.node.disable!
405
+ info.sock.close rescue nil
406
+ rollback_write(info.chunk_id)
407
+ else
408
+ sockets << info.sock
409
+ new_list << info
410
+ end
405
411
  end
412
+ @sock_ack_waiting = new_list
406
413
  end
407
- @sock_ack_waiting = new_list
408
- end
409
414
 
410
- readable_sockets, _, _ = IO.select(sockets, nil, nil, select_interval)
411
- next unless readable_sockets
415
+ readable_sockets, _, _ = IO.select(sockets, nil, nil, select_interval)
416
+ next unless readable_sockets
412
417
 
413
- readable_sockets.each do |sock|
414
- read_ack_from_sock(sock, unpacker)
418
+ readable_sockets.each do |sock|
419
+ chunk_id = read_ack_from_sock(sock, unpacker)
420
+ commit_write(chunk_id)
421
+ end
422
+ rescue => e
423
+ log.error "unexpected error while receiving ack", error: e
424
+ log.error_backtrace
415
425
  end
416
426
  end
417
427
  end
@@ -43,6 +43,8 @@ module Fluent
43
43
  PROCESS_CLOCK_ID = Process::CLOCK_MONOTONIC_RAW rescue Process::CLOCK_MONOTONIC
44
44
 
45
45
  config_param :time_as_integer, :bool, default: false
46
+ desc 'The threshold to show slow flush logs'
47
+ config_param :slow_flush_log_threshold, :float, default: 20.0
46
48
 
47
49
  # `<buffer>` and `<secondary>` sections are available only when '#format' and '#write' are implemented
48
50
  config_section :buffer, param_name: :buffer_config, init: true, required: false, multi: false, final: true do
@@ -985,6 +987,8 @@ module Fluent
985
987
  end
986
988
 
987
989
  begin
990
+ chunk_write_start = Process.clock_gettime(PROCESS_CLOCK_ID)
991
+
988
992
  if output.delayed_commit
989
993
  log.trace "executing delayed write and commit", chunk: dump_unique_id_hex(chunk.unique_id)
990
994
  @counters_monitor.synchronize{ @write_count += 1 }
@@ -992,14 +996,19 @@ module Fluent
992
996
  # delayed_commit_timeout for secondary is configured in <buffer> of primary (<secondary> don't get <buffer>)
993
997
  @dequeued_chunks << DequeuedChunkInfo.new(chunk.unique_id, Time.now, self.delayed_commit_timeout)
994
998
  end
999
+
995
1000
  output.try_write(chunk)
1001
+ check_slow_flush(chunk_write_start)
996
1002
  else # output plugin without delayed purge
997
1003
  chunk_id = chunk.unique_id
998
1004
  dump_chunk_id = dump_unique_id_hex(chunk_id)
999
1005
  log.trace "adding write count", instance: self.object_id
1000
1006
  @counters_monitor.synchronize{ @write_count += 1 }
1001
1007
  log.trace "executing sync write", chunk: dump_chunk_id
1008
+
1002
1009
  output.write(chunk)
1010
+ check_slow_flush(chunk_write_start)
1011
+
1003
1012
  log.trace "write operation done, committing", chunk: dump_chunk_id
1004
1013
  commit_write(chunk_id, delayed: false, secondary: using_secondary)
1005
1014
  log.trace "done to commit a chunk", chunk: dump_chunk_id
@@ -1019,6 +1028,14 @@ module Fluent
1019
1028
  end
1020
1029
  end
1021
1030
 
1031
+ def check_slow_flush(start)
1032
+ elapsed_time = Process.clock_gettime(PROCESS_CLOCK_ID) - start
1033
+ if elapsed_time > @slow_flush_log_threshold
1034
+ log.warn "buffer flush took longer time than slow_flush_log_threshold:",
1035
+ elapsed_time: elapsed_time, slow_flush_log_threshold: @slow_flush_log_threshold, plugin_id: self.plugin_id
1036
+ end
1037
+ end
1038
+
1022
1039
  def update_retry_state(chunk_id, using_secondary, error = nil)
1023
1040
  @retry_mutex.synchronize do
1024
1041
  @counters_monitor.synchronize{ @num_errors += 1 }
@@ -29,10 +29,16 @@ module Fluent
29
29
  DEFAULT_FILE_MODE = 0644
30
30
 
31
31
  config_param :path, :string, default: nil
32
- config_param :mode, :integer, default: DEFAULT_FILE_MODE
33
- config_param :dir_mode, :integer, default: DEFAULT_DIR_MODE
32
+ config_param :mode, default: DEFAULT_FILE_MODE do |v|
33
+ v.to_i(8)
34
+ end
35
+ config_param :dir_mode, default: DEFAULT_DIR_MODE do |v|
36
+ v.to_i(8)
37
+ end
34
38
  config_param :pretty_print, :bool, default: false
35
39
 
40
+ attr_reader :store # for test
41
+
36
42
  def initialize
37
43
  super
38
44
  @store = {}
@@ -42,9 +48,13 @@ module Fluent
42
48
  super
43
49
 
44
50
  @on_memory = false
45
- if !@path && !@_plugin_id_configured
51
+ if @path
52
+ # use it
53
+ elsif root_dir = owner.plugin_root_dir
54
+ @path = File.join(root_dir, 'storage.json')
55
+ else
46
56
  if @persistent
47
- raise Fluent::ConfigError, "Plugin @id or path for <storage> required to save data"
57
+ raise Fluent::ConfigError, "Plugin @id or path for <storage> required when 'persistent' is true"
48
58
  else
49
59
  if @autosave
50
60
  log.warn "both of Plugin @id and path for <storage> are not specified. Using on-memory store."
@@ -53,18 +63,11 @@ module Fluent
53
63
  end
54
64
  @on_memory = true
55
65
  end
56
- elsif @path
57
- # ok
58
- else # @_plugin_id_configured is true
59
- log.warn "path for <storage> is not specified. Using on-memory store temporarily, but will use file store after support global storage path"
60
- @on_memory = true
61
- ## TODO: get process-wide directory for plugin storage, and generate path for this plugin storage instance
62
- # path =
63
66
  end
64
67
 
65
68
  if !@on_memory
66
69
  dir = File.dirname(@path)
67
- FileUtils.mkdir_p(dir, mode: @dir_mode) unless File.exist?(dir)
70
+ FileUtils.mkdir_p(dir, mode: @dir_mode) unless Dir.exist?(dir)
68
71
  if File.exist?(@path)
69
72
  raise Fluent::ConfigError, "Plugin storage path '#{@path}' is not readable/writable" unless File.readable?(@path) && File.writable?(@path)
70
73
  begin
@@ -75,7 +78,7 @@ module Fluent
75
78
  raise Fluent::ConfigError, "Unexpected error: failed to read data from plugin storage file: '#{@path}'"
76
79
  end
77
80
  else
78
- raise Fluent::ConfigError, "Directory is not writable for plugin storage file '#{dir}'" unless File.writable?(dir)
81
+ raise Fluent::ConfigError, "Directory is not writable for plugin storage file '#{@path}'" unless File.stat(dir).writable?
79
82
  end
80
83
  end
81
84
  end