fluentd 1.15.3-x86-mingw32 → 1.16.2-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.yaml +1 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.yaml +1 -0
  4. data/.github/workflows/linux-test.yaml +2 -2
  5. data/.github/workflows/macos-test.yaml +2 -2
  6. data/.github/workflows/stale-actions.yml +24 -0
  7. data/.github/workflows/windows-test.yaml +2 -2
  8. data/CHANGELOG.md +151 -0
  9. data/CONTRIBUTING.md +1 -1
  10. data/MAINTAINERS.md +5 -3
  11. data/README.md +0 -1
  12. data/SECURITY.md +5 -9
  13. data/fluentd.gemspec +3 -3
  14. data/lib/fluent/command/ctl.rb +2 -2
  15. data/lib/fluent/command/fluentd.rb +55 -53
  16. data/lib/fluent/command/plugin_config_formatter.rb +1 -1
  17. data/lib/fluent/config/dsl.rb +1 -1
  18. data/lib/fluent/config/v1_parser.rb +2 -2
  19. data/lib/fluent/counter/server.rb +1 -1
  20. data/lib/fluent/counter/validator.rb +3 -3
  21. data/lib/fluent/daemon.rb +2 -4
  22. data/lib/fluent/engine.rb +1 -1
  23. data/lib/fluent/event.rb +8 -4
  24. data/lib/fluent/log/console_adapter.rb +66 -0
  25. data/lib/fluent/log.rb +44 -5
  26. data/lib/fluent/match.rb +1 -1
  27. data/lib/fluent/msgpack_factory.rb +6 -1
  28. data/lib/fluent/plugin/base.rb +6 -8
  29. data/lib/fluent/plugin/buf_file.rb +32 -3
  30. data/lib/fluent/plugin/buf_file_single.rb +32 -3
  31. data/lib/fluent/plugin/buffer/file_chunk.rb +1 -1
  32. data/lib/fluent/plugin/buffer.rb +21 -0
  33. data/lib/fluent/plugin/filter_record_transformer.rb +1 -1
  34. data/lib/fluent/plugin/in_forward.rb +1 -1
  35. data/lib/fluent/plugin/in_http.rb +8 -8
  36. data/lib/fluent/plugin/in_sample.rb +1 -1
  37. data/lib/fluent/plugin/in_tail/position_file.rb +32 -18
  38. data/lib/fluent/plugin/in_tail.rb +58 -24
  39. data/lib/fluent/plugin/in_tcp.rb +47 -2
  40. data/lib/fluent/plugin/out_exec_filter.rb +2 -2
  41. data/lib/fluent/plugin/out_forward/ack_handler.rb +19 -4
  42. data/lib/fluent/plugin/out_forward.rb +2 -2
  43. data/lib/fluent/plugin/out_secondary_file.rb +39 -22
  44. data/lib/fluent/plugin/output.rb +50 -13
  45. data/lib/fluent/plugin/parser_json.rb +1 -1
  46. data/lib/fluent/plugin_helper/event_loop.rb +2 -2
  47. data/lib/fluent/plugin_helper/http_server/server.rb +2 -1
  48. data/lib/fluent/plugin_helper/record_accessor.rb +1 -1
  49. data/lib/fluent/plugin_helper/server.rb +8 -0
  50. data/lib/fluent/plugin_helper/thread.rb +3 -3
  51. data/lib/fluent/plugin_id.rb +1 -1
  52. data/lib/fluent/supervisor.rb +157 -251
  53. data/lib/fluent/test/driver/base.rb +11 -5
  54. data/lib/fluent/test/driver/filter.rb +4 -0
  55. data/lib/fluent/test/startup_shutdown.rb +6 -8
  56. data/lib/fluent/version.rb +1 -1
  57. data/templates/new_gem/test/helper.rb.erb +0 -1
  58. data/test/command/test_ctl.rb +1 -1
  59. data/test/command/test_fluentd.rb +137 -6
  60. data/test/command/test_plugin_config_formatter.rb +0 -1
  61. data/test/compat/test_parser.rb +5 -5
  62. data/test/config/test_system_config.rb +0 -8
  63. data/test/log/test_console_adapter.rb +110 -0
  64. data/test/plugin/in_tail/test_position_file.rb +31 -1
  65. data/test/plugin/out_forward/test_ack_handler.rb +39 -0
  66. data/test/plugin/test_base.rb +99 -1
  67. data/test/plugin/test_buf_file.rb +62 -23
  68. data/test/plugin/test_buf_file_single.rb +65 -0
  69. data/test/plugin/test_buffer_chunk.rb +11 -0
  70. data/test/plugin/test_in_forward.rb +9 -9
  71. data/test/plugin/test_in_http.rb +2 -3
  72. data/test/plugin/test_in_monitor_agent.rb +2 -3
  73. data/test/plugin/test_in_tail.rb +379 -0
  74. data/test/plugin/test_in_tcp.rb +87 -2
  75. data/test/plugin/test_in_udp.rb +28 -0
  76. data/test/plugin/test_in_unix.rb +2 -2
  77. data/test/plugin/test_multi_output.rb +1 -1
  78. data/test/plugin/test_out_exec_filter.rb +2 -2
  79. data/test/plugin/test_out_file.rb +2 -2
  80. data/test/plugin/test_out_forward.rb +14 -18
  81. data/test/plugin/test_out_http.rb +1 -0
  82. data/test/plugin/test_output.rb +281 -12
  83. data/test/plugin/test_output_as_buffered.rb +44 -44
  84. data/test/plugin/test_output_as_buffered_compress.rb +32 -18
  85. data/test/plugin/test_output_as_buffered_retries.rb +1 -1
  86. data/test/plugin/test_output_as_buffered_secondary.rb +2 -2
  87. data/test/plugin/test_parser_regexp.rb +1 -6
  88. data/test/plugin_helper/test_child_process.rb +2 -2
  89. data/test/plugin_helper/test_http_server_helper.rb +1 -1
  90. data/test/plugin_helper/test_server.rb +60 -6
  91. data/test/test_config.rb +0 -21
  92. data/test/test_formatter.rb +23 -20
  93. data/test/test_log.rb +108 -36
  94. data/test/test_msgpack_factory.rb +32 -0
  95. data/test/test_supervisor.rb +287 -279
  96. metadata +15 -21
  97. data/.drone.yml +0 -35
  98. data/.gitlab-ci.yml +0 -103
  99. data/test/test_logger_initializer.rb +0 -46
