fluentd 1.19.4 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +46 -0
- data/CHANGELOG.md +83 -0
- data/GithubWorkflow.md +1 -1
- data/MAINTAINERS.md +2 -2
- data/README.md +3 -2
- data/Rakefile +2 -1
- data/SECURITY.md +26 -1
- data/lib/fluent/command/cat.rb +2 -2
- data/lib/fluent/compat/exec_util.rb +20 -5
- data/lib/fluent/config/literal_parser.rb +4 -3
- data/lib/fluent/config/yaml_parser/loader.rb +12 -1
- data/lib/fluent/config/yaml_parser/parser.rb +14 -13
- data/lib/fluent/event.rb +37 -0
- data/lib/fluent/load.rb +0 -1
- data/lib/fluent/log.rb +2 -2
- data/lib/fluent/plugin/buf_file.rb +2 -2
- data/lib/fluent/plugin/buf_file_single.rb +6 -2
- data/lib/fluent/plugin/buffer/file_chunk.rb +23 -10
- data/lib/fluent/plugin/buffer/file_single_chunk.rb +23 -10
- data/lib/fluent/plugin/buffer.rb +102 -36
- data/lib/fluent/plugin/formatter_out_file.rb +3 -4
- data/lib/fluent/plugin/in_exec.rb +6 -4
- data/lib/fluent/plugin/in_forward.rb +39 -29
- data/lib/fluent/plugin/in_object_space.rb +2 -2
- data/lib/fluent/plugin/in_syslog.rb +2 -2
- data/lib/fluent/plugin/in_tail/compatibility.rb +111 -0
- data/lib/fluent/plugin/in_tail/dir_watcher.rb +713 -0
- data/lib/fluent/plugin/in_tail/group_watch.rb +13 -13
- data/lib/fluent/plugin/in_tail/io_handler.rb +391 -0
- data/lib/fluent/plugin/in_tail/line_feeder.rb +230 -0
- data/lib/fluent/plugin/in_tail/stat_watcher.rb +52 -0
- data/lib/fluent/plugin/in_tail/tail_watcher.rb +276 -0
- data/lib/fluent/plugin/in_tail/worker_pool.rb +298 -0
- data/lib/fluent/plugin/in_tail.rb +324 -815
- data/lib/fluent/plugin/in_unix.rb +5 -3
- data/lib/fluent/plugin/out_exec_filter.rb +2 -2
- data/lib/fluent/plugin/out_file.rb +10 -4
- data/lib/fluent/plugin/out_forward.rb +8 -0
- data/lib/fluent/plugin/out_http.rb +0 -10
- data/lib/fluent/plugin/output.rb +49 -19
- data/lib/fluent/plugin/parser_csv.rb +7 -2
- data/lib/fluent/plugin/parser_json.rb +20 -12
- data/lib/fluent/plugin/parser_ltsv.rb +2 -4
- data/lib/fluent/plugin/parser_msgpack.rb +7 -6
- data/lib/fluent/plugin/parser_syslog.rb +52 -5
- data/lib/fluent/plugin/parser_tsv.rb +6 -1
- data/lib/fluent/plugin_helper/http_server/server.rb +29 -13
- data/lib/fluent/plugin_helper/http_server.rb +2 -1
- data/lib/fluent/root_agent.rb +1 -1
- data/lib/fluent/supervisor.rb +24 -5
- data/lib/fluent/system_config.rb +5 -2
- data/lib/fluent/time.rb +8 -7
- data/lib/fluent/version.rb +1 -1
- data/templates/new_gem/.gitignore +8 -0
- metadata +25 -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
|
|
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
|
-
|
|
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.
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
911
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
919
|
-
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
934
|
-
|
|
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
|
-
|
|
800
|
+
def drain_worker_completions(resume:)
|
|
801
|
+
while (result = @worker_pool.next_result)
|
|
938
802
|
begin
|
|
939
|
-
|
|
940
|
-
rescue
|
|
941
|
-
|
|
942
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
836
|
+
log.warn 'worker parsing failed during shutdown; leaving position unchanged', path: batch.watcher.path, error: result.error
|
|
1065
837
|
end
|
|
1066
|
-
|
|
1067
|
-
|
|
838
|
+
else
|
|
839
|
+
emitted = @line_feeder.emit(result.value, batch.watcher)
|
|
1068
840
|
end
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
1321
|
-
|
|
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
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1371
|
-
|
|
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
|