qingfureal opened a new pull request, #58877:
URL: https://github.com/apache/spark/pull/58877

   ### What changes were proposed in this pull request?
   
   Adds `closeOutputStream()` to the `Queue` trait, defaulting to a no-op, 
overrides it in
   `DiskRowQueue` to close and clear the writer, and calls it from 
`HybridQueue.spill()` once a
   queue has been drained.
   
   `DiskRowQueue.remove()` needed a related change: it used `out != null` as 
its "this is the
   first read" signal and opened the input stream inside that branch. Now that 
the output stream
   can already be closed before the first read, that branch no longer fires, so 
the check is
   keyed off `in == null` instead. Opening the input stream is also deferred 
until there is
   actually something to read, so an exhausted queue no longer opens a file 
just to return `null`.
   
   Follow-up on SPARK-53481, which extracted the generic `HybridQueue` / 
`Queue` classes.
   
   ### Why are the changes needed?
   
   `HybridQueue.spill()` converts every in-memory queue but the last into a 
disk-backed queue and
   drains the rows into it:
   
   ```scala
   val diskQueue = createDiskQueue()
   var item = queue.remove()
   while (item != null) {
     diskQueue.add(item)
     item = queue.remove()
   }
   released += getPageSize(queue)
   queue.close()
   diskQueue
   ```
   
   That queue is complete — only the last queue is ever written to — but 
nothing closes its
   output stream. `DiskRowQueue` opens the writer eagerly in its constructor:
   
   ```scala
   private var out = new DataOutputStream(serMgr.wrapForEncryption(
     new BufferedOutputStream(new FileOutputStream(file.toString))))
   ```
   
   and closes it only on the queue's *first read*, inside `remove()`. So after 
N spills a task
   holds N open `DataOutputStream` -> `BufferedOutputStream` -> 
`FileOutputStream` chains, plus
   the encryption wrapper when I/O encryption is enabled, each with its own 8 
KB buffer, until
   those queues are read. That is a file descriptor and a buffer per spilled 
queue retained at
   exactly the moment memory is scarce — spilling happens *because* the task is 
under memory
   pressure.
   
   It is directly observable as unflushed data. In the added test, after a 
spill the on-disk
   bytes grow from 8184 to 12264 on the first read: 4080 bytes of rows were 
still sitting in a
   retained buffer rather than on disk.
   
   ### Does this PR introduce _any_ user-facing change?
   
   No. This is an internal resource-lifetime fix in the Python UDF row queue; 
queue semantics and
   the rows returned are unchanged.
   
   ### How was this patch tested?
   
   Two tests added to `RowQueueSuite`, both run with I/O encryption on and off 
via
   `encryptionTest`:
   
   - `closeOutputStream flushes the queue and keeps it readable` — pins the new 
contract on
     `DiskRowQueue`: the rows reach disk, the call is idempotent, the queue 
then rejects further
     rows, and it is still fully readable afterwards.
   - `spilling does not hold an output stream per spilled queue` — spills a 
`HybridRowQueue`, then
     asserts the on-disk size does not change on the first read. If a spilled 
queue were still
     holding its output stream, that first read is what would close and flush 
it, and the file
     would grow.
   
   Each of the two behavioural hunks was confirmed to be load-bearing by 
reverting it
   individually:
   
   | Reverted hunk | Result |
   |---|---|
   | `diskQueue.closeOutputStream()` in the spill loop | Both encryption 
variants fail: `on-disk bytes grew from 8184 to 12264 on the first read` |
   | `remove()` keyed off `in` instead of `out` | `NullPointerException: Cannot 
invoke "java.io.DataInputStream.readInt()" because ... DiskRowQueue.in() is 
null` — and this also breaks the **pre-existing** `hybrid queue` tests, 8 
failures in total |
   
   The second row is the reason the `remove()` change is part of this PR rather 
than a separate
   cleanup: closing the output stream early without it is a hard regression.
   
   Also ran `RowQueueSuite`, `BatchEvalPythonExecSuite` and 
`EvaluatePythonSuite` (35 tests) and
   `scalastyle` on `sql/core`: all pass. `RowQueue.scala` holds the only 
implementations of
   `Queue` / `HybridQueue`, so the new default method changes no other queue.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code (Opus 5)
   


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


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

Reply via email to