@@ -53,10 +53,16 @@ module Fluent::Plugin
53
53
  }
54
54
  end
55
55
 
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
+
56
64
  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)
60
66
  end
61
67
 
62
68
  def load(existing_targets = nil)
@@ -96,6 +102,7 @@ module Fluent::Plugin
96
102
  end
97
103
 
98
104
  entries = fetch_compacted_entries
105
+ @logger&.debug "Compacted entries: ", entries.keys
99
106
 
100
107
  @file_mutex.synchronize do
101
108
  if last_modified == @file.mtime && size == @file.size
@@ -117,17 +124,31 @@ module Fluent::Plugin
117
124
 
118
125
  private
119
126
 
127
+ def unwatch_key(key)
128
+ if (entry = @map.delete(key))
129
+ entry.update_pos(UNWATCHED_POSITION)
130
+ end
131
+ end
132
+
120
133
  def compact(existing_targets = nil)
121
134
  @file_mutex.synchronize do
122
- entries = fetch_compacted_entries(existing_targets).values.map(&:to_entry_fmt)
135
+ entries = fetch_compacted_entries
136
+ @logger&.debug "Compacted entries: ", entries.keys
137
+
138
+ if existing_targets
139
+ entries = remove_deleted_files_entries(entries, existing_targets)
140
+ @logger&.debug "Remove missing entries.",
141
+ existing_targets: existing_targets.keys,
142
+ entries_after_removing: entries.keys
143
+ end
123
144
 
124
145
  @file.pos = 0
125
146
  @file.truncate(0)
126
- @file.write(entries.join)
147
+ @file.write(entries.values.map(&:to_entry_fmt).join)
127
148
  end
128
149
  end
129
150
 
130
- def fetch_compacted_entries(existing_targets = nil)
151
+ def fetch_compacted_entries
131
152
  entries = {}
132
153
 
133
154
  @file.pos = 0
@@ -145,31 +166,24 @@ module Fluent::Plugin
145
166
  if pos == UNWATCHED_POSITION
146
167
  @logger.debug "Remove unwatched line from pos_file: #{line}" if @logger
147
168
  else
148
- if entries.include?(path)
149
- @logger.warn("#{path} already exists. use latest one: deleted #{entries[path]}") if @logger
150
- end
151
-
152
169
  if @follow_inodes
170
+ @logger&.warn("#{path} (inode: #{ino}) already exists. use latest one: deleted #{entries[ino]}") if entries.include?(ino)
153
171
  entries[ino] = Entry.new(path, pos, ino, file_pos + path.bytesize + 1)
154
172
  else
173
+ @logger&.warn("#{path} already exists. use latest one: deleted #{entries[path]}") if entries.include?(path)
155
174
  entries[path] = Entry.new(path, pos, ino, file_pos + path.bytesize + 1)
