Skip to content

time: fix TimeFormatter returning nil for the Unix epoch#5420

Open
Watson1978 wants to merge 1 commit into
fluent:masterfrom
Watson1978:fix/time-formatter-epoch
Open

time: fix TimeFormatter returning nil for the Unix epoch#5420
Watson1978 wants to merge 1 commit into
fluent:masterfrom
Watson1978:fix/time-formatter-epoch

Conversation

@Watson1978

@Watson1978 Watson1978 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Which issue(s) this PR fixes:
Fixes #

What this PR does / why we need it:
TimeFormatter caches the last two formatted timestamps in two slots, and an empty slot was marked by its key being 0. But 0 is a valid timestamp, so the epoch matched an empty slot and its nil string was returned as a cache hit:

Fluent::TimeFormatter.new(nil, false, nil).format(0)  #=> nil

format_with_subsec compares slots with EventTime.eq?, which falls back to EventTime#== and compares seconds alone, so there the whole first second of the epoch is affected:

fmt = Fluent::TimeFormatter.new("%Y%m%d %H%M%S.%N", false, nil)
fmt.format(Fluent::EventTime.new(0, 123456789))  #=> nil

The fix is to mark an empty slot by its cached string being nil rather than by its key.
format_nocache always returns a String, so a filled slot can never be mistaken for an empty one. Every other timestamp behaves exactly as before, since key 0 could never have matched it.

Impact is small, and I don't think this is urgent.
A slot keeps the key 0 only until it is written, so a formatter self-heals once it has formatted two distinct timestamps, within a second of the first record.
The exception is a stream whose timestamps are all the epoch: the early return never fills a slot, and out_file then writes every line with an empty time column.
That is reachable via time_type unixtime, which parses with value.to_i and turns an empty or non-numeric time_key into EventTime.new(0), but such a pipeline is misconfigured anyway. The reason to fix it is simply that nil is never a correct result.

Docs Changes:

Release Note:

  • time: fix TimeFormatter returning nil for the Unix epoch

TimeFormatter caches the last two formatted timestamps in two slots, and an
empty slot was marked by its key being 0. But 0 is a valid timestamp, so the
epoch matched the empty slot and the slot's nil string was returned instead
of a formatted time:

    Fluent::TimeFormatter.new(nil, false, nil).format(0)  #=> nil

Both cache paths are affected. format_without_subsec compares the key with
==, so only the epoch itself is hit. format_with_subsec compares it with
EventTime.eq?, which falls back to EventTime#== when either side is not an
EventTime, and that compares seconds alone -- so every timestamp within the
first second of the epoch is hit:

    fmt = Fluent::TimeFormatter.new("%Y%m%d %H%M%S.%N", false, nil)
    fmt.format(Fluent::EventTime.new(0, 123456789))  #=> nil

A slot stops carrying the key 0 once it is written, and the victim-selection
rule fills the second slot first, so the first slot keeps the key 0 until two
distinct timestamps have been formatted. The failure is therefore confined to
a cold cache, with one exception: a stream whose timestamps are all the epoch
returns early on every call, never fills a slot, and loses the time column on
every line for as long as the process runs.

Mark an empty slot by its cached string being nil instead. format_nocache
always returns a String, so a filled slot can never be mistaken for an empty
one, whatever its key.

Signed-off-by: Shizuo Fujita <fujita@clear-code.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant