fluentd 1.19.4 → 1.20.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +46 -0
  3. data/CHANGELOG.md +75 -0
  4. data/GithubWorkflow.md +1 -1
  5. data/MAINTAINERS.md +2 -2
  6. data/README.md +3 -2
  7. data/Rakefile +2 -1
  8. data/SECURITY.md +26 -1
  9. data/lib/fluent/command/cat.rb +2 -2
  10. data/lib/fluent/compat/exec_util.rb +20 -5
  11. data/lib/fluent/config/literal_parser.rb +4 -3
  12. data/lib/fluent/config/yaml_parser/loader.rb +12 -1
  13. data/lib/fluent/config/yaml_parser/parser.rb +14 -13
  14. data/lib/fluent/event.rb +37 -0
  15. data/lib/fluent/load.rb +0 -1
  16. data/lib/fluent/log.rb +2 -2
  17. data/lib/fluent/plugin/buf_file.rb +2 -2
  18. data/lib/fluent/plugin/buf_file_single.rb +6 -2
  19. data/lib/fluent/plugin/buffer/file_chunk.rb +23 -10
  20. data/lib/fluent/plugin/buffer/file_single_chunk.rb +23 -10
  21. data/lib/fluent/plugin/buffer.rb +102 -36
  22. data/lib/fluent/plugin/formatter_out_file.rb +3 -4
  23. data/lib/fluent/plugin/in_exec.rb +6 -4
  24. data/lib/fluent/plugin/in_forward.rb +39 -29
  25. data/lib/fluent/plugin/in_object_space.rb +2 -2
  26. data/lib/fluent/plugin/in_syslog.rb +2 -2
  27. data/lib/fluent/plugin/in_tail/compatibility.rb +111 -0
  28. data/lib/fluent/plugin/in_tail/dir_watcher.rb +713 -0
  29. data/lib/fluent/plugin/in_tail/group_watch.rb +13 -13
  30. data/lib/fluent/plugin/in_tail/io_handler.rb +391 -0
  31. data/lib/fluent/plugin/in_tail/line_feeder.rb +230 -0
  32. data/lib/fluent/plugin/in_tail/stat_watcher.rb +52 -0
  33. data/lib/fluent/plugin/in_tail/tail_watcher.rb +276 -0
  34. data/lib/fluent/plugin/in_tail/worker_pool.rb +298 -0
  35. data/lib/fluent/plugin/in_tail.rb +324 -815
  36. data/lib/fluent/plugin/in_unix.rb +5 -3
  37. data/lib/fluent/plugin/out_exec_filter.rb +2 -2
  38. data/lib/fluent/plugin/out_file.rb +10 -4
  39. data/lib/fluent/plugin/out_forward.rb +8 -0
  40. data/lib/fluent/plugin/out_http.rb +0 -10
  41. data/lib/fluent/plugin/output.rb +49 -19
  42. data/lib/fluent/plugin/parser_csv.rb +7 -2
  43. data/lib/fluent/plugin/parser_json.rb +20 -12
  44. data/lib/fluent/plugin/parser_ltsv.rb +2 -4
  45. data/lib/fluent/plugin/parser_msgpack.rb +7 -6
  46. data/lib/fluent/plugin/parser_syslog.rb +52 -5
  47. data/lib/fluent/plugin/parser_tsv.rb +6 -1
  48. data/lib/fluent/plugin_helper/http_server/server.rb +29 -13
  49. data/lib/fluent/plugin_helper/http_server.rb +2 -1
  50. data/lib/fluent/root_agent.rb +1 -1
  51. data/lib/fluent/supervisor.rb +24 -5
  52. data/lib/fluent/system_config.rb +5 -2
  53. data/lib/fluent/time.rb +8 -7
  54. data/lib/fluent/version.rb +1 -1
  55. data/templates/new_gem/.gitignore +8 -0
  56. metadata +39 -16
@@ -20,16 +20,26 @@ require 'fluent/plugin/input'
20
20
  require 'fluent/config/error'
21
21
  require 'fluent/event'
22
22
  require 'fluent/plugin/buffer'
23
+ require 'fluent/ext_monitor_require'
23
24
  require 'fluent/plugin/parser_multiline'
24
25
  require 'fluent/variable_store'
25
26
  require 'fluent/capability'
26
27
  require 'fluent/plugin/in_tail/position_file'
27
28
  require 'fluent/plugin/in_tail/group_watch'
29
+ require 'fluent/plugin/in_tail/stat_watcher'
30
+ require 'fluent/plugin/in_tail/dir_watcher'
31
+ require 'fluent/plugin/in_tail/io_handler'
32
+ require 'fluent/plugin/in_tail/tail_watcher'
33
+ require 'fluent/plugin/in_tail/line_feeder'
34
+ require 'fluent/plugin/in_tail/compatibility'
35
+ require 'fluent/plugin/in_tail/worker_pool'
28
36
  require 'fluent/file_wrapper'
29
37
 
30
38
  module Fluent::Plugin
31
39
  class TailInput < Fluent::Plugin::Input
32
40
  include GroupWatch
41
+ include Compatibility
42
+ include DirWatcher
33
43
 
34
44
  Fluent::Plugin.register_input('tail', self)
35
45
 
@@ -54,12 +64,14 @@ module Fluent::Plugin
54
64
  @tails = {}
55
65
  @tails_rotate_wait = {}
56
66
  @pf_file = nil
67
+ @watcher_refresh_monitor = Monitor.new
57
68
  @pf = nil
58
69
  @ignore_list = []
59
70
  @shutdown_start_time = nil
60
71
  @metrics = nil
61
72
  @startup = true
62
73
  @capability = Fluent::Capability.new(:current_process)
74
+ initialize_dir_watchers
63
75
  end
64
76
 
65
77
  desc 'The paths to read. Multiple paths can be specified, separated by comma.'
@@ -87,6 +99,8 @@ module Fluent::Plugin
87
99
  config_param :refresh_interval, :time, default: 60
88
100
  desc 'The number of reading lines at each IO.'
89
101
  config_param :read_lines_limit, :integer, default: 1000
102
+ desc 'The number of threads used to parse tailed files. Set to 1 to keep parsing synchronous; 2 or more enables worker parsing. The default is 1.'
103
+ config_param :num_threads, :integer, default: 1
90
104
  desc 'The number of reading bytes per second'
91
105
  config_param :read_bytes_limit_per_second, :size, default: -1
92
106
  desc 'The interval of flushing the buffer for multiline format'
@@ -95,8 +109,12 @@ module Fluent::Plugin
95
109
  config_param :emit_unmatched_lines, :bool, default: false
96
110
  desc 'Enable the additional watch timer.'
97
111
  config_param :enable_watch_timer, :bool, default: true
98
- desc 'Enable the stat watcher based on inotify.'
112
+ desc 'Enable stat watchers for individual files, using inotify where available.'
99
113
  config_param :enable_stat_watcher, :bool, default: true
114
+ desc 'Watch directories to discover files before refresh_interval. May use inotify regardless of enable_stat_watcher.'
115
+ config_param :enable_dir_watcher, :bool, default: false
116
+ desc 'Maximum number of directories watched at once. When exceeded, rotate the watched subset at each periodic refresh.'
117
+ config_param :dir_watcher_limit, :integer, default: 100
100
118
  desc 'The encoding of the input.'
101
119
  config_param :encoding, :string, default: nil
102
120
  desc "The original encoding of the input. If set, in_tail tries to encode string from this to 'encoding'. Must be set with 'encoding'. "
@@ -140,9 +158,22 @@ module Fluent::Plugin
140
158
 
141
159
  super
142
160
 
161
+ if @dir_watcher_limit <= 0
162
+ raise Fluent::ConfigError, "dir_watcher_limit must be greater than zero"
163
+ end
164
+
143
165
  if !@enable_watch_timer && !@enable_stat_watcher
144
166
  raise Fluent::ConfigError, "either of enable_watch_timer or enable_stat_watcher must be true"
145
167
  end
168
+ raise Fluent::ConfigError, "'num_threads' must be greater than 0" unless @num_threads.positive?
169
+
170
+ if @enable_dir_watcher && !@enable_stat_watcher
171
+ log.info(
172
+ 'enable_stat_watcher is disabled for files,' +
173
+ ' but enable_dir_watcher still uses directory stat watchers' +
174
+ ' and may use inotify'
175
+ )
176
+ end
146
177
 
147
178
  if @glob_policy == :always && @path_delimiter == ','
148
179
  raise Fluent::ConfigError, "cannot use glob_policy as always with the default path_delimiter: `,\""
@@ -187,11 +218,6 @@ module Fluent::Plugin
187
218
  configure_encoding
188
219
 
189
220
  @multiline_mode = parser_config["@type"].include?("multiline")
190
- @receive_handler = if @multiline_mode
191
- method(:parse_multilines)
192
- else
193
- method(:parse_singleline)
194
- end
195
221
  @file_perm = system_config.file_permission || Fluent::DEFAULT_FILE_PERMISSION
196
222
  @dir_perm = system_config.dir_permission || Fluent::DEFAULT_DIR_PERMISSION
197
223
  # parser is already created by parser helper
@@ -267,6 +293,9 @@ module Fluent::Plugin
267
293
  def start
268
294
  super
269
295
 
296
+ @line_feeder = build_line_feeder
297
+ configure_worker_mode if @num_threads > 1
298
+
270
299
  if @pos_file
271
300
  pos_file_dir = File.dirname(@pos_file)
272
301
  FileUtils.mkdir_p(pos_file_dir, mode: @dir_perm) unless Dir.exist?(pos_file_dir)
@@ -283,7 +312,8 @@ module Fluent::Plugin
283
312
  end
284
313
 
285
314
  refresh_watchers unless @skip_refresh_on_startup
286
- timer_execute(:in_tail_refresh_watchers, @refresh_interval, &method(:refresh_watchers))
315
+ update_dir_watchers
316
+ timer_execute(:in_tail_refresh_watchers, @refresh_interval, &method(:refresh_watchers_on_timer))
287
317
  end
288
318
 
289
319
  def stop
@@ -296,21 +326,39 @@ module Fluent::Plugin
296
326
 
297
327
  def shutdown
298
328
  @shutdown_start_time = Fluent::Clock.now
329
+ # Initial reads run as part of a refresh. Signal their timeout before
330
+ # waiting for the refresh, so a large file cannot delay shutdown forever.
331
+ (@tails.values + @tails_rotate_wait.keys).uniq.each do |tw|
332
+ tw.ready_to_shutdown(@shutdown_start_time)
333
+ end
334
+ # A refresh or a rotation handling may yield during file operations. Let it
335
+ # finish before detaching watchers or closing the position file.
336
+ @watcher_refresh_monitor.synchronize {}
337
+ # The event loop is still running at this point, so the watchers added to
338
+ # detect new files must be detached before the position file is closed:
339
+ # a change noticed by them would ask for refreshing the watchers, and
340
+ # #refresh_watchers writes the position file.
341
+ detach_dir_watchers
342
+ detach_refresh_timer
299
343
  # during shutdown phase, don't close io. It should be done in close after all threads are stopped. See close.
300
344
  stop_watchers(existence_path, immediate: true, remove_watcher: false)
301
- @tails_rotate_wait.keys.each do |tw|
302
- detach_watcher(tw, @tails_rotate_wait[tw][:ino], false)
345
+ @tails_rotate_wait.dup.each do |tw, rotation|
346
+ event_loop_detach(rotation[:timer]) if @worker_pool
347
+ detach_watcher(tw, rotation[:ino], false)
303
348
  end
304
- @pf_file.close if @pf_file
349
+ shutdown_worker_pool
350
+ @pf_file.close if @pf_file && !@worker_pool
305
351
 
306
352
  super
307
353
  end
308
354
 
309
355
  def close
310
356
  super
357
+ @worker_completion_watcher&.close
311
358
  # close file handles after all threads stopped (in #close of thread plugin helper)
312
359
  # It may be because we need to wait IOHandler.ready_to_shutdown()
313
360
  close_watcher_handles
361
+ @pf_file.close if @pf_file && !@pf_file.closed?
314
362
  end
315
363
 
316
364
  def have_read_capability?
@@ -318,174 +366,35 @@ module Fluent::Plugin
318
366
  @capability.have_capability?(:effective, :dac_override)
319
367
  end
320
368
 
321
- def extended_glob_pattern(path)
322
- path.include?('*') || path.include?('?') || /\[.*\]/.match?(path)
323
- end
324
-
325
- # Curly braces is not supported with default path_delimiter
326
- # because the default delimiter of path is ",".
327
- # This should be collided for wildcard pattern for curly braces and
328
- # be handled as an error on #configure.
329
- def use_glob?(path)
330
- if @glob_policy == :always
331
- # For future extensions, we decided to use `always' term to handle
332
- # regular expressions as much as possible.
333
- # This is because not using `true' as a returning value
334
- # when choosing :always here.
335
- extended_glob_pattern(path) || /\{.*,.*\}/.match?(path)
336
- elsif @glob_policy == :extended
337
- extended_glob_pattern(path)
338
- elsif @glob_policy == :backward_compatible
339
- path.include?('*')
340
- end
341
- end
342
-
343
- def expand_paths_raw
344
- date = Fluent::EventTime.now
345
- paths = []
346
- @paths.each { |path|
347
- path = if @path_timezone
348
- @path_formatters[path].call(date)
349
- else
350
- date.to_time.strftime(path)
351
- end
352
- if use_glob?(path)
353
- paths += Dir.glob(path).select { |p|
354
- begin
355
- is_file = !File.directory?(p)
356
- if (File.readable?(p) || have_read_capability?) && is_file
357
- if @limit_recently_modified && File.mtime(p) < (date.to_time - @limit_recently_modified)
358
- false
359
- else
360
- true
361
- end
362
- else
363
- if is_file
364
- unless @ignore_list.include?(p)
365
- log.warn "#{p} unreadable. It is excluded and would be examined next time."
366
- @ignore_list << p if @ignore_repeated_permission_error
367
- end
368
- end
369
- false
370
- end
371
- rescue Errno::ENOENT, Errno::EACCES
372
- log.debug { "#{p} is missing after refresh file list" }
373
- false
374
- end
375
- }
376
- else
377
- # When file is not created yet, Dir.glob returns an empty array. So just add when path is static.
378
- paths << path
379
- end
380
- }
381
- excluded = @exclude_path.map { |path|
382
- path = if @path_timezone
383
- @exclude_path_formatters[path].call(date)
384
- else
385
- date.to_time.strftime(path)
386
- end
387
- use_glob?(path) ? Dir.glob(path) : path
388
- }.flatten.uniq
389
- paths - excluded
390
- end
391
-
392
- def expand_paths
393
- # filter out non existing files, so in case pattern is without '*' we don't do unnecessary work
394
- hash = {}
395
- expand_paths_raw.select { |path|
396
- File.exist?(path)
397
- }.each { |path|
398
- # Even we just checked for existence, there is a race condition here as
399
- # of which stat() might fail with ENOENT. See #3224.
400
- begin
401
- target_info = TargetInfo.new(path, Fluent::FileWrapper.stat(path).ino)
402
- if @follow_inodes
403
- hash[target_info.ino] = target_info
404
- else
405
- hash[target_info.path] = target_info
406
- end
407
- rescue Errno::ENOENT, Errno::EACCES => e
408
- log.warn "expand_paths: stat() for #{path} failed with #{e.class.name}. Skip file."
409
- end
410
- }
411
- hash
412
- end
413
-
414
- def existence_path
415
- hash = {}
416
- @tails.each {|path, tw|
417
- if @follow_inodes
418
- hash[tw.ino] = TargetInfo.new(tw.path, tw.ino)
419
- else
420
- hash[tw.path] = TargetInfo.new(tw.path, tw.ino)
421
- end
422
- }
423
- hash
424
- end
425
-
426
- # in_tail with '*' path doesn't check rotation file equality at refresh phase.
427
- # So you should not use '*' path when your logs will be rotated by another tool.
428
- # It will cause log duplication after updated watch files.
429
- # In such case, you should separate log directory and specify two paths in path parameter.
430
- # e.g. path /path/to/dir/*,/path/to/rotated_logs/target_file
431
- def refresh_watchers
432
- target_paths_hash = expand_paths
433
- existence_paths_hash = existence_path
434
-
435
- log.debug {
436
- target_paths_str = target_paths_hash.collect { |key, target_info| target_info.path }.join(",")
437
- existence_paths_str = existence_paths_hash.collect { |key, target_info| target_info.path }.join(",")
438
- "tailing paths: target = #{target_paths_str} | existing = #{existence_paths_str}"
439
- }
440
-
441
- if !@follow_inodes
442
- need_unwatch_in_stop_watchers = true
443
- else
444
- # When using @follow_inodes, need this to unwatch the rotated old inode when it disappears.
445
- # After `update_watcher` detaches an old TailWatcher, the inode is lost from the `@tails`.
446
- # So that inode can't be contained in `removed_hash`, and can't be unwatched by `stop_watchers`.
447
- #
448
- # This logic may work for `@follow_inodes false` too.
449
- # Just limiting the case to suppress the impact to existing logics.
450
- @pf&.unwatch_removed_targets(target_paths_hash)
451
- need_unwatch_in_stop_watchers = false
452
- end
453
-
454
- removed_hash = existence_paths_hash.reject {|key, value| target_paths_hash.key?(key)}
455
- added_hash = target_paths_hash.reject {|key, value| existence_paths_hash.key?(key)}
456
-
457
- # If an existing TailWatcher already follows a target path with the different inode,
458
- # it means that the TailWatcher following the rotated file still exists. In this case,
459
- # `refresh_watcher` can't start the new TailWatcher for the new current file. So, we
460
- # should output a warning log in order to prevent silent collection stops.
461
- # (Such as https://github.com/fluent/fluentd/pull/4327)
462
- # (Usually, such a TailWatcher should be removed from `@tails` in `update_watcher`.)
463
- # (The similar warning may work for `@follow_inodes true` too. Just limiting the case
464
- # to suppress the impact to existing logics.)
465
- unless @follow_inodes
466
- target_paths_hash.each do |path, target|
467
- next unless @tails.key?(path)
468
- # We can't use `existence_paths_hash[path].ino` because it is from `TailWatcher.ino`,
469
- # which is very unstable parameter. (It can be `nil` or old).
470
- # So, we need to use `TailWatcher.pe.read_inode`.
471
- existing_watcher_inode = @tails[path].pe.read_inode
472
- if existing_watcher_inode != target.ino
473
- log.warn "Could not follow a file (inode: #{target.ino}) because an existing watcher for that filepath follows a different inode: #{existing_watcher_inode} (e.g. keeps watching a already rotated file). If you keep getting this message, please restart Fluentd.",
474
- filepath: target.path
475
- end
476
- end
477
- end
478
-
479
- stop_watchers(removed_hash, unwatched: need_unwatch_in_stop_watchers) unless removed_hash.empty?
480
- start_watchers(added_hash) unless added_hash.empty?
481
- @metrics.tracked.set(@tails.size)
482
- @startup = false if @startup
369
+ # TailInput builds its LineFeeder in start, after the configure of a plugin
370
+ # which inherits TailInput is done, so that the parameters set by that
371
+ # plugin are used.
372
+ def build_line_feeder
373
+ LineFeeder.new(
374
+ parser: @parser,
375
+ router_provider: method(:router),
376
+ log: log,
377
+ tag: @tag,
378
+ tag_prefix: @tag_prefix,
379
+ tag_suffix: @tag_suffix,
380
+ path_key: @path_key,
381
+ emit_unmatched_lines: @emit_unmatched_lines,
382
+ multiline_mode: @multiline_mode,
383
+ parse_handler: method(@multiline_mode ? :parse_multilines : :parse_singleline),
384
+ convert_handler: method(:convert_line_to_event),
385
+ flush_handler: method(:flush_buffer),
386
+ )
483
387
  end
484
388
 
485
389
  def setup_watcher(target_info, pe)
486
- line_buffer_timer_flusher = @multiline_mode ? TailWatcher::LineBufferTimerFlusher.new(log, @multiline_flush_interval, &method(:flush_buffer)) : nil
390
+ # A plugin may build a watcher before start. The LineFeeder of that
391
+ # moment has the same parameters, because the configure of every plugin
392
+ # is done before the watchers are built.
393
+ @line_feeder ||= build_line_feeder
394
+ file_feed = @line_feeder.new_file_feed(flush_interval: @multiline_flush_interval)
487
395
  read_from_head = !@startup || @read_from_head
488
- tw = TailWatcher.new(target_info, pe, log, read_from_head, @follow_inodes, method(:update_watcher), line_buffer_timer_flusher, method(:io_handler), @metrics)
396
+ tw = TailWatcher.new(target_info, pe, log, read_from_head, @follow_inodes, method(:update_watcher), file_feed, method(:io_handler), @metrics)
397
+ tw.ready_to_shutdown(@shutdown_start_time) if @shutdown_start_time
489
398
 
490
399
  if @enable_watch_timer
491
400
  tt = TimerTrigger.new(1, log) { tw.on_notify }
@@ -501,7 +410,7 @@ module Fluent::Plugin
501
410
  event_loop_attach(watcher)
502
411
  end
503
412
 
504
- tw.group_watcher = add_path_to_group_watcher(target_info.path)
413
+ tw.group_watcher = add_path_to_group_watcher(target_info.path, tw)
505
414
 
506
415
  tw
507
416
  rescue => e
@@ -541,6 +450,7 @@ module Fluent::Plugin
541
450
  end
542
451
 
543
452
  @tails[path] = tw
453
+ tw.ready_to_shutdown(@shutdown_start_time) if @shutdown_start_time
544
454
  tw.on_notify
545
455
  end
546
456
 
@@ -553,8 +463,6 @@ module Fluent::Plugin
553
463
 
554
464
  def stop_watchers(targets_info, immediate: false, unwatched: false, remove_watcher: true)