156
175
  end
157
176
  file_pos += line.size
158
177
  end
159
178
  end
160
179
 
161
- entries = remove_deleted_files_entries(entries, existing_targets)
162
180
  entries
163
181
  end
164
182
 
165
183
  def remove_deleted_files_entries(existent_entries, existing_targets)
166
- if existing_targets
167
- existent_entries.select { |path_or_ino|
168
- existing_targets.key?(path_or_ino)
169
- }
170
- else
171
- existent_entries
172
- end
184
+ existent_entries.select { |path_or_ino|
185
+ existing_targets.key?(path_or_ino)
186
+ }
173
187
  end
174
188
  end
175
189
 
@@ -370,17 +370,30 @@ module Fluent::Plugin
370
370
  def refresh_watchers
371
371
  target_paths_hash = expand_paths
372
372
  existence_paths_hash = existence_path
373
-
373
+
374
374
  log.debug {
375
375
  target_paths_str = target_paths_hash.collect { |key, target_info| target_info.path }.join(",")
376
376
  existence_paths_str = existence_paths_hash.collect { |key, target_info| target_info.path }.join(",")
377
377
  "tailing paths: target = #{target_paths_str} | existing = #{existence_paths_str}"
378
378
  }
379
379
 
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)}
381
394
  added_hash = target_paths_hash.reject {|key, value| existence_paths_hash.key?(key)}
382
395
 
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?
384
397
  start_watchers(added_hash) unless added_hash.empty?
385
398
  @startup = false if @startup
386
399
  end
@@ -484,8 +497,26 @@ module Fluent::Plugin
484
497
  end
485
498
 
486
499
  # refresh_watchers calls @tails.keys so we don't use stop_watcher -> start_watcher sequence for safety.
487
- def update_watcher(target_info, pe)
488
- path = target_info.path
500
+ def update_watcher(tail_watcher, pe, new_inode)
501
+ # TODO we should use another callback for this.
502
+ # To supress impact to existing logics, limit the case to `@follow_inodes`.
503
+ # We may not need `@follow_inodes` condition.
504
+ if @follow_inodes && new_inode.nil?
505
+ # nil inode means the file disappeared, so we only need to stop it.
506
+ @tails.delete(tail_watcher.path)
507
+ # https://github.com/fluent/fluentd/pull/4237#issuecomment-1633358632
508
+ # Because of this problem, log duplication can occur during `rotate_wait`.
509
+ # Need to set `rotate_wait 0` for a workaround.
510
+ # Duplication will occur if `refresh_watcher` is called during the `rotate_wait`.
511
+ # In that case, `refresh_watcher` will add the new TailWatcher to tail the same target,
512
+ # and it causes the log duplication.
513
+ # (Other `detach_watcher_after_rotate_wait` may have the same problem.
514
+ # We need the mechanism not to add duplicated TailWathcer with detaching TailWatcher.)
515
+ detach_watcher_after_rotate_wait(tail_watcher, pe.read_inode)
516
+ return
517
+ end
518
+
519
+ path = tail_watcher.path
489
520
 
490
521
  log.info("detected rotation of #{path}; waiting #{@rotate_wait} seconds")
491
522
 
@@ -499,23 +530,22 @@ module Fluent::Plugin
499
530
  end
500
531
  end
501
532
 
502
- rotated_tw = @tails[path]
533
+ new_target_info = TargetInfo.new(path, new_inode)
503
534
 
504
535
  if @follow_inodes
505
- new_position_entry = @pf[target_info]
506
-
536
+ new_position_entry = @pf[new_target_info]
537
+ # If `refresh_watcher` find the new file before, this will not be zero.
538
+ # In this case, only we have to do is detaching the current tail_watcher.
507
539
  if new_position_entry.read_inode == 0
508
- # When follow_inodes is true, it's not cleaned up by refresh_watcher.
509
- # So it should be unwatched here explicitly.
510
- rotated_tw.unwatched = true if rotated_tw
511
- @tails[path] = setup_watcher(target_info, new_position_entry)
540
+ @tails[path] = setup_watcher(new_target_info, new_position_entry)
512
541
  @tails[path].on_notify
513
542
  end
514
543
  else
515
- @tails[path] = setup_watcher(target_info, pe)
544
+ @tails[path] = setup_watcher(new_target_info, pe)
516
545
  @tails[path].on_notify
517
546
  end
518
- detach_watcher_after_rotate_wait(rotated_tw, pe.read_inode) if rotated_tw
547
+
548
+ detach_watcher_after_rotate_wait(tail_watcher, pe.read_inode)
519
549
  end
