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

ASF GitHub Bot updated SPARK-59509:
-----------------------------------
    Labels: pull-request-available  (was: )

> Fix record undercount in UnsafeSorterSpillMerger and document the 
> getNumRecords() contract
> ------------------------------------------------------------------------------------------
>
>                 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
>            Priority: Major
>              Labels: pull-request-available
>
> h3. The contract
> {{UnsafeSorterIterator.getNumRecords()}} has two contracts, and the abstract 
> method documents neither. Four of the five implementations report the *total* 
> number of records and never change it as the iterator is consumed:
> || 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* |
> The two readings agree only while an iterator is untouched.
> h3. The bug
> {{UnsafeSorterSpillMerger.addSpillIfNotEmpty}} reads the count *after* 
> calling {{loadNext()}}:
> {code:java}
> public void addSpillIfNotEmpty(UnsafeSorterIterator spillReader) throws 
> IOException {
>   if (spillReader.hasNext()) {
>     spillReader.loadNext();
>     priorityQueue.add(spillReader);
>     numRecords += spillReader.getNumRecords();
>   }
> }
> {code}
> {{UnsafeExternalSorter.getSortedIterator()}} adds its in-memory 
> {{SpillableIterator}} to the merger alongside the spill readers, so the 
> merged iterator reports one record fewer than it will actually produce, per 
> in-memory iterator.
> Reproducer: two spills plus five records still in memory. 
> {{getSortedIterator().getNumRecords()}} returns 14, and the iterator then 
> yields 15 records.
> h3. Impact
> None today, and this should be stated plainly. A scan of the repository finds 
> no caller that reads a merged iterator's {{getNumRecords()}} - the value is 
> computed and never consumed. Iteration itself has always been correct, 
> because the merged iterator's {{hasNext()}} is driven by the priority queue 
> and not by the counter. The other three call sites that do read 
> {{getNumRecords()}} all read fresh, unconsumed iterators, so none of them is 
> affected.
> That is also why this has gone unnoticed since the line was introduced in 
> SPARK-12295 (2016).
> It is still a wrong value, and a trap for the next caller who hands a merged 
> iterator to {{UnsafeSorterSpillWriter}}, which enforces the declared count 
> and fails with {{IllegalStateException: Number of records written exceeded 
> numRecordsToWrite}}.
> h3. Proposed change
> Read the count before {{loadNext()}}, which is correct under either contract, 
> and document the contract on {{UnsafeSorterIterator.getNumRecords()}} so the 
> ambiguity that produced this does not produce the next one.



--
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