@@ -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