520
550
 
521
551
  # TailWatcher#close is called by another thread at shutdown phase.
@@ -523,6 +553,10 @@ module Fluent::Plugin
523
553
  # so adding close_io argument to avoid this problem.
524
554
  # At shutdown, IOHandler's io will be released automatically after detached the event loop
525
555
  def detach_watcher(tw, ino, close_io = true)
556
+ if @follow_inodes && tw.ino != ino
557
+ log.warn("detach_watcher could be detaching an unexpected tail_watcher with a different ino.",
558
+ path: tw.path, actual_ino_in_tw: tw.ino, expect_ino_to_close: ino)
559
+ end
526
560
  tw.watchers.each do |watcher|
527
561
  event_loop_detach(watcher)
528
562
  end
@@ -778,7 +812,7 @@ module Fluent::Plugin
778
812
  attr_accessor :group_watcher
779
813
 
780
814
  def tag
781
- @parsed_tag ||= @path.tr('/', '.').gsub(/\.+/, '.').gsub(/^\./, '')
815
+ @parsed_tag ||= @path.tr('/', '.').squeeze('.').gsub(/^\./, '')
782
816
  end
783
817
 
784
818
  def register_watcher(watcher)
@@ -874,21 +908,21 @@ module Fluent::Plugin
874
908
 
875
909
  if watcher_needs_update
876
910
  if @follow_inodes
877
- # No need to update a watcher if stat is nil (file not present), because moving to inodes will create
878
- # new watcher, and old watcher will be closed by stop_watcher in refresh_watchers method
879
- # don't want to swap state because we need latest read offset in pos file even after rotate_wait
880
- if stat
881
- target_info = TargetInfo.new(@path, stat.ino)
882
- @update_watcher.call(target_info, @pe)
883
- end
911
+ # If stat is nil (file not present), NEED to stop and discard this watcher.
912
+ # When the file is disappeared but is resurrected soon, then `#refresh_watcher`
913
+ # can't recognize this TailWatcher needs to be stopped.
914
+ # This can happens when the file is rotated.
915
+ # If a notify comes before the new file for the path is created during rotation,
916
+ # then it appears as if the file was resurrected once it disappeared.
917
+ # Don't want to swap state because we need latest read offset in pos file even after rotate_wait
918
+ @update_watcher.call(self, @pe, stat&.ino)
884
919
  else
885
920
  # Permit to handle if stat is nil (file not present).
886
921
  # If a file is mv-ed and a new file is created during
887
922
  # calling `#refresh_watchers`s, and `#refresh_watchers` won't run `#start_watchers`
888
923
  # and `#stop_watchers()` for the path because `target_paths_hash`
889
924
  # always contains the path.
890
- target_info = TargetInfo.new(@path, stat ? stat.ino : nil)
891
- @update_watcher.call(target_info, swap_state(@pe))
925
+ @update_watcher.call(self, swap_state(@pe), stat&.ino)
892
926
  end
893
927
  else
894
928
  @log.info "detected rotation of #{@path}"
@@ -36,10 +36,16 @@ module Fluent::Plugin
36
36
  desc "The field name of the client's address."
37
37
  config_param :source_address_key, :string, default: nil
38
38
 
39
+ # Setting default to nil for backward compatibility
40
+ desc "The max bytes of message."
41
+ config_param :message_length_limit, :size, default: nil
42
+
39
43
  config_param :blocking_timeout, :time, default: 0.5
40
44
 
41
45
  desc 'The payload is read up to this character.'
42
46
  config_param :delimiter, :string, default: "\n" # syslog family add "\n" to each message and this seems only way to split messages in tcp stream
47
+ desc 'Check the remote connection is still available by sending a keepalive packet if this value is true.'
48
+ config_param :send_keepalive_packet, :bool, default: false
43
49
 
44
50
  # in_forward like host/network restriction
45
51
  config_section :security, required: false, multi: false do
@@ -100,8 +106,9 @@ module Fluent::Plugin
100
106
 
101
107
  log.info "listening tcp socket", bind: @bind, port: @port
102
108
  del_size = @delimiter.length
109
+ discard_till_next_delimiter = false
103
110
  if @_extract_enabled && @_extract_tag_key
104
- server_create(:in_tcp_server_single_emit, @port, bind: @bind, resolve_name: !!@source_hostname_key) do |data, conn|
111
+ server_create(:in_tcp_server_single_emit, @port, bind: @bind, resolve_name: !!@source_hostname_key, send_keepalive_packet: @send_keepalive_packet) do |data, conn|
105
112
  unless check_client(conn)