555
465
  targets_info.each_value { |target_info|
556
- remove_path_from_group_watcher(target_info.path)
557
-
558
466
  if remove_watcher
559
467
  tw = @tails.delete(target_info.path)
560
468
  else
@@ -572,6 +480,11 @@ module Fluent::Plugin
572
480
  end
573
481
 
574
482
  def close_watcher_handles
483
+ if @worker_pool
484
+ close_worker_watcher_handles
485
+ return
486
+ end
487
+
575
488
  @tails.keys.each do |path|
576
489
  tw = @tails.delete(path)
577
490
  if tw
@@ -583,22 +496,22 @@ module Fluent::Plugin
583
496
  end
584
497
  end
585
498
 
586
- # refresh_watchers calls @tails.keys so we don't use stop_watcher -> start_watcher sequence for safety.
587
499
  def update_watcher(tail_watcher, pe, new_inode)
500
+ @watcher_refresh_monitor.synchronize do
501
+ return if @shutdown_start_time
502
+
503
+ update_watcher_raw(tail_watcher, pe, new_inode)
504
+ end
505
+ end
506
+
507
+ # refresh_watchers calls @tails.keys so we don't use stop_watcher -> start_watcher sequence for safety.
508
+ def update_watcher_raw(tail_watcher, pe, new_inode)
588
509
  # TODO we should use another callback for this.
589
510
  # To suppress impact to existing logics, limit the case to `@follow_inodes`.
590
511
  # We may not need `@follow_inodes` condition.
591
512
  if @follow_inodes && new_inode.nil?
592
513
  # nil inode means the file disappeared, so we only need to stop it.
593
514
  @tails.delete(tail_watcher.path)
594
- # https://github.com/fluent/fluentd/pull/4237#issuecomment-1633358632
595
- # Because of this problem, log duplication can occur during `rotate_wait`.
596
- # Need to set `rotate_wait 0` for a workaround.
597
- # Duplication will occur if `refresh_watcher` is called during the `rotate_wait`.
598
- # In that case, `refresh_watcher` will add the new TailWatcher to tail the same target,
599
- # and it causes the log duplication.
600
- # (Other `detach_watcher_after_rotate_wait` may have the same problem.
601
- # We need the mechanism not to add duplicated TailWatcher with detaching TailWatcher.)
602
515
  detach_watcher_after_rotate_wait(tail_watcher, pe.read_inode)
603
516
  return
604
517
  end
@@ -625,10 +538,12 @@ module Fluent::Plugin
625
538
  # In this case, only we have to do is detaching the current tail_watcher.
626
539
  if new_position_entry.read_inode == 0
627
540
  @tails[path] = setup_watcher(new_target_info, new_position_entry)
541
+ @tails[path].ready_to_shutdown(@shutdown_start_time) if @shutdown_start_time
628
542
  @tails[path].on_notify
629
543
  end
630
544
  else
631
545
  @tails[path] = setup_watcher(new_target_info, pe)
546
+ @tails[path].ready_to_shutdown(@shutdown_start_time) if @shutdown_start_time
632
547
  @tails[path].on_notify
633
548
  end
634
549
 
@@ -645,11 +560,24 @@ module Fluent::Plugin
645
560
  end
646
561
  tw.detach(@shutdown_start_time)
647
562
 
563
+ # The final read in detach may itself submit a worker batch.
564
+ defer_worker_completion = tw.pending? || @worker_waiting_watchers&.include?(tw)
565
+ if close_io && defer_worker_completion
566
+ tw.defer_close
567
+ @worker_detached_watchers << tw unless @worker_detached_watchers.include?(tw)
568
+ end
648
569
  tw.close if close_io
649
570
 
650
- if @pf && tw.unwatched && (@follow_inode || !@tails[tw.path])
571
+ # A watcher waiting for `rotate_wait` cannot read once its path leaves the group watcher.
572
+ tw.group_watcher&.delete(tw.path, tw)
573
+
574
+ if @pf && tw.unwatched && (@follow_inodes || !@tails[tw.path])
651
575
  target_info = TargetInfo.new(tw.path, ino)
652
- @pf.unwatch(target_info)
576
+ if defer_worker_completion
577
+ @worker_deferred_unwatch[tw] = target_info
578
+ else
579
+ @pf.unwatch(target_info)
580
+ end
653
581
  end
654
582
  end
655
583
 
@@ -665,6 +593,7 @@ module Fluent::Plugin
665
593
  if @open_on_every_update
666
594
  # Detach now because it's already closed, waiting it doesn't make sense.
667
595
  detach_watcher(tw, ino)
596
+ return
668
597
  end
669
598
 
670
599
  return if @tails_rotate_wait[tw]
@@ -674,6 +603,15 @@ module Fluent::Plugin
674
603
  # Should ensure to read all contents before closing it, with keeping throttling.
675
604
  start_time_to_wait = Fluent::Clock.now
676
605
  timer = timer_execute(:in_tail_close_watcher, 1, repeat: true) do
606
+ # Without the watch timer, nothing else notifies a watcher whose path has been rotated away.
607
+ unless @enable_watch_timer
608
+ begin
609
+ tw.read_more
610
+ rescue => e
611
+ log.error e.to_s
612
+ log.error_backtrace
613
+ end
614
+ end
677
615
  elapsed = Fluent::Clock.now - start_time_to_wait
678
616
  if tw.eof? && elapsed >= @rotate_wait
679
617
  timer.detach
@@ -692,124 +630,6 @@ module Fluent::Plugin
692
630
  end
693
631
  end
694
632
 
695
- def flush_buffer(tw, buf)
696
- buf.chomp!
697
- @parser.parse(buf) { |time, record|
698
- if time && record
699
- tag = if @tag_prefix || @tag_suffix
700
- @tag_prefix + tw.tag + @tag_suffix
701
- else
702
- @tag
703
- end
704
- record[@path_key] ||= tw.path unless @path_key.nil?
705
- router.emit(tag, time, record)
706
- else
707
- if @emit_unmatched_lines
708
- record = { 'unmatched_line' => buf }
709
- record[@path_key] ||= tail_watcher.path unless @path_key.nil?
710
- tag = if @tag_prefix || @tag_suffix
711
- @tag_prefix + tw.tag + @tag_suffix
712
- else
713
- @tag
714
- end
715
- router.emit(tag, Fluent::EventTime.now, record)
716
- end
717
- log.warn "got incomplete line at shutdown from #{tw.path}: #{buf.inspect}"
718
- end
719
- }
720
- end
721
-
722
- # @return true if no error or unrecoverable error happens in emit action. false if got BufferOverflowError
723
- def receive_lines(lines, tail_watcher)
724
- es = @receive_handler.call(lines, tail_watcher)
725
- unless es.empty?
726
- tag = if @tag_prefix || @tag_suffix
727
- @tag_prefix + tail_watcher.tag + @tag_suffix
728
- else
729
- @tag
730
- end
731
- begin
732
- router.emit_stream(tag, es)
733
- rescue Fluent::Plugin::Buffer::BufferOverflowError
734
- return false
735
- rescue
736
- # ignore non BufferQueueLimitError errors because in_tail can't recover. Engine shows logs and backtraces.
737
- return true
738
- end
739
- end
740
-
741
- return true
742
- end
743
-
744
- def convert_line_to_event(line, es, tail_watcher)
745
- begin
746
- line.chomp! # remove \n
747
- @parser.parse(line) { |time, record|
748
- if time && record
749
- record[@path_key] ||= tail_watcher.path unless @path_key.nil?
750
- es.add(time, record)
751
- else
752
- if @emit_unmatched_lines
753
- record = {'unmatched_line' => line}
754
- record[@path_key] ||= tail_watcher.path unless @path_key.nil?
755
- es.add(Fluent::EventTime.now, record)
756
- end
757
- log.warn { "pattern not matched: #{line.inspect}" }
758
- end
759
- }
760
- rescue => e
761
- log.warn 'invalid line found', file: tail_watcher.path, line: line, error: e.to_s
762
- log.debug_backtrace(e.backtrace)
763
- end
764
- end
765
-
766
- def parse_singleline(lines, tail_watcher)
767
- es = Fluent::MultiEventStream.new
768
- lines.each { |line|
769
- convert_line_to_event(line, es, tail_watcher)
770
- }
771
- es
772
- end
773
-
774
- # No need to check if line_buffer_timer_flusher is nil, since line_buffer_timer_flusher should exist
775
- def parse_multilines(lines, tail_watcher)
776
- lb = tail_watcher.line_buffer_timer_flusher.line_buffer
777
- es = Fluent::MultiEventStream.new
778
- if @parser.has_firstline?
779
- tail_watcher.line_buffer_timer_flusher.reset_timer
780
- lines.each { |line|
781
- if @parser.firstline?(line)
782
- if lb
783
- convert_line_to_event(lb, es, tail_watcher)
784
- end
785
- lb = line
786
- else
787
- if lb.nil?
788
- if @emit_unmatched_lines
789
- convert_line_to_event(line, es, tail_watcher)
790
- end
791
- log.warn "got incomplete line before first line from #{tail_watcher.path}: #{line.inspect}"
792
- else
793
- lb << line
794
- end
795
- end
796
- }
797
- else
798
- lb ||= ''
799
- lines.each do |line|
800
- lb << line
801
- @parser.parse(lb) { |time, record|
802
- if time && record
803
- convert_line_to_event(lb, es, tail_watcher)
804
- lb = ''
805
- end
806
- }
807
- end
808
- end
809
- tail_watcher.line_buffer_timer_flusher.line_buffer = lb
810
- es
811
- end
812
-
813
633
  def statistics
814
634
  stats = super
815
635
 
@@ -822,6 +642,13 @@ module Fluent::Plugin
822
642
  'tracked_file_count' => @metrics.tracked.get,
823
643
  })
