Skip to content

Commit a60bede

Browse files
authored
config: fix duplicate config file loading in config_include_dir (#5228)
**Which issue(s) this PR fixes**: Fixes # Ref. #5100 **What this PR does / why we need it**: Currently, if a user explicitly `@include`s a file that is also located in `config_include_dir`, the file is loaded twice. This causes startup failures due to port conflicts or ID duplication. This commit introduces a deduplication mechanism using a callback: 1. Add `on_file_parsed` callback to `Fluent::Config.build` to track files loaded during the user config parsing phase. 2. In `Supervisor`, record loaded files via this callback. 3. When expanding `config_include_dir`, skip files that have already been loaded in the user configuration. **Docs Changes**: **Release Note**: --------- Signed-off-by: Shizuo Fujita <[email protected]>
1 parent a22ce7f commit a60bede

File tree

7 files changed

+446
-21
lines changed

7 files changed

+446
-21
lines changed

lib/fluent/config.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ module Config
2626
# @param additional_config [String] config which is added to last of config body
2727
# @param use_v1_config [Bool] config is formatted with v1 or not
2828
# @return [Fluent::Config]
29-
def self.build(config_path:, encoding: 'utf-8', additional_config: nil, use_v1_config: true, type: nil)
29+
def self.build(config_path:, encoding: 'utf-8', additional_config: nil, use_v1_config: true, type: nil, on_file_parsed: nil)
3030
if type == :guess
3131
config_file_ext = File.extname(config_path)
3232
if config_file_ext == '.yaml' || config_file_ext == '.yml'
@@ -35,7 +35,7 @@ def self.build(config_path:, encoding: 'utf-8', additional_config: nil, use_v1_c
3535
end
3636

3737
if type == :yaml || type == :yml
38-
return Fluent::Config::YamlParser.parse(config_path)
38+
return Fluent::Config::YamlParser.parse(config_path, on_file_parsed: on_file_parsed)
3939
end
4040

4141
config_fname = File.basename(config_path)
@@ -49,10 +49,10 @@ def self.build(config_path:, encoding: 'utf-8', additional_config: nil, use_v1_c
4949
s
5050
end
5151

52-
Fluent::Config.parse(config_data, config_fname, config_basedir, use_v1_config)
52+
Fluent::Config.parse(config_data, config_fname, config_basedir, use_v1_config, on_file_parsed: on_file_parsed)
5353
end
5454

55-
def self.parse(str, fname, basepath = Dir.pwd, v1_config = nil, syntax: :v1)
55+
def self.parse(str, fname, basepath = Dir.pwd, v1_config = nil, syntax: :v1, on_file_parsed: nil)
5656
parser = if fname =~ /\.rb$/ || syntax == :ruby
5757
:ruby
5858
elsif v1_config.nil?
@@ -68,7 +68,7 @@ def self.parse(str, fname, basepath = Dir.pwd, v1_config = nil, syntax: :v1)
6868
case parser
6969
when :v1
7070
require 'fluent/config/v1_parser'
71-
V1Parser.parse(str, fname, basepath, Kernel.binding)
71+
V1Parser.parse(str, fname, basepath, Kernel.binding, on_file_parsed: on_file_parsed)
7272
when :v0
7373
# TODO: show deprecated message in v1
7474
require 'fluent/config/parser'

lib/fluent/config/v1_parser.rb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,18 @@ module Config
2727
class V1Parser < LiteralParser
2828
ELEMENT_NAME = /[a-zA-Z0-9_]+/
2929

30-
def self.parse(data, fname, basepath = Dir.pwd, eval_context = nil)
30+
def self.parse(data, fname, basepath = Dir.pwd, eval_context = nil, on_file_parsed: nil)
3131
ss = StringScanner.new(data)
32-
ps = V1Parser.new(ss, basepath, fname, eval_context)
32+
ps = V1Parser.new(ss, basepath, fname, eval_context, on_file_parsed: on_file_parsed)
3333
ps.parse!
3434
end
3535

36-
def initialize(strscan, include_basepath, fname, eval_context)
36+
def initialize(strscan, include_basepath, fname, eval_context, on_file_parsed: nil)
3737
super(strscan, eval_context)
3838
@include_basepath = include_basepath
3939
@fname = fname
4040
@logger = defined?($log) ? $log : nil
41+
@on_file_parsed = on_file_parsed
4142
end
4243

4344
def parse!
@@ -138,6 +139,8 @@ def parse_element(root_element, elem_name, attrs = {}, elems = [])
138139
end
139140
end
140141

142+
@on_file_parsed&.call(File.expand_path(File.join(@include_basepath, @fname))) if root_element
143+
141144
return attrs, elems
142145
end
143146

@@ -166,7 +169,7 @@ def eval_include(attrs, elems, uri)
166169
data = File.read(entry)
167170
data.force_encoding('UTF-8')
168171
ss = StringScanner.new(data)
169-
V1Parser.new(ss, basepath, fname, @eval_context).parse_element(true, nil, attrs, elems)
172+
V1Parser.new(ss, basepath, fname, @eval_context, on_file_parsed: @on_file_parsed).parse_element(true, nil, attrs, elems)
170173
}
171174
else
172175
require 'open-uri'
@@ -175,7 +178,7 @@ def eval_include(attrs, elems, uri)
175178
data = u.open { |f| f.read }
176179
data.force_encoding('UTF-8')
177180
ss = StringScanner.new(data)
178-
V1Parser.new(ss, basepath, fname, @eval_context).parse_element(true, nil, attrs, elems)
181+
V1Parser.new(ss, basepath, fname, @eval_context, on_file_parsed: @on_file_parsed).parse_element(true, nil, attrs, elems)
179182
end
180183
rescue SystemCallError => e
181184
cpe = ConfigParseError.new("include error #{uri} - #{e}")

lib/fluent/config/yaml_parser.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
module Fluent
2222
module Config
2323
module YamlParser
24-
def self.parse(path)
24+
def self.parse(path, on_file_parsed: nil)
2525
context = Kernel.binding
2626

2727
unless context.respond_to?(:use_nil)
@@ -48,7 +48,7 @@ def self.parse(path)
4848
end
4949
end
5050

51-
s = Fluent::Config::YamlParser::Loader.new(context).load(Pathname.new(path))
51+
s = Fluent::Config::YamlParser::Loader.new(context, on_file_parsed: on_file_parsed).load(Pathname.new(path))
5252
Fluent::Config::YamlParser::Parser.new(s).build.to_element
5353
end
5454
end

lib/fluent/config/yaml_parser/loader.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ class Loader
3030
FLUENT_STR_TAG = 'tag:fluent/s'.freeze
3131
SHOVEL = '<<'.freeze
3232

33-
def initialize(context = Kernel.binding)
33+
def initialize(context = Kernel.binding, on_file_parsed: nil)
3434
@context = context
3535
@current_path = nil
36+
@on_file_parsed = on_file_parsed
3637
end
3738

3839
# @param [String] path
@@ -55,9 +56,13 @@ def load(path)
5556
Fluent::Config::YamlParser::FluentValue::StringValue.new(val, @context)
5657
end
5758

58-
path.open do |f|
59+
config = path.open do |f|
5960
visitor.accept(Psych.parse(f))
6061
end
62+
63+
@on_file_parsed&.call(File.expand_path(path.to_s))
64+
65+
config
6166
end
6267

6368
def eval_include(path, parent)

lib/fluent/supervisor.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
require 'open3'
1919
require 'pathname'
2020
require 'find'
21+
require 'set'
2122

2223
require 'fluent/config'
2324
require 'fluent/counter'
@@ -796,18 +797,20 @@ def configure(supervisor: false)
796797
$log.warn('the value "-" for `inline_config` is deprecated. See https://github.com/fluent/fluentd/issues/2711')
797798
@inline_config = STDIN.read
798799
end
800+
parsed_files = Set.new
799801
@conf = Fluent::Config.build(
800802
config_path: @config_path,
801803
encoding: @conf_encoding,
802804
additional_config: @inline_config,
803805
use_v1_config: @use_v1_config,
804806
type: @config_file_type,
807+
on_file_parsed: ->(path) { parsed_files << path },
805808
)
806809
@system_config = build_system_config(@conf)
807810

808811
$log.info :supervisor, 'parsing config file is succeeded', path: @config_path
809812

810-
build_additional_configurations do |additional_conf|
813+
build_additional_configurations(parsed_files) do |additional_conf|
811814
@conf += additional_conf
812815
end
813816

@@ -854,6 +857,7 @@ def setup_global_logger(supervisor: false)
854857
additional_config: @inline_config,
855858
use_v1_config: @use_v1_config,
856859
type: @config_file_type,
860+
on_file_parsed: nil,
857861
)
858862
system_config = build_system_config(conf)
859863

@@ -1088,15 +1092,17 @@ def reload_config
10881092
$log.debug('worker got SIGUSR2')
10891093

10901094
begin
1095+
parsed_files = Set.new
10911096
conf = Fluent::Config.build(
10921097
config_path: @config_path,
10931098
encoding: @conf_encoding,
10941099
additional_config: @inline_config,
10951100
use_v1_config: @use_v1_config,
10961101
type: @config_file_type,
1102+
on_file_parsed: ->(path) { parsed_files << path },
10971103
)
10981104

1099-
build_additional_configurations do |additional_conf|
1105+
build_additional_configurations(parsed_files) do |additional_conf|
11001106
conf += additional_conf
11011107
end
11021108

@@ -1206,7 +1212,7 @@ def build_system_config(conf)
12061212
system_config
12071213
end
12081214

1209-
def build_additional_configurations
1215+
def build_additional_configurations(parsed_files)
12101216
if @system_config.config_include_dir&.empty?
12111217
$log.info :supervisor, 'configuration include directory is disabled'
12121218
return
@@ -1218,11 +1224,17 @@ def build_additional_configurations
12181224
next unless supported_suffixes.include?(File.extname(path))
12191225
# NOTE: both types of normal config (.conf) and YAML will be loaded.
12201226
# Thus, it does not care whether @config_path is .conf or .yml.
1227+
if parsed_files.include?(path)
1228+
$log.info :supervisor, 'skip auto loading, it was already loaded', path: path
1229+
next
1230+
end
1231+
12211232
$log.info :supervisor, 'loading additional configuration file', path: path
12221233
yield Fluent::Config.build(config_path: path,
12231234
encoding: @conf_encoding,
12241235
use_v1_config: @use_v1_config,
1225-
type: :guess)
1236+
type: :guess,
1237+
on_file_parsed: nil)
12261238
end
12271239
rescue Errno::ENOENT
12281240
$log.info :supervisor, 'inaccessible include directory was specified', path: @system_config.config_include_dir

