Bettle created FLINK-41085:
------------------------------

             Summary: AppendOnlyTopNFunction emits an inconsistent changelog 
after cache eviction when sorting on NaN
                 Key: FLINK-41085
                 URL: https://issues.apache.org/jira/browse/FLINK-41085
             Project: Flink
          Issue Type: Bug
            Reporter: Bettle


Related issues: FLINK-40927 and FLINK-27627.

Changing only the TopN cache capacity changes the emitted changelog when the 
sort field contains NaN. After cache eviction, rebuilding the buffer from keyed 
state can silently lose a record and subsequently emit an additional INSERT 
without a corresponding DELETE.

*Environment*
||Setting||Value||
|Flink version|1.20.3 release dependencies|
|Java|Temurin 8u492|
|Operator|AppendOnlyTopNFunction|
|State backend|HashMapStateBackend|
|State TTL|Disabled|
|Input|Append-only|
|Partition key|city|
|Sort order|amount DOUBLE DESC|
|Rank range|ROW_NUMBER, [1, 2]|
|Output rank column|Disabled|
|Offset|None|

The reproduction uses a Java operator test harness with released operator 
bytecode, not copied or modified operator sources. Partition and sort keys are 
generated through KeySelectorUtil.getRowDataSelector(), as used by 
StreamExecRank, producing BinaryRowData keys.

*Reproduction*

Process the following records in order:
||Step||City||Record||Amount||
|1|Beijing|A|NaN|
|2|Beijing|B|20.0|
|3|Shanghai|X|77.0|
|4|Beijing|C|19.0|

Run the same input twice, changing only the cacheSize constructor argument:
||Run||cacheSize||Cache behavior||
|Without eviction|10000|Beijing's buffer remains cached|
|With eviction|2|For Top2, only one partition is cached; Shanghai evicts 
Beijing|

When C19 arrives in the second run, Beijing's buffer must be reconstructed from 
state. TTL is disabled, so no state expiration is involved.

*Actual output*

The following table shows only the Beijing changelog:
||Output order||Without cache eviction||With cache eviction||
|1|INSERT A(NaN)|INSERT A(NaN)|
|2|INSERT B(20.0)|INSERT B(20.0)|
|3|No further output|INSERT C(19.0)|
|DELETE emitted|None|None|
|Records retained downstream|2|3|

No DELETE accompanies the additional INSERT. A downstream consumer applying 
this changelog retains three Beijing records, although the requested result is 
Top2.

The assertion that both runs should produce identical changelogs failed with 
the same difference in three separate Maven executions.

*Observed state contents*

The actual MapState contents for Beijing were inspected during execution:
||After processing||State key||Stored records||
|A(NaN)|NaN|[A(NaN)]|
|B(20.0)|NaN|[A(NaN)]|
|B(20.0)|20.0|[A(NaN), B(20.0)]|

After B20, there are two distinct state entries. The original NaN entry remains 
unchanged, while the entry under 20.0 contains both records.

*Observed reconstruction sequence*

On Beijing's cache miss, a read-only tracer recorded the following actual 
loading sequence:
||Loading order||State key||Loaded records||Buffer after loading||Record count||
|First|20.0|[A(NaN), B(20.0)]|20.0 → [A(NaN), B(20.0)]|2|
|Second|NaN|[A(NaN)]|20.0 → [A(NaN)]|1|

The second load replaces the first list. The reconstructed buffer contains only 
one record, so C19 is subsequently admitted and emitted as an INSERT.

The same output difference was also verified without the tracing proxy.

*Cause*

The generated FLOAT/DOUBLE comparator uses the following expression:

a > b ? 1 : a < b ? -1 : 0

It returns 0 when comparing NaN with 20.0, although the generated binary state 
keys are not equal.

This creates inconsistent key semantics between the in-memory TopN buffer and 
MapState:
||Operation||Treatment of NaN and 20.0||
|MapState key equality|Different keys; both entries are retained|
|TopNBuffer TreeMap comparison|Comparator-equivalent keys; one entry is used|

AppendOnlyTopNFunction writes the complete comparator-equivalent group using 
the current input's sort key:

dataState.put(sortKey, new ArrayList<>(inputs));

During cache reconstruction, initHeapStates() iterates the state entries and 
calls:

buffer.putAll(sortKey, values);

TopNBuffer.putAll() calls TreeMap.put(). Because the comparator considers the 
keys equal, the later list replaces the earlier list. Consequently, the 
state-entry loading order can affect the reconstructed buffer and subsequent 
output.

*Expected behavior*

Changing the capacity of an optimization cache must not change the logical TopN 
contents or the emitted changelog.

Rebuilding the TopN buffer should preserve its logical contents and must not 
silently lose a previously emitted record.

The broader NaN ordering semantics may require discussion under FLINK-40927, 
but cache eviction should not change query results.

*Scope*

This is an operator-level reproduction using Flink 1.20.3 release dependencies, 
not an end-to-end SQL job. It does not involve checkpoint recovery.

If this consequence should be tracked directly under FLINK-40927, please feel 
free to mark this report as a duplicate.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to