824
644
  }
645
+ if @worker_pool
646
+ stats['input'].merge!(
647
+ 'worker_pending_batch_count' => @worker_metrics[:pending_batches].get,
648
+ 'worker_pending_bytes' => @worker_metrics[:pending_bytes].get,
649
+ 'worker_parse_error_count' => @worker_metrics[:parse_errors].get,
650
+ )
651
+ end
825
652
  stats
826
653
  end
827
654
 
@@ -852,556 +679,238 @@ module Fluent::Plugin
852
679
  &method(:receive_lines)
853
680
  )
854
681
  end
682
+ end
855
683
 
856
- class StatWatcher < Coolio::StatWatcher
857
- def initialize(path, log, &callback)
858
- @callback = callback
859
- @log = log
860
- super(path)
861
- end
684
+ # Reopen TailInput to keep worker-mode setup and lifecycle helpers grouped
685
+ # separately from the synchronous file-watching implementation above.
686
+ class TailInput
687
+ WORKER_COMPATIBILITY = WorkerCompatibility.new(self)
688
+ WORKER_PARSER_TYPES = %w[
689
+ none regexp json csv tsv ltsv msgpack apache apache2 apache_error nginx syslog
690
+ ].freeze
691
+ WORKER_QUEUE_BYTES = 64 * 1024 * 1024
692
+ WorkerBatch = Struct.new(:watcher, :lines)
862
693
 
863
- def on_change(prev, cur)
864
- @callback.call
865
- rescue
866
- @log.error $!.to_s
867
- @log.error_backtrace
868
- end
869
- end
870
-
871
- class TimerTrigger < Coolio::TimerWatcher
872
- def initialize(interval, log, &callback)
873
- @log = log
874
- @callback = callback
875
- super(interval, true)
876
- end
694
+ private
877
695
 
878
- def on_timer
879
- @callback.call
880
- rescue => e
881
- @log.error e.to_s
882
- @log.error_backtrace
696
+ def configure_worker_mode
697
+ incompatibility_reasons = worker_incompatibility_reasons
698
+ @worker_compatible = incompatibility_reasons.empty?
699
+ if @worker_compatible
700
+ setup_worker_pool
701
+ log.info "in_tail worker parsing enabled with #{@num_threads} threads"
702
+ else
703
+ log.warn "in_tail worker parsing is unavailable; using synchronous parsing (#{incompatibility_reasons.join(', ')})"
883
704
  end
884
705
  end
885
706
 
886
- class TailWatcher
887
- def initialize(target_info, pe, log, read_from_head, follow_inodes, update_watcher, line_buffer_timer_flusher, io_handler_build, metrics)
888
- @path = target_info.path
889
- @ino = target_info.ino
890
- @pe = pe || MemoryPositionEntry.new
891
- @read_from_head = read_from_head
892
- @follow_inodes = follow_inodes
893
- @update_watcher = update_watcher
894
- @log = log
895
- @rotate_handler = RotateHandler.new(log, &method(:on_rotate))
896
- @line_buffer_timer_flusher = line_buffer_timer_flusher
897
- @io_handler = nil
898
- @io_handler_build = io_handler_build
899
- @metrics = metrics
900
- @watchers = []
901
- end
707
+ def worker_incompatibility_reasons
708
+ reasons = WORKER_COMPATIBILITY.incompatible_hooks(self).map { |hook| "overridden hook: #{hook}" }
709
+ reasons << 'multiline parsing is enabled' if @multiline_mode
710
+ reasons << 'open_on_every_update is enabled' if @open_on_every_update
711
+ parser_type = @parser_configs.first[:@type]
712
+ reasons << "unsupported parser: #{parser_type}" unless WORKER_PARSER_TYPES.include?(parser_type)
713
+ parser_usage = @parser_configs.first.usage
714
+ reasons << 'the configured parser instance was replaced' unless @parser.equal?(@_parsers[parser_usage])
715
+ reasons
716
+ end
902
717
 
903
- attr_reader :path, :ino
904
- attr_reader :pe
905
- attr_reader :line_buffer_timer_flusher
906
- attr_accessor :unwatched # This is used for removing position entry from PositionFile
907
- attr_reader :watchers
908
- attr_accessor :group_watcher
718
+ def create_worker_parsers(num_threads)
719
+ return [] unless @worker_compatible
720
+ raise ArgumentError, 'worker count must be positive' unless num_threads.positive?
909
721
 
910
- def tag
911
- @parsed_tag ||= @path.tr('/', '.').squeeze('.').gsub(/^\./, '')
722
+ parser_config = @parser_configs.first.corresponding_config_element
723
+ Array.new(num_threads) do |index|
724
+ usage = "__in_tail_worker_#{object_id}_#{index}"
725
+ parser_create(usage: usage, conf: parser_config)
912
726
  end
727
+ end
913
728
 
914
- def register_watcher(watcher)
915
- @watchers << watcher
916
- end
729
+ def setup_worker_pool
730
+ @worker_metrics = {
731
+ pending_batches: metrics_create(namespace: 'fluentd', subsystem: 'input', name: 'worker_pending_batches',
732
+ help_text: 'Number of worker batches awaiting completion or acknowledgment',
733
+ prefer_gauge: true),
734
+ pending_bytes: metrics_create(namespace: 'fluentd', subsystem: 'input', name: 'worker_pending_bytes',
735
+ help_text: 'Estimated bytes accounted for by pending worker batches',
736
+ prefer_gauge: true),
737
+ parse_errors: metrics_create(namespace: 'fluentd', subsystem: 'input', name: 'worker_parse_errors_total',
738
+ help_text: 'Total number of worker parse errors'),
739
+ }
740
+ @worker_parsers = create_worker_parsers(@num_threads)
741
+ @worker_line_feeders = @worker_parsers.map { |parser| build_worker_line_feeder(parser) }
742
+ @worker_waiting_watchers = []
743
+ @worker_deferred_unwatch = {}
744
+ @worker_detached_watchers = []
745
+ @worker_completion_mutex = Mutex.new
746
+ @worker_completions_stopped = false
747
+ @worker_completion_watcher = WorkerCompletionWatcher.new(log: log) { process_worker_completions }
748
+ event_loop_attach(@worker_completion_watcher)
749
+ @worker_pool = WorkerPool.new(
750
+ num_threads: @num_threads,
751
+ task_limit: @num_threads * 2,
752
+ byte_limit: WORKER_QUEUE_BYTES,
753
+ thread_create: ->(title, &block) { Thread.new { Thread.current.name = title.to_s if Thread.current.respond_to?(:name=); block.call } },
754
+ notify: @worker_completion_watcher.method(:signal)
755
+ ) do |index, batch|
756
+ @worker_line_feeders[index].parse(batch.lines.map(&:dup), batch.watcher)
757
+ end
758
+ @worker_pool.start
759
+ rescue
760
+ event_loop_detach(@worker_completion_watcher) if @worker_completion_watcher&.attached?
761
+ @worker_completion_watcher&.close
762
+ raise
763
+ end
764
+
765
+ def build_worker_line_feeder(parser)
766
+ LineFeeder.new(
767
+ parser: parser,
768
+ router_provider: method(:router),
769
+ log: log,
770
+ tag: @tag,
771
+ tag_prefix: @tag_prefix,
772
+ tag_suffix: @tag_suffix,
773
+ path_key: @path_key,
774
+ emit_unmatched_lines: @emit_unmatched_lines,
775
+ multiline_mode: false
776
+ )
777
+ end
917
778
 
918
- def detach(shutdown_start_time = nil)
919
- if @io_handler
920
- @io_handler.ready_to_shutdown(shutdown_start_time)
921
- @io_handler.on_notify
922
- end
923
- @line_buffer_timer_flusher&.close(self)
924
- end
779
+ def async_receive_lines(lines, watcher)
780
+ return @line_feeder.feed_lines(lines, watcher) unless @worker_pool.statistics[:state] == :running
925
781
 
926
- def close
927
- if @io_handler
928
- @io_handler.close
929
- @io_handler = nil
930
- end
782
+ bytes = lines.sum(&:bytesize) * 3
783
+ batch = WorkerBatch.new(watcher, lines)
784
+ if @worker_pool.submit(watcher, batch, bytes: bytes)
785
+ update_worker_metrics
786
+ @worker_waiting_watchers.delete(watcher)
787
+ TailWatcher::IOHandler::ASYNC_PENDING
788
+ else
789
+ @worker_waiting_watchers << watcher unless @worker_waiting_watchers.include?(watcher)
790
+ false
931
791
  end
792
+ end
932
793
 
933
- def eof?
934
- @io_handler.nil? || @io_handler.eof?
794
+ def process_worker_completions
795
+ @worker_completion_mutex.synchronize do
796
+ drain_worker_completions(resume: true) unless @worker_completions_stopped
935
797
  end
798
+ end
936
799
 
937
- def on_notify
800
+ def drain_worker_completions(resume:)
801
+ while (result = @worker_pool.next_result)
938
802
  begin
939
- stat = Fluent::FileWrapper.stat(@path)
940
- rescue Errno::ENOENT, Errno::EACCES
941
- # moved or deleted
942
- stat = nil
803
+ complete_worker_result(result, resume: resume)
804
+ rescue => error
805
+ log.error 'unexpected error while completing in_tail worker batch', error: error
806
+ log.error_backtrace
943
807
  end
944
-
945
- @rotate_handler.on_notify(stat) if @rotate_handler
946
- @line_buffer_timer_flusher.on_notify(self) if @line_buffer_timer_flusher
947
- @io_handler.on_notify if @io_handler
948
808
  end
949
809
 
950
- def on_rotate(stat)
951
- if @io_handler.nil?
952
- if stat
953
- # first time
954
- fsize = stat.size
955
- inode = stat.ino
956
-
957
- last_inode = @pe.read_inode
958
- if inode == last_inode
959
- # rotated file has the same inode number with the last file.
960
- # assuming following situation:
961
- # a) file was once renamed and backed, or
962
- # b) symlink or hardlink to the same file is recreated
963
- # in either case of a and b, seek to the saved position
964
- # c) file was once renamed, truncated and then backed
965
- # in this case, consider it truncated
966
- @pe.update(inode, 0) if fsize < @pe.read_pos
967
- elsif last_inode != 0
968
- # this is FilePositionEntry and fluentd once started.
969
- # read data from the head of the rotated file.
970
- # logs never duplicate because this file is a rotated new file.
971
- @pe.update(inode, 0)
972
- else
973
- # this is MemoryPositionEntry or this is the first time fluentd started.
974
- # seek to the end of the any files.
975
- # logs may duplicate without this seek because it's not sure the file is
976
- # existent file or rotated new file.
977
- pos = @read_from_head ? 0 : fsize
978
- @pe.update(inode, pos)
979
- end
980
- @io_handler = io_handler
981
- else
982
- @io_handler = NullIOHandler.new
983
- end
984
- else
985
- watcher_needs_update = false
986
-
987
- if stat
988
- inode = stat.ino
989
- if inode == @pe.read_inode # truncated
990
- @pe.update_pos(0)
991
- @io_handler.close
992
- elsif !@io_handler.opened? # There is no previous file. Reuse TailWatcher
993
- @pe.update(inode, 0)
994
- else # file is rotated and new file found
995
- watcher_needs_update = true
996
- # Handle the old log file before renewing TailWatcher [fluentd#1055]
997
- @io_handler.on_notify
998
- end
999
- else # file is rotated and new file not found
1000
- # Clear RotateHandler to avoid duplicated file watch in same path.
1001
- @rotate_handler = nil
1002
- watcher_needs_update = true
1003
- end
810
+ return unless resume
1004
811
 
1005
- if watcher_needs_update
1006
- if @follow_inodes
1007
- # If stat is nil (file not present), NEED to stop and discard this watcher.
1008
- # When the file is disappeared but is resurrected soon, then `#refresh_watcher`
1009
- # can't recognize this TailWatcher needs to be stopped.
1010
- # This can happens when the file is rotated.
1011
- # If a notify comes before the new file for the path is created during rotation,
1012
- # then it appears as if the file was resurrected once it disappeared.
1013
- # Don't want to swap state because we need latest read offset in pos file even after rotate_wait
1014
- @update_watcher.call(self, @pe, stat&.ino)
1015
- else
1016
- # Permit to handle if stat is nil (file not present).
1017
- # If a file is mv-ed and a new file is created during
1018
- # calling `#refresh_watchers`s, and `#refresh_watchers` won't run `#start_watchers`
1019
- # and `#stop_watchers()` for the path because `target_paths_hash`
1020
- # always contains the path.
1021
- @update_watcher.call(self, swap_state(@pe), stat&.ino)
1022
- end
1023
- else
1024
- @log.info "detected rotation of #{@path}"
1025
- @io_handler = io_handler
1026
- end
1027
- @metrics.rotated.inc
812
+ waiting = @worker_waiting_watchers
813
+ @worker_waiting_watchers = []
814
+ waiting.each do |watcher|
815
+ begin
816
+ watcher.detached? ? watcher.read_more : watcher.on_notify
817
+ rescue => error
818
+ log.error 'unexpected error while retrying in_tail worker batch', error: error
819
+ log.error_backtrace
820
+ ensure
821
+ finish_worker_deferred_close(watcher)
1028
822
  end
1029
823
  end
824
+ end
1030
825
 
