[ 
https://issues.apache.org/jira/browse/SPARK-59509?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Wenchen Fan reassigned SPARK-59509:
-----------------------------------

    Assignee: Qing Fu

> Make UnsafeSorterIterator.getNumRecords() a consistent total
> ------------------------------------------------------------
>
>                 Key: SPARK-59509
>                 URL: https://issues.apache.org/jira/browse/SPARK-59509
>             Project: Spark
>          Issue Type: Bug
>          Components: Spark Core
>    Affects Versions: 5.0.0
>            Reporter: Qing Fu
>            Assignee: Qing Fu
>            Priority: Major
>              Labels: pull-request-available
>
> h3. The contract
> {{UnsafeSorterIterator.getNumRecords()}} is meant to be a fixed total. 
> SPARK-12295 introduced it and normalised two implementations to that meaning 
> in the same commit: it gave {{UnsafeSorterSpillReader}} a stable 
> {{numRecords}} field alongside its existing {{numRecordsRemaining}}, and 
> converted {{UnsafeInMemorySorter.SortedIterator.numRecordsLeft()}} into a 
> stable {{getNumRecords()}}. {{UnsafeExternalSorter.SpillableIterator}} was 
> the one implementation left wired to a field that counts down on every 
> {{loadNext()}}.
> || Implementation || {{getNumRecords()}} returns || Decrements on 
> {{loadNext()}}? ||
> | {{UnsafeSorterSpillReader}} | total from the spill file header | no |
> | {{UnsafeInMemorySorter.SortedIterator}} | total | no |
> | {{UnsafeSorterSpillMerger}} (anonymous) | sum of the counts added | no |
> | {{UnsafeExternalSorter.ChainedIterator}} | sum taken at construction | no |
> | {{UnsafeExternalSorter.SpillableIterator}} | records *remaining* | *yes* |
> h3. Consequences
> *1. The spill merger under-reports.* 
> {{UnsafeSorterSpillMerger.addSpillIfNotEmpty}} reads each input's count after 
> calling {{loadNext()}}, so a merged iterator that includes the in-memory 
> {{SpillableIterator}} reports one record fewer than it will produce, per such 
> iterator. Two call sites feed it one: 
> {{UnsafeExternalSorter.getSortedIterator()}}, and since SPARK-56410 the final 
> round of {{UnsafeSorterBoundedSpillMerger.merge()}}.
> Reproducer: two spills plus five records still in memory. 
> {{getSortedIterator().getNumRecords()}} returns 14, and the iterator then 
> yields 15 records.
> *2. {{getSortedIterator()}} is incoherent with itself.* Its no-spill branch 
> returns a {{SpillableIterator}}, whose count falls to zero as it is consumed; 
> its spilled branch returns a merged iterator reporting a stable total. Same 
> public method, two different meanings.
> h3. Impact
> None today, and this should be stated plainly. No caller in the repository 
> reads a merged iterator's {{getNumRecords()}}. Iteration has always been 
> correct, because the merged iterator's {{hasNext()}} is driven by the 
> priority queue rather than the counter, and every other read of 
> {{getNumRecords()}} is of a fresh, unconsumed iterator.
> Wrong since {{getNumRecords()}} was introduced in SPARK-12295 (2016-01-06); 
> first shipped in 2.0.0; never reported. In the ten years since, the only 
> edits to the relevant lines have been a line-length reflow and a comment typo.
> The reason to fix it now rather than leave it is that the undocumented split 
> contract is actively propagating: the open PR for SPARK-57714 adds a third 
> merger, {{UnsafeLoserTreeSpillMerger}}, with the same 
> {{loadNext()}}-then-count ordering, and feeds a {{SpillableIterator}} into it.
> h3. Proposed change
> Give {{SpillableIterator}} a {{totalRecords}} field for {{getNumRecords()}}, 
> leaving {{numRecords}} to count down for {{hasNext()}} and for sizing the 
> spill writer. Its {{getNumRecords()}} has exactly one caller, so this is 
> contained; it completes the normalisation SPARK-12295 started, resolves 
> consequence 2 as well, and makes the statement ordering in all three mergers 
> immaterial. Document the contract on the abstract method, including that the 
> total does not account for records an iterator was advanced past before being 
> handed on.
> h3. Related, not included
> * {{UnsafeSorterSpillMerger.numRecords}} is an unguarded {{int}} summed 
> across all spills, so a sort of more than {{Integer.MAX_VALUE}} records 
> reports a negative total. {{UnsafeSorterBoundedSpillMerger}} already sums in 
> a {{long}} with an explicit check. Worth a separate ticket.
> * {{ChainedIterator}} sums its children's totals, but 
> {{getIterator(startIndex)}} advances those children via {{moveOver()}} first, 
> so the sum includes skipped records. Consistent with the documented contract, 
> but the call site's use of it is misleading. Also unread today.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to