From a239f3b792d4dc0e665e0f0fc78d04aa830908e8 Mon Sep 17 00:00:00 2001 From: Qingyuan Wang <146524024+NUAAwqy@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:35:34 +0800 Subject: [PATCH] time: fix parse_float with ten or more fractional digits (#5575) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Which issue(s) this PR fixes**: None — self-found while auditing `NumericTimeParser#parse_float`. **What this PR does / why we need it**: `NumericTimeParser#parse_float` (`lib/fluent/time.rb`) kept **ten** characters of the fractional part (`nsec_s[0..9]`), so a timestamp with ten or more decimal digits stored an `nsec` of `1_000_000_000` or more. `EventTime` assumes `nsec` fits inside a second: `to_f`/`to_r` report the wrong instant, `to_time` normalises into the next second, `%N` prints ten digits, and `to_msgpack_ext` forwards the oversized field as-is, so other fluentd nodes inherit it. Of fraction lengths 1..20, eleven produce an out-of-range `nsec`: ``` Fluent::NumericTimeParser.new(:float).parse("1700000000.1234567890").to_f # => 1700000001.2345679 (should be 1700000000.123456789) ``` **The fix**: keep the first nine fraction digits (`nsec_s[0, 9]`, sub-nanosecond digits dropped, which is what `Time#nsec` does). Padding and caching behaviour are untouched, so the hot path keeps its shape. Per the review, the negative-timestamp adjustment introduced earlier in this PR was dropped again: it changed how negative values interact with existing formatting and MessagePack serialization, and `EventTime` is also defined in the [Forward protocol](https://github.com/fluent/fluentd/wiki/Forward-Protocol-Specification-v1#eventtime-ext-format), so changing its encoding needs a separate discussion. Negative values now parse exactly as before this PR. **Validation** (Ruby 3.3.6, `TZ=UTC`): | check | result | | --- | --- | | new test (`test/test_time_parser.rb`) | 31 tests, **0 failures, 0 errors** | | regression slice: test_time_formatter 77, test_event_time 22, test_formatter 36, test_mixin 21, plugin/test_parser 23, plugin/test_parser_json 66, plugin/test_parser_apache 3, plugin_helper/test_compat_parameters 10, plugin/test_out_exec_filter 22 | all **0 failures, 0 errors** | | rubocop 1.92.1 + rubocop-performance 1.27.0 | no offenses | `plugin/test_in_exec` reports 2 failures (`can handle non-ascii characters`) that reproduce identically at `f984d65` with the fix reverted — the flaky encoding tests pinned in #5562/#5572, unrelated to time parsing. **Docs Changes**: None needed. **Release Note**: time: fix parse_float with ten or more fractional digits * With `time_type float`, a time string with ten or more fractional digits was parsed into a wrong time. Digits beyond nanoseconds are now dropped. --------- Signed-off-by: Qingyuan Wang <162230229@nuaa.edu.cn> Signed-off-by: NUAAwqy Co-authored-by: NUAAwqy Signed-off-by: github-actions[bot] --- lib/fluent/time.rb | 2 +- test/test_time_parser.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/fluent/time.rb b/lib/fluent/time.rb index e535a027ab..58b8b71677 100644 --- a/lib/fluent/time.rb +++ b/lib/fluent/time.rb @@ -346,7 +346,7 @@ def parse_float(value) begin sec_s, nsec_s, _ = value.to_s.split('.', 3) # throw away second-dot and later - nsec_s = nsec_s && nsec_s[0..9] || '0' + nsec_s = nsec_s && nsec_s[0, 9] || '0' # nanoseconds: 9 digits at most nsec_s += '0' * (9 - nsec_s.size) if nsec_s.size < 9 time = Fluent::EventTime.new(sec_s.to_i, nsec_s.to_i) rescue => e diff --git a/test/test_time_parser.rb b/test/test_time_parser.rb index 7575a2e24d..6c4ac1a19e 100644 --- a/test/test_time_parser.rb +++ b/test/test_time_parser.rb @@ -359,4 +359,21 @@ class DummyForTimeParser assert_equal([expected, expected + 1], [time1, time2]) end + + # A float timestamp's fraction is nanoseconds: at most 9 digits, and always below + # 1_000_000_000 like every other EventTime producer. + def test_parse_float_keeps_nine_digits_of_the_fraction + parser = Fluent::NumericTimeParser.new(:float) + + assert_equal_event_time(Fluent::EventTime.new(1700000000, 123456789), parser.parse('1700000000.123456789')) + assert_equal_event_time(Fluent::EventTime.new(1700000000, 123456789), parser.parse('1700000000.1234567890')) + assert_equal_event_time(Fluent::EventTime.new(1700000000, 999999999), parser.parse('1700000000.9999999999')) + assert_equal_event_time(Fluent::EventTime.new(0, 0), parser.parse('0.0000000001')) + + (1..20).each do |n| + time = parser.parse("1700000000.#{'9' * n}") + assert_true time.nsec < 1_000_000_000, "nsec #{time.nsec} is out of range with #{n} digits of fraction" + end + end + end