1031
- def io_handler
1032
- @io_handler_build.call(self, @path)
1033
- end
1034
-
1035
- def swap_state(pe)
1036
- # Use MemoryPositionEntry for rotated file temporary
1037
- mpe = MemoryPositionEntry.new
1038
- mpe.update(pe.read_inode, pe.read_pos)
1039
- @pe = mpe
1040
- pe # This pe will be updated in on_rotate after TailWatcher is initialized
1041
- end
1042
-
1043
- class FIFO
1044
- def initialize(encoding, log, max_line_size=nil, encoding_to_convert=nil)
1045
- @buffer = ''.force_encoding(encoding)
1046
- @eol = "\n".encode(encoding).freeze
1047
- @encoding_to_convert = encoding_to_convert
1048
- @max_line_size = max_line_size
1049
- @skip_current_line = false
1050
- @skipping_current_line_bytesize = 0
1051
- @log = log
1052
- end
1053
-
1054
- attr_reader :buffer, :max_line_size
1055
-
1056
- def <<(chunk)
1057
- @buffer << chunk
1058
- end
1059
-
1060
- def convert(s)
1061
- if @encoding_to_convert
1062
- s.encode!(@encoding_to_convert)
826
+ def complete_worker_result(result, resume:)
827
+ batch = result.task.payload
828
+ emitted = false
829
+ begin
830
+ if result.error
831
+ @worker_metrics[:parse_errors].inc
832
+ if resume
833
+ log.warn 'worker parsing failed; retrying synchronously', path: batch.watcher.path, error: result.error
834
+ emitted = @line_feeder.feed_lines(batch.lines, batch.watcher)
1063
835
  else
1064
- s
836
+ log.warn 'worker parsing failed during shutdown; leaving position unchanged', path: batch.watcher.path, error: result.error
1065
837
  end
1066
- rescue
1067
- s.encode!(@encoding_to_convert, :invalid => :replace, :undef => :replace)
838
+ else
839
+ emitted = @line_feeder.emit(result.value, batch.watcher)
1068
840
  end
1069
-
1070
- def read_lines(lines)
1071
- idx = @buffer.index(@eol)
1072
- has_skipped_line = false
1073
-
1074
- until idx.nil?
1075
- # Using freeze and slice is faster than slice!
1076
- # See https://github.com/fluent/fluentd/pull/2527
1077
- @buffer.freeze
1078
- slice_position = idx + 1
1079
- rbuf = @buffer.slice(0, slice_position)
1080
- @buffer = @buffer.slice(slice_position, @buffer.size - slice_position)
1081
- idx = @buffer.index(@eol)
1082
-
1083
- is_long_line = @max_line_size && (
1084
- @skip_current_line || rbuf.bytesize > @max_line_size
1085
- )
1086
-
1087
- if is_long_line
1088
- @log.warn "received line length is longer than #{@max_line_size}"
1089
- if @skip_current_line
1090
- @log.debug("The continuing line is finished. Finally discarded data: ") { convert(rbuf).chomp }
1091
- else
1092
- @log.debug("skipped line: ") { convert(rbuf).chomp }
1093
- end
1094
- has_skipped_line = true
1095
- @skip_current_line = false
1096
- @skipping_current_line_bytesize = 0
1097
- next
1098
- end
1099
-
1100
- lines << convert(rbuf)
1101
- end
1102
-
1103
- is_long_current_line = @max_line_size && (
1104
- @skip_current_line || @buffer.bytesize > @max_line_size
1105
- )
1106
-
1107
- if is_long_current_line
1108
- @log.debug(
1109
- "The continuing current line length is longer than #{@max_line_size}." +
1110
- " The received data will be discarded until this line is finished." +
1111
- " Discarded data: "
1112
- ) { convert(@buffer).chomp }
1113
- @skip_current_line = true
1114
- @skipping_current_line_bytesize += @buffer.bytesize
1115
- @buffer.clear
841
+ ensure
842
+ begin
843
+ @worker_pool.acknowledge(result)
844
+ update_worker_metrics
845
+ ensure
846
+ begin
847
+ batch.watcher.complete_async(emitted, resume: resume)
848
+ ensure
849
+ finish_worker_deferred_close(batch.watcher)
1116
850
  end
1117
-
1118
- return has_skipped_line
1119
- end
1120
-
1121
- def reading_bytesize
1122
- return @skipping_current_line_bytesize if @skip_current_line
1123
- @buffer.bytesize
1124
851
  end
1125
852
  end
853
+ end
1126
854
 
1127
- class IOHandler
1128
- BYTES_TO_READ = 8192
1129
- SHUTDOWN_TIMEOUT = 5
1130
-
1131
- attr_accessor :shutdown_timeout
1132
-
1133
- def initialize(watcher, path:, read_lines_limit:, read_bytes_limit_per_second:, max_line_size: nil, log:, open_on_every_update:, encoding: Encoding::ASCII_8BIT, encoding_to_convert: nil, metrics:, &receive_lines)
1134
- @watcher = watcher
1135
- @path = path
1136
- @read_lines_limit = read_lines_limit
1137
- @read_bytes_limit_per_second = read_bytes_limit_per_second
1138
- @receive_lines = receive_lines
1139
- @open_on_every_update = open_on_every_update
1140
- @encoding = encoding
1141
- @fifo = FIFO.new(encoding, log, max_line_size, encoding_to_convert)
1142
- @lines = []
1143
- @io = nil
1144
- @notify_mutex = Mutex.new
1145
- @log = log
1146
- @start_reading_time = nil
1147
- @number_bytes_read = 0
1148
- @shutdown_start_time = nil
1149
- @shutdown_timeout = SHUTDOWN_TIMEOUT
1150
- @shutdown_mutex = Mutex.new
1151
- @eof = false
1152
- @metrics = metrics
1153
-
1154
- @log.info "following tail of #{@path}"
1155
- end
1156
-
1157
- def group_watcher
1158
- @watcher.group_watcher
1159
- end
1160
-
1161
- def on_notify
1162
- @notify_mutex.synchronize { handle_notify }
1163
- end
1164
-
1165
- def ready_to_shutdown(shutdown_start_time = nil)
1166
- @shutdown_mutex.synchronize {
1167
- @shutdown_start_time =
1168
- shutdown_start_time || Fluent::Clock.now
1169
- }
1170
- end
1171
-
1172
- def close
1173
- if @io && !@io.closed?
1174
- @io.close
1175
- @io = nil
1176
- @metrics.closed.inc
1177
- end
1178
- end
1179
-
1180
- def opened?
1181
- !!@io
1182
- end
1183
-
1184
- def eof?
1185
- @eof
1186
- end
1187
-
1188
- private
1189
-
1190
- def limit_bytes_per_second_reached?
1191
- return false if @read_bytes_limit_per_second < 0 # not enabled by conf
1192
- return false if @number_bytes_read < @read_bytes_limit_per_second
1193
-
1194
- @start_reading_time ||= Fluent::Clock.now
1195
- time_spent_reading = Fluent::Clock.now - @start_reading_time
1196
- @log.debug("time_spent_reading: #{time_spent_reading} #{ @watcher.path}")
1197
-
1198
- if time_spent_reading < 1
1199
- true
1200
- else
1201
- @start_reading_time = nil
1202
- @number_bytes_read = 0
1203
- false
1204
- end
1205
- end
1206
-
1207
- def should_shutdown_now?
1208
- # Ensure to read all remaining lines, but abort immediately if it
1209
- # seems to take too long time.
1210
- @shutdown_mutex.synchronize {
1211
- return false if @shutdown_start_time.nil?
1212
- return Fluent::Clock.now - @shutdown_start_time > @shutdown_timeout
1213
- }
1214
- end
855
+ def finish_worker_deferred_close(watcher)
856
+ return if watcher.pending? || @worker_waiting_watchers.include?(watcher)
1215
857
 
1216
- def handle_notify
1217
- if limit_bytes_per_second_reached? || group_watcher&.limit_lines_reached?(@path)
1218
- @metrics.throttled.inc
1219
- return
1220
- end
858
+ # Match synchronous detach on failed emit or a throttled final read:
859
+ # close the handle, leaving uncommitted data for a future watch.
860
+ watcher.close_if_ready(force: watcher.detached?)
861
+ @worker_detached_watchers.delete(watcher)
1221
862
 
