Skip to content

Commit d05f592

Browse files
authored
Merge pull request #4237 from daipom/in_tail-prevent-wrongly-unwatching-2
in_tail: prevent wrongly unwatching with follow_inodes
2 parents e120693 + 51848da commit d05f592

File tree

4 files changed

+71
-33
lines changed

4 files changed

+71
-33
lines changed

lib/fluent/plugin/in_tail.rb

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -370,17 +370,30 @@ def existence_path
370370
def refresh_watchers
371371
target_paths_hash = expand_paths
372372
existence_paths_hash = existence_path
373-
373+
374374
log.debug {
375375
target_paths_str = target_paths_hash.collect { |key, target_info| target_info.path }.join(",")
376376
existence_paths_str = existence_paths_hash.collect { |key, target_info| target_info.path }.join(",")
377377
"tailing paths: target = #{target_paths_str} | existing = #{existence_paths_str}"
378378
}
379379

380-
unwatched_hash = existence_paths_hash.reject {|key, value| target_paths_hash.key?(key)}
380+
if !@follow_inodes
381+
need_unwatch_in_stop_watchers = true
382+
else
383+
# When using @follow_inodes, need this to unwatch the rotated old inode when it disappears.
384+
# After `update_watcher` detaches an old TailWatcher, the inode is lost from the `@tails`.
385+
# So that inode can't be contained in `removed_hash`, and can't be unwatched by `stop_watchers`.
386+
#
387+
# This logic may work for `@follow_inodes false` too.
388+
# Just limiting the case to supress the impact to existing logics.
389+
@pf&.unwatch_removed_targets(target_paths_hash)
390+
need_unwatch_in_stop_watchers = false
391+
end
392+
393+
removed_hash = existence_paths_hash.reject {|key, value| target_paths_hash.key?(key)}
381394
added_hash = target_paths_hash.reject {|key, value| existence_paths_hash.key?(key)}
382395

383-
stop_watchers(unwatched_hash, immediate: false, unwatched: true) unless unwatched_hash.empty?
396+
stop_watchers(removed_hash, unwatched: need_unwatch_in_stop_watchers) unless removed_hash.empty?
384397
start_watchers(added_hash) unless added_hash.empty?
385398
@startup = false if @startup
386399
end
@@ -502,10 +515,6 @@ def update_watcher(tail_watcher, pe, new_inode)
502515
new_target_info = TargetInfo.new(path, new_inode)
503516

504517
if @follow_inodes
505-
# When follow_inodes is true, it's not cleaned up by refresh_watcher.
506-
# So it should be unwatched here explicitly.
507-
tail_watcher.unwatched = true
508-
509518
new_position_entry = @pf[new_target_info]
510519
# If `refresh_watcher` find the new file before, this will not be zero.
511520
# In this case, only we have to do is detaching the current tail_watcher.

lib/fluent/plugin/in_tail/position_file.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,16 @@ def [](target_info)
5353
}
5454
end
5555

56+
def unwatch_removed_targets(existing_targets)
57+
@map.reject { |key, entry|
58+
existing_targets.key?(key)
59+
}.each_key { |key|
60+
unwatch_key(key)
61+
}
62+
end
63+
5664
def unwatch(target_info)
57-
if (entry = @map.delete(@follow_inodes ? target_info.ino : target_info.path))
58-
entry.update_pos(UNWATCHED_POSITION)
59-
end
65+
unwatch_key(@follow_inodes ? target_info.ino : target_info.path)
6066
end
6167

6268
def load(existing_targets = nil)
@@ -118,6 +124,12 @@ def try_compact
118124

119125
private
120126

127+
def unwatch_key(key)
128+
if (entry = @map.delete(key))
129+
entry.update_pos(UNWATCHED_POSITION)
130+
end
131+
end
132+
121133
def compact(existing_targets = nil)
122134
@file_mutex.synchronize do
123135
entries = fetch_compacted_entries

test/plugin/in_tail/test_position_file.rb

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ class IntailPositionFileTest < Test::Unit::TestCase
2626
"valid_path" => Fluent::Plugin::TailInput::TargetInfo.new("valid_path", 1),
2727
"inode23bit" => Fluent::Plugin::TailInput::TargetInfo.new("inode23bit", 0),
2828
}
29+
TEST_CONTENT_INODES = {
30+
1 => Fluent::Plugin::TailInput::TargetInfo.new("valid_path", 1),
31+
0 => Fluent::Plugin::TailInput::TargetInfo.new("inode23bit", 0),
32+
}
2933

3034
def write_data(f, content)
3135
f.write(content)
@@ -221,7 +225,7 @@ def follow_inodes_block
221225
end
222226

223227
sub_test_case '#unwatch' do
224-
test 'deletes entry by path' do
228+
test 'unwatch entry by path' do
225229
write_data(@file, TEST_CONTENT)
226230
pf = Fluent::Plugin::TailInput::PositionFile.load(@file, false, {}, logger: $log)
227231
inode1 = File.stat(@file).ino
@@ -239,6 +243,32 @@ def follow_inodes_block
239243

