viirya commented on code in PR #59663:
URL: https://github.com/apache/spark/pull/59663#discussion_r4361885159


##########
core/src/test/scala/org/apache/spark/scheduler/TaskContextSuite.scala:
##########
@@ -931,6 +934,351 @@ class TaskContextSuite extends SparkFunSuite with 
BeforeAndAfter with LocalSpark
     completion
   }
 
+  private def startDaemonThread(name: String)(body: => Unit): Thread = {
+    val thread = new Thread(() => body, name)
+    thread.setDaemon(true)
+    thread.start()
+    thread
+  }
+
+  private def joinThread(thread: Thread): Unit = {
+    thread.join(TimeUnit.MINUTES.toMillis(1))
+    assert(!thread.isAlive, s"${thread.getName} is still running")
+  }
+
+  /**
+   * Fails the task on a new thread, whose failure listener then holds the 
listener invocation
+   * until the returned latch is counted down. Returns that latch and the 
thread.
+   */
+  private def holdListenerInvocation(context: TaskContext): (CountDownLatch, 
Thread) = {
+    val holding = new CountDownLatch(1)
+    val release = new CountDownLatch(1)
+    context.addTaskFailureListener(new TaskFailureListener {
+      override def onTaskFailure(context: TaskContext, error: Throwable): Unit 
= {
+        holding.countDown()
+        release.await(1, TimeUnit.MINUTES)
+      }
+    })
+    val holder = startDaemonThread("listener-holder") {
+      context.markTaskFailed(new Exception("failed on another thread"))
+    }
+    assert(holding.await(1, TimeUnit.MINUTES))
+    (release, holder)
+  }
+
+  /** Waits until `thread` waits in `markTaskCompleted()` for the listener 
invocation. */
+  private def awaitHandoverWait(thread: Thread): Unit = {
+    eventually(timeout(1.minute)) {
+      assert(thread.getState == Thread.State.TIMED_WAITING)
+    }
+  }
+
+  test("SPARK-60222: a listener added as completion starts does not take over 
the listeners") {
+    // A thread that sees the task completed and adds a listener at that 
moment must not become
+    // the thread that runs the pending listeners: the completing thread 
claims the listener
+    // invocation together with setting `completed`.
+    (1 to 500).foreach { _ =>
+      val context = TaskContext.empty()
+      val pendingListenerThread = new AtomicReference[Thread]()
+      context.addTaskCompletionListener[Unit] { _ =>
+        pendingListenerThread.set(Thread.currentThread())
+      }
+      val ready = new CountDownLatch(1)
+      val adder = startDaemonThread("adder") {
+        ready.countDown()
+        while (!context.isCompleted()) {}
+        context.addTaskCompletionListener[Unit] { _ => }
+      }
+      ready.await()
+      context.markTaskCompleted(None)
+      joinThread(adder)
+      assert(pendingListenerThread.get eq Thread.currentThread())
+    }
+  }
+
+  test("SPARK-60222: a failing listener does not let another thread take over 
the listeners") {
+    // While a failing listener's failure listeners run, the thread invoking 
the listeners must
+    // stay registered, so that another thread adding listeners at that moment 
does not start
+    // running the pending ones, possibly concurrently with this thread.
+    (1 to 200).foreach { _ =>
+      val context = TaskContext.empty()
+      val pendingListenerThread = new AtomicReference[Thread]()
+      context.addTaskCompletionListener[Unit] { _ =>
+        pendingListenerThread.set(Thread.currentThread())
+      }
+      // Two failing listeners, which run first: the second failure finds the 
task already failed.
+      (1 to 2).foreach { i =>
+        context.addTaskCompletionListener(new TaskCompletionListener {
+          override def onTaskCompletion(context: TaskContext): Unit = throw 
new Exception(s"$i")
+        })
+      }
+      context.addTaskFailureListener(new TaskFailureListener {
+        override def onTaskFailure(context: TaskContext, error: Throwable): 
Unit = {}
+      })
+      val ready = new CountDownLatch(1)
+      val adder = startDaemonThread("adder") {
+        ready.countDown()
+        while (!context.isCompleted()) {}
+        (1 to 200).foreach { _ => context.addTaskCompletionListener[Unit] { _ 
=> } }
+      }
+      ready.await()
+      intercept[TaskCompletionListenerException] {
+        context.markTaskCompleted(None)
+      }
+      joinThread(adder)
+      assert(pendingListenerThread.get eq Thread.currentThread())
+    }
+  }
+
+  test("SPARK-60222: completion waits for another thread invoking listeners 
and runs them") {
+    val context = TaskContext.empty()
+    val listenerThreads = new ConcurrentLinkedQueue[(String, Thread)]()
+    context.addTaskCompletionListener[Unit] { _ =>

Review Comment:
   Done, and `holdListenerInvocation` now takes the call that takes the 
invocation as a by-name parameter, so the kill setup uses it too.



##########
core/src/main/scala/org/apache/spark/TaskContextImpl.scala:
##########
@@ -97,6 +100,17 @@ private[spark] class TaskContextImpl(
    */
   @transient @volatile private var listenerInvocationThread: Option[Thread] = 
None

Review Comment:
   Since the re-check in item 9 reads it under the lock, I dropped `@volatile` 
and documented it as protected by the TaskContext lock.



##########
core/src/test/scala/org/apache/spark/scheduler/TaskContextSuite.scala:
##########
@@ -931,6 +934,351 @@ class TaskContextSuite extends SparkFunSuite with 
BeforeAndAfter with LocalSpark
     completion
   }
 
+  private def startDaemonThread(name: String)(body: => Unit): Thread = {
+    val thread = new Thread(() => body, name)
+    thread.setDaemon(true)
+    thread.start()
+    thread
+  }
+
+  private def joinThread(thread: Thread): Unit = {
+    thread.join(TimeUnit.MINUTES.toMillis(1))
+    assert(!thread.isAlive, s"${thread.getName} is still running")
+  }
+
+  /**
+   * Fails the task on a new thread, whose failure listener then holds the 
listener invocation
+   * until the returned latch is counted down. Returns that latch and the 
thread.
+   */
+  private def holdListenerInvocation(context: TaskContext): (CountDownLatch, 
Thread) = {
+    val holding = new CountDownLatch(1)
+    val release = new CountDownLatch(1)
+    context.addTaskFailureListener(new TaskFailureListener {
+      override def onTaskFailure(context: TaskContext, error: Throwable): Unit 
= {
+        holding.countDown()
+        release.await(1, TimeUnit.MINUTES)
+      }
+    })
+    val holder = startDaemonThread("listener-holder") {
+      context.markTaskFailed(new Exception("failed on another thread"))
+    }
+    assert(holding.await(1, TimeUnit.MINUTES))
+    (release, holder)
+  }
+
+  /** Waits until `thread` waits in `markTaskCompleted()` for the listener 
invocation. */
+  private def awaitHandoverWait(thread: Thread): Unit = {
+    eventually(timeout(1.minute)) {
+      assert(thread.getState == Thread.State.TIMED_WAITING)
+    }
+  }
+
+  test("SPARK-60222: a listener added as completion starts does not take over 
the listeners") {
+    // A thread that sees the task completed and adds a listener at that 
moment must not become
+    // the thread that runs the pending listeners: the completing thread 
claims the listener
+    // invocation together with setting `completed`.
+    (1 to 500).foreach { _ =>
+      val context = TaskContext.empty()
+      val pendingListenerThread = new AtomicReference[Thread]()
+      context.addTaskCompletionListener[Unit] { _ =>
+        pendingListenerThread.set(Thread.currentThread())
+      }
+      val ready = new CountDownLatch(1)
+      val adder = startDaemonThread("adder") {
+        ready.countDown()
+        while (!context.isCompleted()) {}
+        context.addTaskCompletionListener[Unit] { _ => }
+      }
+      ready.await()
+      context.markTaskCompleted(None)
+      joinThread(adder)
+      assert(pendingListenerThread.get eq Thread.currentThread())
+    }
+  }
+
+  test("SPARK-60222: a failing listener does not let another thread take over 
the listeners") {
+    // While a failing listener's failure listeners run, the thread invoking 
the listeners must
+    // stay registered, so that another thread adding listeners at that moment 
does not start
+    // running the pending ones, possibly concurrently with this thread.
+    (1 to 200).foreach { _ =>
+      val context = TaskContext.empty()
+      val pendingListenerThread = new AtomicReference[Thread]()
+      context.addTaskCompletionListener[Unit] { _ =>
+        pendingListenerThread.set(Thread.currentThread())
+      }
+      // Two failing listeners, which run first: the second failure finds the 
task already failed.
+      (1 to 2).foreach { i =>
+        context.addTaskCompletionListener(new TaskCompletionListener {
+          override def onTaskCompletion(context: TaskContext): Unit = throw 
new Exception(s"$i")
+        })
+      }
+      context.addTaskFailureListener(new TaskFailureListener {
+        override def onTaskFailure(context: TaskContext, error: Throwable): 
Unit = {}
+      })
+      val ready = new CountDownLatch(1)
+      val adder = startDaemonThread("adder") {
+        ready.countDown()
+        while (!context.isCompleted()) {}
+        (1 to 200).foreach { _ => context.addTaskCompletionListener[Unit] { _ 
=> } }
+      }
+      ready.await()
+      intercept[TaskCompletionListenerException] {
+        context.markTaskCompleted(None)
+      }
+      joinThread(adder)
+      assert(pendingListenerThread.get eq Thread.currentThread())
+    }
+  }
+
+  test("SPARK-60222: completion waits for another thread invoking listeners 
and runs them") {
+    val context = TaskContext.empty()
+    val listenerThreads = new ConcurrentLinkedQueue[(String, Thread)]()
+    context.addTaskCompletionListener[Unit] { _ =>
+      listenerThreads.add(("pending", Thread.currentThread()))
+    }
+    context.addTaskInterruptListener(new TaskInterruptListener {
+      override def onTaskInterrupted(context: TaskContext, reason: String): 
Unit =
+        throw new Exception("interrupt listener")
+    })
+    val holding = new CountDownLatch(1)
+    val release = new CountDownLatch(1)
+    context.addTaskFailureListener(new TaskFailureListener {
+      override def onTaskFailure(context: TaskContext, error: Throwable): Unit 
= {
+        holding.countDown()
+        release.await(1, TimeUnit.MINUTES)
+      }
+    })
+    // Like a kill: an interrupt listener fails, so the kill thread runs the 
failure listeners.
+    val killer = startDaemonThread("killer")(context.markInterrupted("killed"))
+    assert(holding.await(1, TimeUnit.MINUTES))
+
+    val completion = 
startDaemonThread("completion")(context.markTaskCompleted(None))
+    try {
+      awaitHandoverWait(completion)
+      // Neither returns early nor runs the completion listeners while the 
kill thread holds the
+      // listener invocation, and a third thread adding a listener does not 
take them over.
+      val adder = startDaemonThread("adder") {
+        context.addTaskCompletionListener[Unit] { _ =>
+          listenerThreads.add(("added", Thread.currentThread()))
+        }
+      }
+      joinThread(adder)
+      assert(completion.isAlive)
+      assert(listenerThreads.isEmpty)
+    } finally {
+      release.countDown()
+    }
+    joinThread(completion)
+    joinThread(killer)
+    assert(listenerThreads.asScala.toSeq === Seq(("added", completion), 
("pending", completion)))
+  }
+
+  test("SPARK-60222: a listener on the invoking thread completing the task 
runs them nested") {
+    val context = TaskContext.empty()
+    val completionListenerThread = new AtomicReference[Thread]()
+    context.addTaskCompletionListener[Unit] { _ =>
+      completionListenerThread.set(Thread.currentThread())
+    }
+    val ranBeforeReturn = new AtomicReference[Option[Boolean]](None)
+    val laterListenerThread = new AtomicReference[Thread]()
+    context.addTaskFailureListener(new TaskFailureListener {
+      override def onTaskFailure(context: TaskContext, error: Throwable): Unit 
= {
+        // This thread holds the listener invocation, so it must not wait for 
itself.
+        context.markTaskCompleted(Some(error))
+        ranBeforeReturn.set(Some(completionListenerThread.get eq 
Thread.currentThread()))
+        // And it still holds the invocation, so a listener added now is left 
to it.
+        joinThread(startDaemonThread("adder") {
+          context.addTaskCompletionListener[Unit] { _ =>
+            laterListenerThread.set(Thread.currentThread())
+          }
+        })
+        assert(laterListenerThread.get == null)
+      }
+    })
+    val failer = startDaemonThread("failer")(context.markTaskFailed(new 
Exception("failed")))
+    joinThread(failer)
+    assert(ranBeforeReturn.get === Some(true))
+    assert(laterListenerThread.get eq failer)
+  }
+
+  test("SPARK-60222: a failure listener left to the completing thread runs 
before the next " +
+      "completion listener") {
+    val context = TaskContext.empty()
+    val invocations = new ConcurrentLinkedQueue[(String, Thread)]()
+    context.addTaskFailureListener(new TaskFailureListener {
+      override def onTaskFailure(context: TaskContext, error: Throwable): Unit 
=
+        invocations.add(("failure", Thread.currentThread()))
+    })
+    // E.g. a completion listener that releases a state store, which a failure 
listener aborts.
+    context.addTaskCompletionListener[Unit] { _ =>
+      invocations.add(("second", Thread.currentThread()))
+    }
+    context.addTaskCompletionListener[Unit] { _ =>
+      invocations.add(("first", Thread.currentThread()))
+      // Another thread fails the task while this thread runs the completion 
listeners, e.g. a
+      // kill whose interrupt listener failed. It finds the listener 
invocation taken and leaves
+      // the failure listeners to this thread.
+      joinThread(startDaemonThread("failer") {
+        context.markTaskFailed(new Exception("failed elsewhere"))
+      })
+      assert(invocations.size == 1)
+    }
+
+    context.markTaskCompleted(None)
+    val current = Thread.currentThread()
+    assert(invocations.asScala.toSeq ===
+      Seq(("first", current), ("failure", current), ("second", current)))
+  }
+
+  test("SPARK-60222: a failure listener left to the thread the invocation was 
handed over to " +
+      "runs before its next completion listener") {
+    val context = TaskContext.empty()
+    val invocations = new ConcurrentLinkedQueue[(String, Thread)]()
+    context.addTaskCompletionListener[Unit] { _ =>
+      invocations.add(("second", Thread.currentThread()))
+    }
+    context.addTaskCompletionListener[Unit] { _ =>
+      invocations.add(("first", Thread.currentThread()))
+      // The task has failed, so the added failure listener is to run right 
away. This thread
+      // holds the listener invocation, so the adding thread leaves it to this 
one.
+      joinThread(startDaemonThread("adder") {
+        context.addTaskFailureListener(new TaskFailureListener {
+          override def onTaskFailure(context: TaskContext, error: Throwable): 
Unit =
+            invocations.add(("failure", Thread.currentThread()))
+        })
+      })
+      assert(invocations.size == 1)
+    }
+    val (release, holder) = holdListenerInvocation(context)
+    val completion = 
startDaemonThread("completion")(context.markTaskCompleted(None))
+    try {
+      awaitHandoverWait(completion)
+    } finally {
+      release.countDown()
+    }
+    joinThread(completion)
+    joinThread(holder)
+    assert(invocations.asScala.toSeq ===
+      Seq(("first", completion), ("failure", completion), ("second", 
completion)))
+  }
+
+  test("SPARK-60222: completion listeners left to a failure invoker run before 
it releases") {
+    val context = TaskContext.empty()
+    context.markTaskFailed(new Exception("failed"))
+    context.markTaskCompleted(None)
+    val holding = new CountDownLatch(1)
+    val release = new CountDownLatch(1)
+    val invokerError = new AtomicReference[Throwable]()
+    // A failure listener added now runs right away on the adding thread, 
which then holds the
+    // listener invocation until it returns.
+    val invoker = startDaemonThread("invoker") {
+      try {
+        context.addTaskFailureListener(new TaskFailureListener {
+          override def onTaskFailure(context: TaskContext, error: Throwable): 
Unit = {
+            holding.countDown()
+            release.await(1, TimeUnit.MINUTES)
+            throw new Exception("invoker listener")
+          }
+        })
+      } catch {
+        case t: Throwable => invokerError.set(t)
+      }
+    }
+    assert(holding.await(1, TimeUnit.MINUTES))
+    val leftListenerThread = new AtomicReference[Thread]()
+    try {
+      // Finds the invocation taken, so it leaves the new completion listener 
to the invoker.
+      context.addTaskCompletionListener[Unit] { _ =>
+        leftListenerThread.set(Thread.currentThread())
+        throw new Exception("left listener")
+      }
+      assert(leftListenerThread.get == null)
+    } finally {
+      release.countDown()
+    }
+    joinThread(invoker)
+    assert(leftListenerThread.get eq invoker)
+    // Its error is reported together with those of the invoker's own 
listeners.
+    assert(invokerError.get.isInstanceOf[TaskCompletionListenerException])
+    assert(invokerError.get.getMessage.contains("invoker listener"))
+    assert(invokerError.get.getMessage.contains("left listener"))
+    // And the invoker has released the invocation: a listener added now runs 
on this thread.
+    val laterListenerThread = new AtomicReference[Thread]()
+    context.addTaskCompletionListener[Unit] { _ =>
+      laterListenerThread.set(Thread.currentThread())
+    }
+    assert(laterListenerThread.get eq Thread.currentThread())
+  }
+
+  test("SPARK-60222: an interrupt does not stop the handover wait and is 
kept") {
+    val context = TaskContext.empty()
+    val listenerThreads = new ConcurrentLinkedQueue[(String, Thread)]()
+    (1 to 2).foreach { i =>
+      context.addTaskCompletionListener[Unit] { _ =>
+        listenerThreads.add((s"completion $i", Thread.currentThread()))
+      }
+    }
+    val (release, holder) = holdListenerInvocation(context)
+    val completionError = new AtomicReference[Throwable]()
+    val interruptedAfterCompletion = new AtomicReference[Option[Boolean]](None)
+    val completion = startDaemonThread("completion") {
+      try {
+        context.markTaskCompleted(None)
+      } catch {
+        case t: Throwable => completionError.set(t)
+      }
+      
interruptedAfterCompletion.set(Some(Thread.currentThread().isInterrupted))
+    }
+    try {
+      awaitHandoverWait(completion)
+      completion.interrupt()
+      // Still waiting for the holder, without having run any completion 
listener.
+      eventually(timeout(1.minute)) {
+        assert(!completion.isInterrupted)
+        assert(completion.getState == Thread.State.TIMED_WAITING)
+      }
+      assert(listenerThreads.isEmpty)
+    } finally {
+      release.countDown()
+    }
+    joinThread(completion)
+    joinThread(holder)
+    assert(completionError.get == null)
+    assert(interruptedAfterCompletion.get === Some(true))
+    assert(listenerThreads.asScala.toSeq ===
+      Seq(("completion 2", completion), ("completion 1", completion)))
+  }
+
+  test("SPARK-60222: the handover wait warns with the holder's name and stack 
trace") {
+    val context = TaskContext.empty()
+    context.asInstanceOf[TaskContextImpl].listenerHandoverWarnIntervalMs = 100

Review Comment:
   Done.



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