Skip to content

Commit ceb58ad

Browse files
committed
in_tail: fix log duplication when a rotated file is appended during rotate_wait with follow_inodes
With follow_inodes, a TailWatcher detached on rotation keeps reading its inode until rotate_wait elapses, but refresh_watchers only looked at @Tails and started a second TailWatcher for the same inode. Skip inodes still read by a watcher waiting for rotate_wait, and do not register a watcher in @tails_rotate_wait when open_on_every_update already detached it immediately. Fixes #4243 Signed-off-by: Shizuo Fujita <fujita@clear-code.com>
1 parent f727da5 commit ceb58ad

2 files changed

Lines changed: 114 additions & 22 deletions

File tree

‎lib/fluent/plugin/in_tail.rb‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,15 @@ def refresh_watchers
454454
removed_hash = existence_paths_hash.reject {|key, value| target_paths_hash.key?(key)}
455455
added_hash = target_paths_hash.reject {|key, value| existence_paths_hash.key?(key)}
456456

457+
if @follow_inodes
458+
# A TailWatcher waiting for `rotate_wait` is not in `@tails` but still reads its inode.
459+
# Starting another TailWatcher for that inode would collect the same logs twice.
460+
@tails_rotate_wait.each_value do |v|
461+
target = added_hash.delete(v[:ino])
462+
log.debug { "skip #{target.path} (inode: #{target.ino}) because a watcher waiting rotate_wait still reads it" } if target
463+
end
464+
end
465+
457466
# If an existing TailWatcher already follows a target path with the different inode,
458467
# it means that the TailWatcher following the rotated file still exists. In this case,
459468
# `refresh_watcher` can't start the new TailWatcher for the new current file. So, we
@@ -591,14 +600,6 @@ def update_watcher(tail_watcher, pe, new_inode)
591600
if @follow_inodes && new_inode.nil?
592601
# nil inode means the file disappeared, so we only need to stop it.
593602
@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.)
602603
detach_watcher_after_rotate_wait(tail_watcher, pe.read_inode)
603604
return
604605
end
@@ -665,6 +666,7 @@ def detach_watcher_after_rotate_wait(tw, ino)
665666
if @open_on_every_update
666667
# Detach now because it's already closed, waiting it doesn't make sense.
667668
detach_watcher(tw, ino)
669+
return
668670
end
669671

670672
return if @tails_rotate_wait[tw]

‎test/plugin/test_in_tail.rb‎

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2951,6 +2951,7 @@ def test_updateTW_before_refreshTW_and_detach_after_refreshTW
29512951

29522952
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file1 log1"}
29532953

2954+
tail_watcher_count_after_first_refresh = nil
29542955
d.run(expect_records: 4, timeout: 10) do
29552956
# Rotate (If the timing is bad, `TailWatcher::on_notify` might be called between mv and new-file-creation)
29562957
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file1 log2"}
@@ -2963,11 +2964,16 @@ def test_updateTW_before_refreshTW_and_detach_after_refreshTW
29632964

29642965
# This reproduces the following situation:
29652966
# Rotation => update_watcher => refresh_watchers
2966-
# This adds a new TailWatcher: TailWatcher(path: "tail.txt1", inode: inode_0)
2967+
# This does NOT add TailWatcher(path: "tail.txt1", inode: inode_0) because the old
2968+
# TailWatcher waiting `rotate_wait` still reads inode_0.
29672969
d.instance.refresh_watchers
2970+
tail_watcher_count_after_first_refresh = tail_watchers.size
29682971

2969-
# The old TailWatcher is detached here since `rotate_wait` is `4s`.
2970-
sleep 3
2972+
# The old TailWatcher is detached and closed after `rotate_wait` (`4s`).
2973+
waiting(10) { sleep 0.1 until tail_watchers[0].instance_variable_get(:@io_handler).nil? }
2974+
2975+
# This adds a new TailWatcher: TailWatcher(path: "tail.txt1", inode: inode_0)
2976+
d.instance.refresh_watchers
29712977