test/test_config.rb

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,5 +520,118 @@ def write_config(path, data, encoding: 'utf-8')
520520
assert_equal('value', c['key'])
521521
assert_equal('value2', c['key2'])
522522
end
523+
524+
sub_test_case 'on_file_parsed' do
525+
test "calling order on normal configuration files" do
526+
write_config("#{TMP_DIR}/build/common_param.conf", <<~EOS)
527+
flush_interval 5s
528+
total_limit_size 100m
529+
chunk_limit_size 1m
530+
EOS
531+
write_config("#{TMP_DIR}/build/server.conf", <<~EOS)
532+
<server>
533+
host 127.0.0.1
534+
port 24224
535+
</server>
536+
EOS
537+
write_config("#{TMP_DIR}/build/forward.conf", <<~EOS)
538+
<match test.*>
539+
@type forward
540+
541+
@include server.conf
542+
</match>
543+
EOS
544+
write_config("#{TMP_DIR}/build/inline.conf", <<~EOS)
545+
<source>
546+
@type stdout
547+
tag test
548+
</source>
549+
EOS
550+
write_config("#{TMP_DIR}/build/fluent.conf", <<~EOS)
551+
<match sample.*>
552+
@type file
553+
<buffer>
554+
@include common_param.conf
555+
</buffer>
556+
</match>
557+
<match debug.*>
558+
@type stdout
559+
<buffer>
560+
@include common_param.conf
561+
</buffer>
562+
</match>
563+
564+
@include forward.conf
565+
EOS
566+
567+
# parsed_files contains file paths in the order of file parsed
568+
parsed_files = []
569+
Fluent::Config.build(
570+
config_path: "#{TMP_DIR}/build/fluent.conf",
571+
additional_config: "@include inline.conf",
572+
on_file_parsed: ->(path) { parsed_files << path },
573+
)
574+
575+
assert_equal(
576+
[
577+
"#{TMP_DIR}/build/common_param.conf",
578+
"#{TMP_DIR}/build/common_param.conf",
579+
"#{TMP_DIR}/build/server.conf",
580+
"#{TMP_DIR}/build/forward.conf",
581+
"#{TMP_DIR}/build/inline.conf",
582+
"#{TMP_DIR}/build/fluent.conf"
583+
],
584+
parsed_files
585+
)
586+
end
587+
588+
test "calling order on YAML configuration files" do
589+
write_config("#{TMP_DIR}/build/common_buffer.yaml", <<~EOS)
590+
- buffer:
591+
flush_interval: 5s
592+
total_limit_size: 100m
593+
chunk_limit_size: 1m
594+
EOS
595+
write_config("#{TMP_DIR}/build/forward.yaml", <<~EOS)
596+
- match:
597+
$tag: test.*
598+
server:
599+
host: 127.0.0.1
600+
port: 24224
601+
EOS
602+
write_config("#{TMP_DIR}/build/fluent.yaml", <<~EOS)
603+
config:
604+
- match:
605+
$tag: sample.*
606+
$type: file
607+
<<: !include common_buffer.yaml
608+
- match:
609+
$tag: debug.*
610+
$type: stdout
611+
<<: !include common_buffer.yaml
612+
- !include forward.yaml
613+
EOS
614+
615+
# parsed_files contains file paths in the order of file parsed
616+
# `additional_config` does not support YAML config
617+
parsed_files = []
618+
Fluent::Config.build(
619+
config_path: "#{TMP_DIR}/build/fluent.yaml",
620+
type: :yaml,
621+
on_file_parsed: ->(path) { parsed_files << path },
622+
)
623+
624+
assert_equal(
625+
[
626+
"#{TMP_DIR}/build/common_buffer.yaml",
627+
"#{TMP_DIR}/build/common_buffer.yaml",
628+
"#{TMP_DIR}/build/forward.yaml",
629+
"#{TMP_DIR}/build/fluent.yaml"
630+
],
631+
parsed_files
632+
)
633+
end
634+
635+
end
523636
end
524637
end

0 commit comments

Comments
 (0)