1222
- with_io do |io|
1223
- iobuf = ''.force_encoding(@encoding)
1224
- begin
1225
- read_more = false
1226
- has_skipped_line = false
1227
-
1228
- if !io.nil? && @lines.empty?
1229
- begin
1230
- while true
1231
- @start_reading_time ||= Fluent::Clock.now
1232
- group_watcher&.update_reading_time(@path)
1233
-
1234
- data = io.readpartial(BYTES_TO_READ, iobuf)
1235
- @eof = false
1236
- @number_bytes_read += data.bytesize
1237
- @fifo << data
1238
-
1239
- n_lines_before_read = @lines.size
1240
- has_skipped_line = @fifo.read_lines(@lines) || has_skipped_line
1241
- group_watcher&.update_lines_read(@path, @lines.size - n_lines_before_read)
1242
-
1243
- group_watcher_limit = group_watcher&.limit_lines_reached?(@path)
1244
- @log.debug "Reading Limit exceeded #{@path} #{group_watcher.number_lines_read}" if group_watcher_limit
1245
-
1246
- if group_watcher_limit || limit_bytes_per_second_reached? || should_shutdown_now?
1247
- # Just get out from tailing loop.
1248
- @metrics.throttled.inc if group_watcher_limit || limit_bytes_per_second_reached?
1249
- read_more = false
1250
- break
1251
- end
1252
-
1253
- if @lines.size >= @read_lines_limit
1254
- # not to use too much memory in case the file is very large
1255
- read_more = true
1256
- break
1257
- end
1258
- end
1259
- rescue EOFError
1260
- @eof = true
1261
- ensure
1262
- iobuf.clear
1263
- end
1264
- end
1265
-
1266
- if @lines.empty?
1267
- @watcher.pe.update_pos(io.pos - @fifo.reading_bytesize) if has_skipped_line
1268
- else
1269
- if @receive_lines.call(@lines, @watcher)
1270
- @watcher.pe.update_pos(io.pos - @fifo.reading_bytesize)
1271
- @lines.clear
1272
- else
1273
- read_more = false
1274
- end
1275
- end
1276
- end while read_more
1277
- end
1278
- end
863
+ target_info = @worker_deferred_unwatch.delete(watcher)
864
+ @pf.unwatch(target_info) if target_info && @pf
865
+ end
1279
866
 
1280
- def open
1281
- io = Fluent::FileWrapper.open(@path)
1282
- io.seek(@watcher.pe.read_pos + @fifo.reading_bytesize)
1283
- @metrics.opened.inc
1284
- io
1285
- rescue RangeError
1286
- io.close if io
1287
- raise WatcherSetupError, "seek error with #{@path}: file position = #{@watcher.pe.read_pos.to_s(16)}, reading bytesize = #{@fifo.reading_bytesize.to_s(16)}"
1288
- rescue Errno::EACCES => e
1289
- @log.warn "#{e}"
1290
- nil
1291
- rescue Errno::ENOENT
1292
- nil
1293
- end
867
+ def update_worker_metrics
868
+ statistics = @worker_pool.statistics
869
+ @worker_metrics[:pending_batches].set(statistics[:tasks])
870
+ @worker_metrics[:pending_bytes].set(statistics[:bytes])
871
+ end
1294
872
 
1295
- def with_io
1296
- if @open_on_every_update
1297
- io = open
1298
- begin
1299
- yield io
1300
- ensure
1301
- io.close unless io.nil?
1302
- end
1303
- else
1304
- @io ||= open
1305
- yield @io
1306
- @eof = true if @io.nil?
1307
- end
1308
- rescue WatcherSetupError => e
1309
- close
1310
- @eof = true
1311
- raise e
1312
- rescue
1313
- @log.error $!.to_s
1314
- @log.error_backtrace
1315
- close
1316
- @eof = true
1317
- end
873
+ def close_worker_watcher_handles
874
+ watchers = (@tails.values + @tails_rotate_wait.keys + @worker_detached_watchers + @worker_deferred_unwatch.keys).uniq
875
+ watchers.each(&:force_close)
876
+ @tails.clear
877
+ @worker_detached_watchers.clear
878
+ @worker_deferred_unwatch.each_value do |target_info|
879
+ @pf.unwatch(target_info) if @pf
1318
880
  end
881
+ @worker_deferred_unwatch.clear
882
+ end
1319
883
 
1320
- class NullIOHandler
1321
- def initialize
1322
- end
1323
-
1324
- def io
1325
- end
1326
-
1327
- def on_notify
1328
- end
1329
-
1330
- def close
1331
- end
884
+ def shutdown_worker_pool
885
+ return unless @worker_pool
1332
886
 
1333
- def opened?
1334
- false
1335
- end
1336
-
1337
- def eof?
1338
- true
1339
- end
887
+ deadline = Fluent::Clock.now + TailWatcher::IOHandler::SHUTDOWN_TIMEOUT
888
+ while worker_work_pending?
889
+ break if Fluent::Clock.now >= deadline
890
+ sleep 0.01
1340
891
  end
1341
-
1342
- class RotateHandler
1343
- def initialize(log, &on_rotate)
1344
- @log = log
1345
- @inode = nil
1346
- @fsize = -1 # first
1347
- @on_rotate = on_rotate
1348
- end
1349
-
1350
- def on_notify(stat)
1351
- if stat.nil?
1352
- inode = nil
1353
- fsize = 0
1354
- else
1355
- inode = stat.ino
1356
- fsize = stat.size
1357
- end
1358
-
1359
- if @inode != inode || fsize < @fsize
1360
- @on_rotate.call(stat)
1361
- end
1362
- @inode = inode
1363
- @fsize = fsize
1364
- rescue
1365
- @log.error $!.to_s
1366
- @log.error_backtrace
1367
- end
892
+ if worker_work_pending?
893
+ log.warn 'in_tail worker pool did not drain before shutdown timeout'
1368
894
  end
895
+ event_loop_detach(@worker_completion_watcher)
896
+ # Detach alone does not wait for a callback already running. Serialize
897
+ # the handover so every result is handled before output shutdown starts.
898
+ @worker_completion_mutex.synchronize do
899
+ @worker_completions_stopped = true
900
+ @worker_pool.stop
901
+ end
902
+ unless @worker_pool.join(timeout: TailWatcher::IOHandler::SHUTDOWN_TIMEOUT)
903
+ log.warn 'in_tail worker threads did not stop before timeout'
904
+ @worker_pool.kill
905
+ end
906
+ @worker_completion_mutex.synchronize do
907
+ drain_worker_completions(resume: false)
908
+ end
909
+ end
1369
910
 
1370
- class LineBufferTimerFlusher
1371
- attr_accessor :line_buffer
1372
-
1373
- def initialize(log, flush_interval, &flush_method)
1374
- @log = log
1375
- @flush_interval = flush_interval
1376
- @flush_method = flush_method
1377
- @start = nil
1378
- @line_buffer = nil
1379
- end
1380
-
1381
- def on_notify(tw)
1382
- unless @start && @flush_method
1383
- return
1384
- end
1385
-
1386
- if Time.now - @start >= @flush_interval
1387
- @flush_method.call(tw, @line_buffer) if @line_buffer
1388
- @line_buffer = nil
1389
- @start = nil
1390
- end
1391
- end
1392
-
1393
- def close(tw)
1394
- return unless @line_buffer
1395
-
1396
- @flush_method.call(tw, @line_buffer)
1397
- @line_buffer = nil
1398
- end
1399
-
1400
- def reset_timer
1401
- return unless @flush_interval
1402
-
1403
- @start = Time.now
1404
- end
911
+ def worker_work_pending?
912
+ @worker_completion_mutex.synchronize do
913
+ @worker_pool.statistics[:tasks].positive? || !@worker_waiting_watchers.empty?
1405
914
  end
1406
915
  end
1407
916
  end