DanielLeens opened a new pull request, #12203:
URL: https://github.com/apache/seatunnel/pull/12203

   ### Purpose
   
   Adds regression test coverage for the `IMapFileData`/`IMapData` `Comparable` 
contract
   violation reported in #11462 and fixed by #11465. No production code is 
touched by
   this PR; it is test-only.
   
   ### The bug and its fix
   
   Before #11465, `IMapFileData.compareTo` was:
   
   ```java
   @Override
   public int compareTo(IMapFileData o) {
       return o.timestamp - this.timestamp > 0 ? 1 : -1;
   }
   ```
   
   That expression never returns `0`, so for two records sharing the exact same
   `timestamp` (realistic under batch writes, since `IMapFileStorage` stamps 
every
   record with `System.currentTimeMillis()`), both `compare(a, b)` and 
`compare(b, a)`
   evaluated to `-1`, violating `Comparable`'s antisymmetry requirement
   `sgn(compare(x, y)) == -sgn(compare(y, x))`. Two consequences followed:
   
   1. `WALReader.loadAllData`/`loadAllKeys` call `Collections.sort` on every 
full WAL
      read. Once enough same-timestamp records are present to push the sort past
      TimSort's binary-insertion fast path (its `MIN_MERGE` threshold of 32 
elements),
      the merge-invariant check throws `IllegalArgumentException: Comparison 
method
      violates its general contract!`, crashing IMap recovery.
   2. Even without throwing, the unspecified tie order let `WALReader`'s replay 
loop
      (which treats the first record it encounters per key, after sorting, as
      authoritative) silently drop a same-millisecond delete if a put for the 
same key
      happened to sort ahead of it -- the "lost delete" in the issue title.
   
   On current `dev`, the fix (`IMapDataComparator#compare`, shared by 
`IMapFileData`
   and `IMapData`) is:
   
   ```java
   int keyClassCompare = compareNullableString(leftKeyClassName, 
rightKeyClassName);
   if (keyClassCompare != 0) {
       return keyClassCompare;
   }
   int keyCompare = compareBytes(leftKey, rightKey);
   if (keyCompare != 0) {
       return keyCompare;
   }
   int timestampCompare = Long.compare(rightTimestamp, leftTimestamp);
   if (timestampCompare != 0) {
       return timestampCompare;
   }
   // deterministic tie-breaker for genuine same-millisecond collisions:
   int deleteCompare = Boolean.compare(rightDeleted, leftDeleted);
   if (deleteCompare != 0) {
       return deleteCompare;
   }
   int valueClassCompare = compareNullableString(leftValueClassName, 
rightValueClassName);
   if (valueClassCompare != 0) {
       return valueClassCompare;
   }
   return compareBytes(leftValue, rightValue);
   ```
   
   i.e. key class name, then key bytes, then timestamp descending, then (only 
for a
   genuine same-millisecond collision on the same key) 
deleted-before-not-deleted,
   then value class name, then value bytes -- a real total order that correctly
   returns `0` for equal records.
   
   ### Existing coverage vs. what this PR adds
   
   #11465 already added direct comparator-unit coverage
   (`IMapFileDataCompareToTest`, including a 4000-random-element
   `Collections.sort` crash check) and single-pair WAL replay coverage
   (`WALReaderAndWriterTest#testReplayKeepsTombstoneForSameKeyAndTimestamp`). 
This PR
   does not duplicate either; it adds a new class,
   `IMapFileDataTimestampCollisionRegressionTest`, with two complementary 
checks built
   around one shared 85-record, all-same-timestamp fixture (40 independent 
same-key
   put/delete pairs plus 5 uncontested control puts -- comfortably over 
TimSort's
   32-element merge threshold):
   
   - `testCollectionsSortOfLargeSameTimestampBatchSatisfiesComparableContract` 
sorts
     the batch directly and checks the contract holds across all 40 pairs
     *simultaneously* in one real `Collections.sort` call (not just for one 
isolated
     pair): per-pair antisymmetry, delete-before-put tie-break, and a full
     adjacent-element non-decreasing scan of the sorted output (a 
transitivity-implying
     self-consistency check over all 85 records).
   - `testWalReplayDoesNotLoseSameTimestampDeletesAcrossWriteOrders` writes the 
same 40
     pairs through the production `WALWriter`, deliberately alternating the 
physical
     append order per pair (put-before-delete for even-indexed keys,
     delete-before-put for odd-indexed keys), then replays them through the 
production
     `WALReader#loadAllData` and `#loadAllKeys` -- the exact methods the issue 
names as
     the crash site -- and asserts every delete is genuinely observed (the key 
is
     absent from both results) regardless of which physical order it was 
appended in,
     while the 5 control keys keep their values. This is the concrete "lost 
delete"
     proof, exercised at a scale that would have been able to trip the old 
crash too,
     and it additionally proves the outcome is now independent of WAL append 
order --
     closing the specific gap the old, contract-violating comparator left open
     (an antisymmetry-violating comparator gives `Collections.sort` license to 
place
     tied records in an unspecified order, so the old bug was not just "one 
write order
     loses the delete" but "the result was undefined").
   
   ### Verification
   
   Per this task's constraints, no local Maven compile/test/verify was run. Only
   `./mvnw spotless:apply -pl 
seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file
 -nsu -Dmaven.gitcommitid.skip=true`
   was run locally (module-scoped, no `-am`), and the diff is exactly one new 
file
   under `src/test/java` -- no `src/main` or `pom.xml` changes. Correctness was
   established by carefully reading the current `IMapDataComparator`, 
`IMapFileData`,
   `WALWriter`, `WALReader`, and `IMapFileStorage` source and tracing the exact
   sort/dedup logic by hand rather than by running the test locally. GitHub CI 
on this
   PR's head commit is the verification of record.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to