106
113
  conn.close
107
114
  next
@@ -114,6 +121,16 @@ module Fluent::Plugin
114
121
  msg = buf[pos...i]
115
122
  pos = i + del_size
116
123
 
124
+ if discard_till_next_delimiter
125
+ discard_till_next_delimiter = false
126
+ next
127
+ end
128
+
129
+ if !@message_length_limit.nil? && @message_length_limit < msg.bytesize
130
+ log.info "The received data is larger than 'message_length_limit', dropped:", limit: @message_length_limit, size: msg.bytesize, head: msg[...32]
131
+ next
132
+ end
133
+
117
134
  @parser.parse(msg) do |time, record|
118
135
  unless time && record
119
136
  log.warn "pattern not matched", message: msg
@@ -129,9 +146,18 @@ module Fluent::Plugin
129
146
  end
130
147
  end
131
148
  buf.slice!(0, pos) if pos > 0
149
+ # If the buffer size exceeds the limit here, it means that the next message will definitely exceed the limit.
150
+ # So we should clear the buffer here. Otherwise, it will keep storing useless data until the next delimiter comes.
151
+ if !@message_length_limit.nil? && @message_length_limit < buf.bytesize
152
+ log.info "The buffer size exceeds 'message_length_limit', cleared:", limit: @message_length_limit, size: buf.bytesize, head: buf[...32]
153
+ buf.clear
154
+ # We should discard the subsequent data until the next delimiter comes.
155
+ discard_till_next_delimiter = true
156
+ next
157
+ end
132
158
  end
133
159
  else
134
- server_create(:in_tcp_server_batch_emit, @port, bind: @bind, resolve_name: !!@source_hostname_key) do |data, conn|
160
+ server_create(:in_tcp_server_batch_emit, @port, bind: @bind, resolve_name: !!@source_hostname_key, send_keepalive_packet: @send_keepalive_packet) do |data, conn|
135
161
  unless check_client(conn)
136
162
  conn.close
137
163
  next
@@ -145,6 +171,16 @@ module Fluent::Plugin
145
171
  msg = buf[pos...i]
146
172
  pos = i + del_size
147
173
 
174
+ if discard_till_next_delimiter
175
+ discard_till_next_delimiter = false
176
+ next
177
+ end
178
+
179
+ if !@message_length_limit.nil? && @message_length_limit < msg.bytesize
180
+ log.info "The received data is larger than 'message_length_limit', dropped:", limit: @message_length_limit, size: msg.bytesize, head: msg[...32]
181
+ next
182
+ end
183
+
148
184
  @parser.parse(msg) do |time, record|
149
185
  unless time && record
150
186
  log.warn "pattern not matched", message: msg
@@ -159,6 +195,15 @@ module Fluent::Plugin
159
195
  end
160
196
  router.emit_stream(@tag, es)
161
197
  buf.slice!(0, pos) if pos > 0
198
+ # If the buffer size exceeds the limit here, it means that the next message will definitely exceed the limit.
199
+ # So we should clear the buffer here. Otherwise, it will keep storing useless data until the next delimiter comes.
200
+ if !@message_length_limit.nil? && @message_length_limit < buf.bytesize
201
+ log.info "The buffer size exceeds 'message_length_limit', cleared:", limit: @message_length_limit, size: buf.bytesize, head: buf[...32]
202
+ buf.clear
203
+ # We should discard the subsequent data until the next delimiter comes.
204
+ discard_till_next_delimiter = true
205
+ next
206
+ end
162
207
  end
163
208
  end
164
209
  end
@@ -163,7 +163,7 @@ module Fluent::Plugin
163
163
  0
164
164
  elsif (@child_respawn == 'inf') || (@child_respawn == '-1')
165
165
  -1
166
- elsif @child_respawn =~ /^\d+$/
166
+ elsif /^\d+$/.match?(@child_respawn)
167
167
  @child_respawn.to_i
168
168
  else
169
169
  raise ConfigError, "child_respawn option argument invalid: none(or 0), inf(or -1) or positive number"
@@ -187,7 +187,7 @@ module Fluent::Plugin
187
187
  @rr = 0
188
188
 
