HyukjinKwon commented on code in PR #59282:
URL: https://github.com/apache/spark/pull/59282#discussion_r4246824969
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/python/RowQueueSuite.scala:
##########
@@ -170,4 +173,122 @@ class RowQueueSuite extends SparkFunSuite with
EncryptionFunSuite {
queue.close()
}
}
+
+ // Small pages so that a few rows span several in-memory queues.
+ private val smallPageSize = 1024L
+ private val smallPageConf = new SparkConf(false).set(BUFFER_PAGESIZE,
smallPageSize)
+
+ /** Creates two distinct queues with the same constructor arguments. */
+ private def queuesWithSameArguments(
+ taskM: TaskMemoryManager): (HybridRowQueue, HybridRowQueue) = {
+ val serManager = createSerializerManager(smallPageConf)
+ val tempDir = Utils.createTempDir().getCanonicalFile
+ // The second queue uses named arguments to check that they match the
former case class.
+ (HybridRowQueue(taskM, tempDir, 1, serManager),
+ HybridRowQueue(memManager = taskM, tempDir = tempDir, numFields = 1,
serMgr = serManager))
+ }
+
+ private def addRows(queue: HybridRowQueue, n: Int): Unit = {
+ val row = new UnsafeRow(1)
+ row.pointTo(new Array[Byte](16), 16)
+ (0 until n).foreach { i =>
+ row.setLong(0, i)
+ assert(queue.add(row) === QueueMode.IN_MEMORY)
+ }
+ }
+
+ private def drainAndClose(queue: HybridRowQueue, n: Int): Unit = {
+ (0 until n).foreach { i =>
+ assert(queue.remove().getLong(0) === i)
+ }
+ queue.close()
+ }
+
+ test("hybrid queues with the same fields are distinct memory consumers") {
+ val mem = new TestMemoryManager(smallPageConf)
+ val taskM = new TaskMemoryManager(mem, 0)
+ val (queue1, queue2) = queuesWithSameArguments(taskM)
+ assert(queue1 != queue2)
+ assert(queue1.toString.startsWith("HybridRowQueue(numFields=1,
lockFree=false)@"))
+ assert(queue1.toString != queue2.toString)
+
+ // queue1 is registered first and holds a single page, which it never
spills because the
+ // last queue is kept for writing. queue2 holds several pages and can
release all but one.
+ addRows(queue1, 1)
+ val n = 150
+ addRows(queue2, n)
+ assert(queue2.numQueues() > 2)
+ val queue2Used = queue2.getUsed
+
+ // Memory pressure from another consumer must be able to spill queue2.
+ mem.limit(0)
+ val consumer = new TestMemoryConsumer(taskM)
+ consumer.use(smallPageSize)
+ assert(consumer.getUsed === smallPageSize)
+ assert(queue2.getUsed === smallPageSize)
+ assert(queue2.getUsed < queue2Used)
+ consumer.free(smallPageSize)
+
+ drainAndClose(queue1, 1)
+ drainAndClose(queue2, n)
+ assert(taskM.cleanUpAllAllocatedMemory() === 0)
+ }
+
+ test("hybrid queue spills for another queue with the same fields") {
+ val taskM = new TaskMemoryManager(new TestMemoryManager(smallPageConf), 0)
+ val (queue1, queue2) = queuesWithSameArguments(taskM)
+ val n = 150
+ addRows(queue1, n)
+ assert(queue1.numQueues() > 2)
+ val used = queue1.getUsed
+ assert(queue1.spill(Long.MaxValue, queue2) === used - smallPageSize)
+ assert(queue1.spill(Long.MaxValue, queue1) === 0)
Review Comment:
**Nit (P3):** This assertion runs right after `queue1.spill(Long.MaxValue,
queue2)` has moved every in-memory buffer except the last to disk, so queue1
now holds [disk, disk, lastInMemory]. `spill` never moves disk buffers or the
last buffer, so this call returns 0 even if the `trigger eq this` short-circuit
is removed. No other test spills a queue for itself while it still has
spillable pages: the older tests pass `trigger = null`.
Could we check the self-triggered spill first, while queue1 still holds
several in-memory buffers? For example:
```scala
val used = queue1.getUsed
assert(queue1.spill(Long.MaxValue, queue1) === 0)
assert(queue1.getUsed === used)
assert(queue1.spill(Long.MaxValue, queue2) === used - smallPageSize)
```
**Verification:**
- **Regression:** With the self-trigger short-circuit removed from
HybridQueue.spill, the self-triggered spill check fails because in-memory
buffers move to disk and memory is released.
- **Behavior:** With the short-circuit in place, a self-triggered spill on a
queue with several in-memory buffers releases nothing and keeps its buffers,
and a spill triggered by a distinct equal-argument queue releases all but the
last page.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/RowQueue.scala:
##########
@@ -181,8 +181,13 @@ private[python] case class DiskRowQueue(
*
* HybridRowQueue could be safely appended in one thread, and pulled in
another thread in the same
* time.
+ *
+ * This is intentionally not a case class. As a memory consumer it must use
identity equality:
+ * otherwise two queues created with the same arguments in one task are equal,
so only one of
+ * them is tracked (and offered for spilling) by the [[TaskMemoryManager]],
and each refuses to
+ * spill when the other one triggers it.
*/
-case class HybridRowQueue(
+class HybridRowQueue(
Review Comment:
Thanks, verified in the current head. `close()` now swaps `queues` under the
lock and closes the detached buffers after releasing it, so it no longer holds
the queue monitor while calling `TaskMemoryManager.freePage`. A spill that runs
after the swap sees an empty list. The new test forces the two lock orders to
overlap and asserts the handshake and the spill result, so this concern is
resolved.
<!-- SPARK_DEV_REVIEW_REPLY
{"feedback_id":"inline:4228283943","thread_id":"inline:4228283943","verdict_sha256":"7782a87aad767a49068a837bf008a6a354048f59a8e28f0e2a3a92aae2c57855"}
-->
--
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]