240244
assert_not_equal p1, p2
241245
end
246+
247+
test 'unwatch entries by inode' do
248+
write_data(@file, TEST_CONTENT)
249+
pf = Fluent::Plugin::TailInput::PositionFile.load(@file, true, TEST_CONTENT_INODES, logger: $log)
250+
251+
existing_targets = TEST_CONTENT_INODES.select do |inode, target_info|
252+
inode == 1
253+
end
254+
pe_to_unwatch = pf[TEST_CONTENT_INODES[0]]
255+
256+
pf.unwatch_removed_targets(existing_targets)
257+
258+
assert_equal(
259+
{
260+
map_keys: [TEST_CONTENT_INODES[1].ino],
261+
unwatched_pe_pos: Fluent::Plugin::TailInput::PositionFile::UNWATCHED_POSITION,
262+
},
263+
{
264+
map_keys: pf.instance_variable_get(:@map).keys,
265+
unwatched_pe_pos: pe_to_unwatch.read_pos,
266+
}
267+
)
268+
269+
unwatched_pe_retaken = pf[TEST_CONTENT_INODES[0]]
270+
assert_not_equal pe_to_unwatch, unwatched_pe_retaken
271+
end
242272
end
243273

244274
sub_test_case 'FilePositionEntry' do

test/plugin/test_in_tail.rb

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2704,22 +2704,14 @@ def test_updateTW_before_refreshTW_and_detach_before_refreshTW
27042704

27052705
assert_equal(
27062706
{
2707-
# TODO: This is BUG!! We need to fix it and replace this with the next.
2708-
record_values: ["file1 log1", "file1 log1", "file1 log2", "file1 log2", "file2 log1", "file2 log2"],
2709-
# record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2"],
2707+
record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2"],
27102708
tail_watcher_paths: ["#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt1"],
27112709
tail_watcher_inodes: [inode_0, inode_1, inode_0],
27122710
tail_watcher_io_handler_opened_statuses: [false, false, false],
2713-
# TODO: This is BUG!! We need to fix it and replace this with the next.
27142711
position_entries: [
2715-
["#{@tmp_dir}/tail.txt", "ffffffffffffffff", inode_0],
2712+
["#{@tmp_dir}/tail.txt", "0000000000000016", inode_0],
27162713
["#{@tmp_dir}/tail.txt", "0000000000000016", inode_1],
2717-
["#{@tmp_dir}/tail.txt1", "0000000000000016", inode_0],
27182714
],
2719-
# position_entries: [
2720-
# ["#{@tmp_dir}/tail.txt", "ffffffffffffffff", inode_0],
2721-
# ["#{@tmp_dir}/tail.txt", "0000000000000016", inode_1],
2722-
# ],
27232715
},
27242716
{
27252717
record_values: record_values,
@@ -2802,7 +2794,8 @@ def test_updateTW_before_refreshTW_and_detach_after_refreshTW
28022794
tail_watcher_inodes: [inode_0, inode_1, inode_0],
28032795
tail_watcher_io_handler_opened_statuses: [false, false, false],
28042796
position_entries: [
2805-
["#{@tmp_dir}/tail.txt", "ffffffffffffffff", inode_0],
2797+
# The recorded path is old, but it is no problem. The path is not used when using follow_inodes.
2798+
["#{@tmp_dir}/tail.txt", "0000000000000016", inode_0],
28062799
["#{@tmp_dir}/tail.txt", "0000000000000016", inode_1],
28072800
],
28082801
},
@@ -2861,8 +2854,9 @@ def test_updateTW_after_refreshTW
28612854
# This overwrites `@tails["tail.txt"]`
28622855
d.instance.refresh_watchers
28632856

2864-
# `watch_timer` calls `TailWatcher::on_notify`, and then `update_watcher` updates the TailWatcher:
2857+
# `watch_timer` calls `TailWatcher::on_notify`, and then `update_watcher` trys to update the TailWatcher:
28652858
# TailWatcher(path: "tail.txt", inode: inode_0) => TailWatcher(path: "tail.txt", inode: inode_1)
2859+
# However, it is already added in `refresh_watcher`, so `update_watcher` doesn't create the new TailWatcher.
28662860
# The old TailWathcer is detached here since `rotate_wait` is just `1s`.
28672861
sleep 3
28682862

@@ -2886,22 +2880,15 @@ def test_updateTW_after_refreshTW
28862880

28872881
assert_equal(
28882882
{
2889-
# TODO: This is BUG!! We need to fix it and replace this with the next.
2890-
record_values: ["file1 log1", "file1 log1", "file1 log2", "file1 log2", "file2 log1", "file2 log2"],
2891-
# record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2"],
2883+
record_values: ["file1 log1", "file1 log2", "file2 log1", "file2 log2"],
28922884
tail_watcher_paths: ["#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt", "#{@tmp_dir}/tail.txt1"],
28932885
tail_watcher_inodes: [inode_0, inode_1, inode_0],
28942886
tail_watcher_io_handler_opened_statuses: [false, false, false],
2895-
# TODO: This is BUG!! We need to fix it and replace this with the next.
28962887
position_entries: [
2897-
["#{@tmp_dir}/tail.txt", "ffffffffffffffff", inode_0],
2888+
# The recorded path is old, but it is no problem. The path is not used when using follow_inodes.
2889+
["#{@tmp_dir}/tail.txt", "0000000000000016", inode_0],
28982890
["#{@tmp_dir}/tail.txt", "0000000000000016", inode_1],
2899-
["#{@tmp_dir}/tail.txt1", "0000000000000016", inode_0],
29002891
],
2901-
# position_entries: [
2902-
# ["#{@tmp_dir}/tail.txt", "ffffffffffffffff", inode_0],
2903-
# ["#{@tmp_dir}/tail.txt", "0000000000000016", inode_1],
2904-
# ],
29052892
},
29062893
{
29072894
record_values: record_values,

0 commit comments

Comments
 (0)