189
189
  exit_callback = ->(status){
190
- c = @children.select{|child| child.pid == status.pid }.first
190
+ c = @children.find{|child| child.pid == status.pid }
191
191
  if c
192
192
  unless self.stopped?
193
193
  log.warn "child process exits with error code", code: status.to_i, status: status.exitstatus, signal: status.termsig
@@ -59,7 +59,13 @@ module Fluent::Plugin
59
59
  @ack_waitings = new_list
60
60
  end
61
61
 
62
- readable_sockets, _, _ = IO.select(sockets, nil, nil, select_interval)
62
+ begin
63
+ readable_sockets, _, _ = IO.select(sockets, nil, nil, select_interval)
64
+ rescue IOError
65
+ @log.info "connection closed while waiting for readable sockets"
66
+ readable_sockets = nil
67
+ end
68
+
63
69
  if readable_sockets
64
70
  readable_sockets.each do |sock|
65
71
  results << read_ack_from_sock(sock)
@@ -109,13 +115,22 @@ module Fluent::Plugin
109
115
  raw_data = sock.instance_of?(Fluent::PluginHelper::Socket::WrappedSocket::TLS) ? sock.readpartial(@read_length) : sock.recv(@read_length)
110
116
  rescue Errno::ECONNRESET, EOFError # ECONNRESET for #recv, #EOFError for #readpartial
111
117
  raw_data = ''
118
+ rescue IOError
119
+ @log.info "socket closed while receiving ack response"
120
+ return nil, Result::FAILED
112
121
  end
113
122
 
114
123
  info = find(sock)
115
124
 
116
- # When connection is closed by remote host, socket is ready to read and #recv returns an empty string that means EOF.
117
- # If this happens we assume the data wasn't delivered and retry it.
118
- if raw_data.empty?
125
+ if info.nil?
126
+ # The info can be deleted by another thread during `sock.recv()` and `find()`.
127
+ # This is OK since another thread has completed to process the ack, so we can skip this.
128
+ # Note: exclusion mechanism about `collect_response()` may need to be considered.
129
+ @log.debug "could not find the ack info. this ack may be processed by another thread."
130
+ return nil, Result::FAILED
131
+ elsif raw_data.empty?
132
+ # When connection is closed by remote host, socket is ready to read and #recv returns an empty string that means EOF.
133
+ # If this happens we assume the data wasn't delivered and retry it.
119
134
  @log.warn 'destination node closed the connection. regard it as unavailable.', host: info.node.host, port: info.node.port
120
135
  # info.node.disable!
121
136
  return info, Result::FAILED
@@ -521,8 +521,8 @@ module Fluent::Plugin
521
521
  when AckHandler::Result::SUCCESS
522
522
  commit_write(chunk_id)
523
523
  when AckHandler::Result::FAILED
524
- node.disable!
525
- rollback_write(chunk_id, update_retry: false)
524
+ node&.disable!
525
+ rollback_write(chunk_id, update_retry: false) if chunk_id
526
526
  when AckHandler::Result::CHUNKID_UNMATCHED
527
527
  rollback_write(chunk_id, update_retry: false)
528
528
  else
@@ -64,31 +64,29 @@ module Fluent::Plugin
64
64
  end
65
65
 
66
66
  def multi_workers_ready?
67
- ### TODO: add hack to synchronize for multi workers
68
67
  true
69
68
  end
70
69
 
71
70
  def write(chunk)
72
71
  path_without_suffix = extract_placeholders(@path_without_suffix, chunk)
73
- path = generate_path(path_without_suffix)
74
- FileUtils.mkdir_p File.dirname(path), mode: @dir_perm
75
-
76
- case @compress
77
- when :text
78
- File.open(path, "ab", @file_perm) {|f|
79
- f.flock(File::LOCK_EX)
80
- chunk.write_to(f)
81
- }
82
- when :gzip
83
- File.open(path, "ab", @file_perm) {|f|
84
- f.flock(File::LOCK_EX)
85
- gz = Zlib::GzipWriter.new(f)
86
- chunk.write_to(gz)
87
- gz.close
88
- }
72
+ generate_path(path_without_suffix) do |path|
73
+ FileUtils.mkdir_p File.dirname(path), mode: @dir_perm
74
+
75
+ case @compress
76
+ when :text
77
+ File.open(path, "ab", @file_perm) {|f|
78
+ f.flock(File::LOCK_EX)
79
+ chunk.write_to(f)
80
+ }
81
+ when :gzip
82
+ File.open(path, "ab", @file_perm) {|f|
83
+ f.flock(File::LOCK_EX)
84
+ gz = Zlib::GzipWriter.new(f)
85
+ chunk.write_to(gz)
86
+ gz.close
87
+ }
88
+ end
89
89
  end
90
-
91
- path
92
90
  end
93
91
 
94
92
  private
@@ -117,15 +115,34 @@ module Fluent::Plugin
117
115
 
118
116
  def generate_path(path_without_suffix)
119
117
  if @append
120
- "#{path_without_suffix}#{@suffix}"
121
- else
118
+ path = "#{path_without_suffix}#{@suffix}"
119
+ synchronize_path(path) do
120
+ yield path
121
+ end
122
+ return path
123
+ end
124
+
125
+ begin
122
126
  i = 0
123
127
  loop do
124
128
  path = "#{path_without_suffix}.#{i}#{@suffix}"
125
- return path unless File.exist?(path)
129
+ break unless File.exist?(path)
126
130
  i += 1
127
131
  end
132
+ synchronize_path(path) do
133
+ # If multiple processes or threads select the same path and another
134
+ # one entered this locking block first, the file should already
135
+ # exist and this one should retry to find new path.
136
+ raise FileAlreadyExist if File.exist?(path)
137
+ yield path
138
+ end
139
+ rescue FileAlreadyExist
140
+ retry
128
141
  end
142
+ path
143
+ end
144
+
145
+ class FileAlreadyExist < StandardError
129
146
  end
130
147
  end
131
148
  end
@@ -99,7 +99,6 @@ module Fluent
99
99
  config_param :retry_max_interval, :time, default: nil, desc: 'The maximum interval seconds for exponential backoff between retries while failing.'
100
100
 
101
101
  config_param :retry_randomize, :bool, default: true, desc: 'If true, output plugin will retry after randomized interval not to do burst retries.'
102
- config_param :disable_chunk_backup, :bool, default: false, desc: 'If true, chunks are thrown away when unrecoverable error happens'
103
102
  end
104
103
 
105
104
  config_section :secondary, param_name: :secondary_config, required: false, multi: false, final: true do
@@ -199,6 +198,7 @@ module Fluent
199
198
  def initialize
200
199
  super
201
200
  @counter_mutex = Mutex.new
201
+ @flush_thread_mutex = Mutex.new
202
202
  @buffering = false
203
203
  @delayed_commit = false
204
204
  @as_secondary = false
@@ -378,6 +378,7 @@ module Fluent
378
378
  buffer_conf = conf.elements(name: 'buffer').first || Fluent::Config::Element.new('buffer', '', {}, [])
379
379
  @buffer = Plugin.new_buffer(buffer_type, parent: self)
380
380
  @buffer.configure(buffer_conf)
381
+ keep_buffer_config_compat
381
382
  @buffer.enable_update_timekeys if @chunk_key_time
382
383
 
383
384
  @flush_at_shutdown = @buffer_config.flush_at_shutdown
@@ -425,7 +426,9 @@ module Fluent
425
426
  end
426
427
  @secondary.acts_as_secondary(self)
427
428
  @secondary.configure(secondary_conf)
428
- if (self.class != @secondary.class) && (@custom_format || @secondary.implement?(:custom_format))
429
+ if (@secondary.class.to_s != "Fluent::Plugin::SecondaryFileOutput") &&
430
+ (self.class != @secondary.class) &&
431
+ (@custom_format || @secondary.implement?(:custom_format))
429
432
  log.warn "Use different plugin for secondary. Check the plugin works with primary like secondary_file", primary: self.class.to_s, secondary: @secondary.class.to_s
430
433
  end
431
434
  else
@@ -435,6 +438,12 @@ module Fluent
435
438
  self
436
439
  end
437
440
 
441
+ def keep_buffer_config_compat
442
+ # Need this to call `@buffer_config.disable_chunk_backup` just as before,
443
+ # since some plugins may use this option in this way.
444
+ @buffer_config[:disable_chunk_backup] = @buffer.disable_chunk_backup
445
+ end
446
+
438
447
  def start
439
448
  super
440
449
 
@@ -591,6 +600,42 @@ module Fluent
591
600
  super
592
601
  end
593
602
 
603
+ def actual_flush_thread_count
604
+ return 0 unless @buffering
605
+ return @buffer_config.flush_thread_count unless @as_secondary
606
+ @primary_instance.buffer_config.flush_thread_count
607
+ end
608
+
609
+ # Ensures `path` (filename or filepath) processable
610
+ # only by the current thread in the current process.
611
+ # For multiple workers, the lock is shared if `path` is the same value.
612
+ # For multiple threads, the lock is shared by all threads in the same process.
613
+ def synchronize_path(path)
614
+ synchronize_path_in_workers(path) do
615
+ synchronize_in_threads do
616
+ yield
617
+ end
618
+ end
619
+ end
620
+
621
+ def synchronize_path_in_workers(path)
622
+ need_worker_lock = system_config.workers > 1
623
+ if need_worker_lock
624
+ acquire_worker_lock(path) { yield }
625
+ else
626
+ yield
627
+ end
628
+ end
629
+
630
+ def synchronize_in_threads
631
+ need_thread_lock = actual_flush_thread_count > 1
632
+ if need_thread_lock
633
+ @flush_thread_mutex.synchronize { yield }
634
+ else
635
+ yield
636
+ end
637
+ end
638
+
594
639
  def support_in_v12_style?(feature)
595
640
  # for plugins written in v0.12 styles
596
641
  case feature
@@ -779,7 +824,7 @@ module Fluent
779
824
  if str.include?('${tag}')
780
825
  rvalue = rvalue.gsub('${tag}', metadata.tag)
781
826
  end
782
- if str =~ CHUNK_TAG_PLACEHOLDER_PATTERN
827
+ if CHUNK_TAG_PLACEHOLDER_PATTERN.match?(str)
783
828
  hash = {}
784
829
  tag_parts = metadata.tag.split('.')
785
830
  tag_parts.each_with_index do |part, i|
@@ -1240,18 +1285,10 @@ module Fluent
1240
1285
  end
1241
1286
 
1242
1287
  def backup_chunk(chunk, using_secondary, delayed_commit)
1243
- if @buffer_config.disable_chunk_backup
1288
+ if @buffer.disable_chunk_backup
1244
1289
  log.warn "disable_chunk_backup is true. #{dump_unique_id_hex(chunk.unique_id)} chunk is thrown away"
1245
1290
  else
1246
- unique_id = dump_unique_id_hex(chunk.unique_id)
1247
- safe_plugin_id = plugin_id.gsub(/[ "\/\\:;|*<>?]/, '_')
1248
- backup_base_dir = system_config.root_dir || DEFAULT_BACKUP_DIR
1249
- backup_file = File.join(backup_base_dir, 'backup', "worker#{fluentd_worker_id}", safe_plugin_id, "#{unique_id}.log")
1250
- backup_dir = File.dirname(backup_file)
1251
-
1252
- log.warn "bad chunk is moved to #{backup_file}"
1253
- FileUtils.mkdir_p(backup_dir, mode: system_config.dir_permission || Fluent::DEFAULT_DIR_PERMISSION) unless Dir.exist?(backup_dir)
1254
- File.open(backup_file, 'ab', system_config.file_permission || Fluent::DEFAULT_FILE_PERMISSION) { |f|
1291
+ @buffer.backup(chunk.unique_id) { |f|
1255
1292
  chunk.write_to(f)
1256
1293
  }
1257
1294
  end
@@ -60,7 +60,7 @@ module Fluent
60
60
  rescue LoadError => ex
61
61
  name = :yajl
62
62
  if log
63
- if /\boj\z/ =~ ex.message
63
+ if /\boj\z/.match?(ex.message)
64
64
  log.info "Oj is not installed, and failing back to Yajl for json parser"
65
65
  else
66
66
  log.warn ex.message
@@ -99,7 +99,7 @@ module Fluent
99
99
 
100
100
  def shutdown
101
101
  @_event_loop_mutex.synchronize do
102
- @_event_loop_attached_watchers.reverse.each do |w|
102
+ @_event_loop_attached_watchers.reverse_each do |w|
103
103
  if w.attached?
104
104
  begin
105
105
  w.detach
@@ -116,7 +116,7 @@ module Fluent
116
116
  def after_shutdown
117
117
  timeout_at = Fluent::Clock.now + EVENT_LOOP_SHUTDOWN_TIMEOUT
118
118
  @_event_loop_mutex.synchronize do
119
- @_event_loop.watchers.reverse.each do |w|
119
+ @_event_loop.watchers.reverse_each do |w|
120
120
  begin
121
121
  w.detach
122
122
  rescue => e
@@ -21,6 +21,7 @@ require 'async/http/endpoint'
21
21
  require 'fluent/plugin_helper/http_server/app'
22
22
  require 'fluent/plugin_helper/http_server/router'
23
23
  require 'fluent/plugin_helper/http_server/methods'
24
+ require 'fluent/log/console_adapter'
24
25
 
25
26
  module Fluent
26
27
  module PluginHelper
@@ -38,7 +39,7 @@ module Fluent
38
39
  scheme = tls_context ? 'https' : 'http'
39
40
  @uri = URI("#{scheme}://#{@addr}:#{@port}").to_s
40
41
  @router = Router.new(default_app)
41
- @reactor = Async::Reactor.new(nil, logger: @logger)
42
+ @reactor = Async::Reactor.new(nil, logger: Fluent::Log::ConsoleAdapter.wrap(@logger))
42
43
 
43
44
  opts = if tls_context
44
45
  { ssl_context: tls_context }