29722978
# Append to the new current log file.
29732979
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file2 log2"}
@@ -2987,6 +2993,7 @@ def test_updateTW_before_refreshTW_and_detach_after_refreshTW
29872993
assert_equal(
29882994
{
29892995
record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2"],
2996+
tail_watcher_count_after_first_refresh: 2,
29902997
tail_watcher_paths: ["#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt1"],
29912998
tail_watcher_inodes: [inode_0, inode_1, inode_0],
29922999
tail_watcher_io_handler_opened_statuses: [false, false, false],
@@ -2998,6 +3005,7 @@ def test_updateTW_before_refreshTW_and_detach_after_refreshTW
29983005
},
29993006
{
30003007
record_values: record_values,
3008+
tail_watcher_count_after_first_refresh: tail_watcher_count_after_first_refresh,
30013009
tail_watcher_paths: tail_watchers.collect { |tw| tw.path },
30023010
tail_watcher_inodes: tail_watchers.collect { |tw| tw.ino },
30033011
tail_watcher_io_handler_opened_statuses: tail_watchers.collect { |tw| tw.instance_variable_get(:@io_handler)&.opened? || false },
@@ -3111,10 +3119,8 @@ def test_path_resurrection
31113119
# In order to reproduce the same condition stably, ensure that `refresh_watchers` is not
31123120
# called by a timer.
31133121
"refresh_interval" => "1h",
3114-
# https://github.com/fluent/fluentd/pull/4237#issuecomment-1633358632
3115-
# Because of this problem, log duplication can occur during `rotate_wait`.
3116-
# Need to set `rotate_wait 0` for a workaround.
3117-
"rotate_wait" => "0s",
3122+
# In order to keep the first TailWatcher reading the rotated file until the end of the test.
3123+
"rotate_wait" => "10s",
31183124
}
31193125
)
31203126
d = create_driver(config, false)
@@ -3143,9 +3149,10 @@ def test_path_resurrection
31433149
sleep 2 # On Windows and macOS, StatWatcher doesn't work, so need enough interval for TimeTrigger.
31443150
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file2 log1"}
31453151

3146-
# Add new TailWatchers
3152+
# Add a new TailWatcher
31473153
# tail.txt: TailWatcher(path: "tail.txt", inode: inode_1)
3148-
# tail.txt: TailWatcher(path: "tail.txt1", inode: inode_0)
3154+
# TailWatcher(path: "tail.txt1", inode: inode_0) is not added because the first TailWatcher
3155+
# waiting `rotate_wait` still reads inode_0.
31493156
# NOTE: If not discarding the first TailWatcher on notify, this makes it a orphan because
31503157
# this overwrites the `@tails[tail.txt]` by adding TailWatcher(path: "tail.txt", inode: inode_1)
31513158
d.instance.refresh_watchers
@@ -3189,11 +3196,6 @@ def test_path_resurrection
31893196
inode: inode_1,
31903197
io_handler_opened_status: false,
31913198
},
3192-
{
3193-
path: "#{@tmp_dir}/tail.txt1",
3194-
inode: inode_0,
3195-
io_handler_opened_status: false,
3196-
},
31973199
],
31983200
position_entries: [
31993201
["#{@tmp_dir}/tail.txt", "0000000000000021", inode_0],
@@ -3214,6 +3216,94 @@ def test_path_resurrection
32143216
)
32153217
end
32163218

3219+
# https://github.com/fluent/fluentd/issues/4243
3220+
def test_no_duplication_when_appending_to_rotated_file_during_rotate_wait
3221+
config = config_element(
3222+
"ROOT",
3223+
"",
3224+
{
3225+
"path" => "#{@tmp_dir}/tail.txt*",
3226+
"pos_file" => "#{@tmp_dir}/tail.pos",
3227+
"tag" => "t1",
3228+
"format" => "none",
3229+
"read_from_head" => "true",
3230+
"follow_inodes" => "true",
3231+
# In order to keep the old watcher alive while `refresh_watchers` runs.
3232+
"rotate_wait" => "4s",
3233+
# In order to reproduce the same condition stably, ensure that `refresh_watchers` is not
3234+
# called by a timer.
3235+
"refresh_interval" => "1h",
3236+
# stat_watcher often calls `TailWatcher::on_notify` faster than creating a new log file,
3237+
# so disable it in order to reproduce the same condition stably.
3238+
"enable_stat_watcher" => "false",
3239+
}
3240+
)
3241+
d = create_driver(config, false)
3242+
3243+
tail_watchers = []
3244+
stub.proxy(d.instance).setup_watcher do |tw|
3245+
tail_watchers.append(tw)
3246+
tw
3247+
end
3248+
3249+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file1 log1"}
3250+
3251+
d.run(expect_records: 6, timeout: 15) do
3252+
# Rotate
3253+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file1 log2"}
3254+
FileUtils.move("#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt" + "1")
3255+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "wb") {|f| f.puts "file2 log1"}
3256+
3257+
# `watch_timer` calls `TailWatcher::on_notify`, and then `update_watcher` updates the TailWatcher:
3258+
# TailWatcher(path: "tail.txt", inode: inode_0) => TailWatcher(path: "tail.txt", inode: inode_1)
3259+
# The old TailWatcher keeps reading inode_0 during `rotate_wait`.
3260+
sleep 2
3261+
3262+
# `refresh_watchers` must not add another TailWatcher for inode_0 while the old one is
3263+
# still reading it. Otherwise the following append is collected twice.
3264+
d.instance.refresh_watchers
3265+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt1", "ab") {|f| f.puts "file1 log3"}
3266+
3267+
# The old TailWatcher is detached and closed after `rotate_wait` (`4s`).
3268+
waiting(10) { sleep 0.1 until tail_watchers[0].instance_variable_get(:@io_handler).nil? }
3269+
3270+
# Now the rotated file is followed again from the recorded position.
3271+
# TailWatcher(path: "tail.txt1", inode: inode_0)
3272+
d.instance.refresh_watchers
3273+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt1", "ab") {|f| f.puts "file1 log4"}
3274+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.txt", "ab") {|f| f.puts "file2 log2"}
3275+
end
3276+
3277+
inode_0 = tail_watchers[0].ino
3278+
inode_1 = tail_watchers[1].ino
3279+
record_values = d.events.collect { |event| event[2]["message"] }.sort
3280+
position_entries = []
3281+
Fluent::FileWrapper.open("#{@tmp_dir}/tail.pos", "r") do |f|
3282+
f.readlines(chomp: true).each do |line|
3283+
values = line.split("\t")
3284+
position_entries.append([values[0], values[1], values[2].to_i(16)])
3285+
end
3286+
end
3287+
3288+
assert_equal(
3289+
{
3290+
record_values: ["file1 log1", "file1 log2", "file1 log3", "file1 log4", "file2 log1", "file2 log2"],
3291+
tail_watcher_paths: ["#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt1"],
3292+
tail_watcher_inodes: [inode_0, inode_1, inode_0],
3293+
position_entries: [
3294+
["#{@tmp_dir}/tail.txt", "000000000000002c", inode_0],
3295+
["#{@tmp_dir}/tail.txt", "0000000000000016", inode_1],
3296+
],
3297+
},
3298+
{
3299+
record_values: record_values,
3300+
tail_watcher_paths: tail_watchers.collect { |tw| tw.path },
3301+
tail_watcher_inodes: tail_watchers.collect { |tw| tw.ino },
3302+
position_entries: position_entries
3303+
},
3304+
)
3305+
end
3306+
32173307
def test_next_rotation_occurs_very_fast_while_old_TW_still_waiting_rotate_wait
32183308
config = config_element(
32193309
"ROOT",

0 commit comments

Comments
 (0)