viirya commented on code in PR #59663:
URL: https://github.com/apache/spark/pull/59663#discussion_r4428913065
##########
core/src/main/scala/org/apache/spark/TaskContextImpl.scala:
##########
@@ -266,7 +358,10 @@ private[spark] class TaskContextImpl(
try {
markTaskFailed(e)
} catch {
- case t: Throwable => e.addSuppressed(t)
+ // Failures of the failure listeners, or e.g. of the `getMessage` of
one of their
+ // exceptions, which is listener code too. A fatal error (e.g. an
OutOfMemoryError)
+ // propagates, so that the executor still sees it.
+ case NonFatal(t) => e.addSuppressed(t)
Review Comment:
Thanks for following up on this. These four catches now take what
`Utils.isFatalError` does not count as fatal, so a `LinkageError` is reported
with the listener's failure, and only an error that also ends the executor ends
the loops. With item 3 a failing `getMessage` no longer escapes the failure
listener invocation, but a `getMessage` whose failure fails in `getMessage` too
still does, so "an error that is not fatal to the executor does not end the
listener loops" uses that to throw a `LinkageError` after a failing
pre-completion or completion listener and from a left failure listener, and
checks that the next completion listener runs and the task is completed. It
also throws one from the completion listener invocation after a failing
pre-completion listener, which is then reported with that listener's failure
(the catch at L323). The two `LinkageError` tests now use an
`OutOfMemoryError`. The comments no longer say that the executor sees the
error; they say that it ends the ta
sk's listeners, as it ends the executor, and the test says the executor sees
it if the task body succeeded.
##########
core/src/main/scala/org/apache/spark/TaskContextImpl.scala:
##########
@@ -218,30 +244,96 @@ private[spark] class TaskContextImpl(
// writer that adds listeners lazily while it pulls its input. And since
the completion
// listeners only run after `completed` is set, they run after every
pre-completion listener,
// even if another thread is invoking listeners at this point.
+ //
+ // The completion listeners then run on this thread too, and this method
returns only after
+ // they have run. If it returned while another thread is invoking
listeners (e.g. a kill thread
+ // running failure listeners), that thread would not run them, and the
task would go on to
+ // release its memory with them pending. So the listener invocation is
claimed together with
+ // setting `completed`, which also keeps a thread that adds a completion
listener at that
+ // moment from running them; if another thread holds it, that thread hands
it over to this one
+ // when done. Failure listeners that another thread leaves to this
invocation run before the
+ // next completion listener (see `invokeListeners()`). A completion
listener added after this
+ // method has run the others runs on the thread adding it, unless another
thread is invoking
+ // listeners at that moment: that thread runs it only if it is running
completion listeners
+ // (see `invokeListeners()`).
+ //
+ // The wait for the other thread has no time bound: if its listeners
block, so does this task.
+ // It logs a warning periodically, the first one with that thread's stack
trace. The task waits
+ // here before it releases its block locks and memory (the executor does
that once `Task.run`
+ // returns), so a failure listener on another thread, e.g. a kill thread,
must not wait for
+ // either. Nothing bounds a wait for a kill thread: with
`spark.task.reaper.enabled`, every
+ // kill runs on a reaper thread, which checks
`spark.task.reaper.killTimeout` only after its
+ // kill call returns. If another thread holds the invocation and the task
is killed, the reaper
+ // with a positive kill timeout bounds the wait by killing the executor
JVM, except in local
+ // mode, where it only logs an error.
+ val currentThread = Thread.currentThread()
+ var mode: InvocationMode = Claimed
+ var awaitHandover = false
+ // Whether this call registered this thread as the listener invocation
thread or as the next
+ // one, so that it must make sure not to leave it registered.
+ var registered = false
val preCompletionExceptions = new ArrayBuffer[Throwable](2)
def nextPreCompletionListener(): Option[TaskCompletionListener] =
synchronized {
if (onPreCompleteCallbacks.empty()) {
// Set together with finding no more pre-completion listeners, so that
one added later
- // is handed to `addTaskCompletionListener` instead of being left in
the stack.
+ // is handed to `addTaskCompletionListener` instead of being left in
the stack. The
+ // listener invocation is claimed in the same step (see above).
completed = true
+ if (listenerInvocationThread.isEmpty) {
+ listenerInvocationThread = Some(currentThread)
+ registered = true
+ } else if (listenerInvocationThread.contains(currentThread)) {
+ // Called from a listener on this thread: run the completion
listeners as part of that
+ // invocation.
+ mode = Nested
+ } else {
+ nextListenerInvocationThread = Some(currentThread)
+ awaitHandover = true
+ registered = true
+ }
None
} else {
Some(onPreCompleteCallbacks.pop())
}
}
var listener: Option[TaskCompletionListener] = None
- while ({listener = nextPreCompletionListener(); listener.nonEmpty}) {
- invokeTaskPreCompletionListener(listener.get, preCompletionExceptions)
- }
-
try {
- invokeTaskCompletionListeners(error)
- } catch {
- case e: TaskCompletionListenerException if
preCompletionExceptions.nonEmpty =>
- // Merge the completion listeners' own failures, not the exception
that wraps them. Any
- // other error comes from invokeListeners itself (e.g. an
OutOfMemoryError) and is left
- // to propagate, so that the executor still sees a fatal one.
- preCompletionExceptions ++= e.getSuppressed.toSeq
+ while ({listener = nextPreCompletionListener(); listener.nonEmpty}) {
+ invokeTaskPreCompletionListener(listener.get, preCompletionExceptions)
+ }
+ try {
+ val warningFailure =
+ if (awaitHandover) awaitListenerInvocationHandover(currentThread)
else None
+ try {
+ invokeTaskCompletionListeners(error, mode)
+ } catch {
+ // The failure to warn came after the pre-completion listeners'
failures, but before
+ // those of the completion listeners.
+ case e: Throwable if warningFailure.exists(_ ne e) =>
warningFailure.get.addSuppressed(e)
Review Comment:
Done, as you suggested. "a failing handover warning is reported with the
failures of the completion listeners, unless one of them is fatal" covers the
case where the completion listener invocation throws an `OutOfMemoryError`.
##########
core/src/main/scala/org/apache/spark/TaskContextImpl.scala:
##########
@@ -449,6 +654,52 @@ private[spark] class TaskContextImpl(
}
}
}
+
+ def runLeftFailureListeners(cause: Throwable): Unit = {
+ try {
+ invokeTaskFailureListeners(cause, Nested)
+ } catch {
+ // Merge the listeners' own failures, which the nested invocation has
logged, not the
+ // exception that wraps them. A fatal error (e.g. an OutOfMemoryError)
propagates, through
+ // the release below.
+ case e: TaskCompletionListenerException => listenerExceptions ++=
e.getSuppressed.toSeq
+ case NonFatal(t) => listenerExceptions += t
Review Comment:
Done: `newTaskCompletionListenerException` now reports the failure of
`getMessage` in place of the exception it fails for, and the interrupt listener
and `invokeListeners()` paths use it too. It calls `getMessage` once per
exception, collecting the message with the exception it reports. "a failure of
getMessage is reported in place of the exception it fails for" replaces the
test you mentioned, and checks your example with two left failure listeners,
two pre-completion listeners, and two interrupt listeners, where the failure is
now recorded instead of escaping `markInterrupted`.
##########
core/src/test/scala/org/apache/spark/scheduler/TaskContextSuite.scala:
##########
@@ -1195,19 +1192,570 @@ class TaskContextSuite extends SparkFunSuite with
BeforeAndAfter with LocalSpark
completion
}
+ /** A daemon thread that records what `body` throws, for `joinThread` to
check. */
+ private class DaemonThread(name: String, body: => Unit) extends Thread(name)
{
+ setDaemon(true)
+ @volatile var error: Throwable = _
+ override def run(): Unit = {
+ try {
+ body
+ } catch {
+ case t: Throwable => error = t
+ }
+ }
+ }
+
+ private def startDaemonThread(name: String)(body: => Unit): DaemonThread = {
+ val thread = new DaemonThread(name, body)
+ thread.start()
+ thread
+ }
+
+ private def joinThread(thread: DaemonThread): Unit = {
+ thread.join(TimeUnit.MINUTES.toMillis(1))
+ assert(!thread.isAlive, s"${thread.getName} is still running")
+ Option(thread.error).foreach(e => fail(s"${thread.getName} failed", e))
+ }
+
+ /**
+ * Runs `takeInvocation` on a new thread named `name`, by default failing
the task, and waits
+ * until a failure listener on it holds the listener invocation, which it
does until the
+ * returned latch is counted down. Returns that latch and the thread.
+ */
+ private def holdListenerInvocation(context: TaskContext, name: String =
"listener-holder")(
+ takeInvocation: => Unit = context.markTaskFailed(new Exception("failed
on another thread")))
+ : (CountDownLatch, DaemonThread) = {
+ 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(name)(takeInvocation)
+ assert(holding.await(1, TimeUnit.MINUTES))
+ (release, holder)
+ }
+
/**
- * An appender that throws `failure` for every event whose message contains
`trigger`. Unlike
- * `LogAppender`, it does not ignore exceptions, so log4j throws them from
the logging call.
+ * Fails every logging call whose message contains `trigger` with `failure`.
Unlike
+ * `LogAppender`, it does not ignore its exceptions, so log4j passes them to
the caller.
*/
private class FailingAppender(trigger: String, failure: RuntimeException)
extends AbstractAppender("failingAppender", null, null, false,
Property.EMPTY_ARRAY) {
+ val failures = new AtomicInteger()
+ // Of these, the failures of events that carry a throwable.
+ val failuresWithThrown = new AtomicInteger()
override def append(event: LogEvent): Unit = {
if (event.getMessage.getFormattedMessage.contains(trigger)) {
+ failures.incrementAndGet()
+ if (event.getThrown != null) failuresWithThrown.incrementAndGet()
throw failure
}
}
}
+ private def isCausedBy(error: Throwable, cause: Throwable): Boolean =
+ Iterator.iterate(error)(_.getCause).takeWhile(_ != null).exists(_ eq cause)
+
+ /** An exception whose `getMessage`, which is listener code too, throws
`failure`. */
+ private class MessageFailingException(failure: Throwable) extends Exception {
+ override def getMessage: String = throw failure
+ }
+
+ private def addFailureListenerThrowing(context: TaskContext)(failure: =>
Throwable): Unit =
+ context.addTaskFailureListener(new TaskFailureListener {
+ override def onTaskFailure(context: TaskContext, error: Throwable): Unit
= throw failure
+ })
+
+ /** Waits until `thread` waits in `markTaskCompleted()` for the listener
invocation. */
+ private def awaitHandoverWait(thread: Thread): Unit = {
+ eventually(timeout(1.minute)) {
+ assert(!thread.isAlive || thread.getState == Thread.State.TIMED_WAITING)
+ }
+ assert(thread.isAlive, s"${thread.getName} returned instead of 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(loggingListener(listenerThreads,
"pending"))
+ context.addTaskInterruptListener(new TaskInterruptListener {
+ override def onTaskInterrupted(context: TaskContext, reason: String):
Unit =
+ throw new Exception("interrupt listener")
+ })
+ // Like a kill: an interrupt listener fails, so the kill thread runs the
failure listeners.
+ val (release, killer) = holdListenerInvocation(context, "killer") {
+ context.markInterrupted("killed")
+ }
+
+ 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.
+ joinThread(startDaemonThread("adder") {
+ context.addTaskCompletionListener(loggingListener(listenerThreads,
"added"))
+ })
+ 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 another thread adding a
listener does not run it.
+ joinThread(startDaemonThread("adder") {
+ context.addTaskCompletionListener[Unit] { _ =>
+ laterListenerThread.set(Thread.currentThread())
+ }
+ })
+ }
+ })
+ joinThread(startDaemonThread("failer")(context.markTaskFailed(new
Exception("failed"))))
+ assert(ranBeforeReturn.get === Some(true))
+ assert(laterListenerThread.get == null)
+ }
+
+ 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()))
+ throw new Exception("failure listener")
+ }
+ })
+ // E.g. a completion listener that releases a state store, which a failure
listener aborts.
+ context.addTaskCompletionListener(loggingListener(invocations, "second"))
+ 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)
+ }
+
+ val error = intercept[TaskCompletionListenerException] {
+ context.markTaskCompleted(None)
+ }
+ val current = Thread.currentThread()
+ assert(invocations.asScala.toSeq ===
+ Seq(("first", current), ("failure", current), ("second", current)))
+ // The failure listener's failure is reported as its own, not wrapped in
the exception of the
+ // failure listener invocation.
+ assert(error.getSuppressed.map(_.getMessage).toSeq === Seq("failure
listener"))
+ }
+
+ 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(loggingListener(invocations, "second"))
+ 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: an exception whose getMessage throws does not end the
listener loops") {
+ val messageFailure = new IllegalStateException("no message")
+ val current = Thread.currentThread()
+ // The failure listener invocation that a failing pre-completion or
completion listener
+ // starts throws the failure of `getMessage`, while building its exception.
+ Seq(true, false).foreach { preCompletion =>
+ val context = TaskContext.empty()
+ val invocations = new ConcurrentLinkedQueue[(String, Thread)]()
+ addFailureListenerThrowing(context)(new
MessageFailingException(messageFailure))
+ context.addTaskCompletionListener(loggingListener(invocations, "next"))
+ val failing = new TaskCompletionListener {
+ override def onTaskCompletion(context: TaskContext): Unit = throw new
Exception("listener")
+ }
+ if (preCompletion) {
+ context.addTaskPreCompletionListener(failing)
+ } else {
+ context.addTaskCompletionListener(failing)
+ }
+ val error = intercept[TaskCompletionListenerException] {
+ context.markTaskCompleted(None)
+ }
+ assert(invocations.asScala.toSeq === Seq(("next", current)),
s"preCompletion=$preCompletion")
+ assert(context.isCompleted())
+ val listenerFailure = error.getSuppressed.toSeq.head
+ assert(listenerFailure.getMessage === "listener")
+ assert(listenerFailure.getSuppressed.exists(_ eq messageFailure))
+ }
+
+ // Likewise for the failure listeners left to the completing thread.
+ val context = TaskContext.empty()
+ val invocations = new ConcurrentLinkedQueue[(String, Thread)]()
+ addFailureListenerThrowing(context)(new
MessageFailingException(messageFailure))
+ context.addTaskCompletionListener(loggingListener(invocations, "second"))
+ context.addTaskCompletionListener[Unit] { _ =>
+ joinThread(startDaemonThread("failer") {
+ context.markTaskFailed(new Exception("failed elsewhere"))
+ })
+ }
+ val error = intercept[TaskCompletionListenerException] {
+ context.markTaskCompleted(None)
+ }
+ assert(invocations.asScala.toSeq === Seq(("second", current)))
+ assert(error.getSuppressed.toSeq === Seq(messageFailure))
+ }
+
+ test("SPARK-60222: an error that escapes the listener loop still releases
the invocation") {
+ val context = TaskContext.empty()
+ context.markTaskCompleted(None)
+ val fatal = new LinkageError("no message")
+ addFailureListenerThrowing(context)(new MessageFailingException(fatal))
+ val error = intercept[LinkageError] {
+ // Runs right away on this thread and fails, and the failure listener
invocation that this
+ // starts throws `fatal` while building its exception.
+ context.addTaskCompletionListener[Unit] { _ => throw new
Exception("listener") }
+ }
+ assert(error eq fatal)
+ // Released: a listener added now runs right away on this thread.
+ val laterListenerThread = new AtomicReference[Thread]()
+ context.addTaskCompletionListener[Unit] { _ =>
+ laterListenerThread.set(Thread.currentThread())
+ }
+ assert(laterListenerThread.get eq Thread.currentThread())
+ }
+
+ test("SPARK-60222: a fatal error of a failure listener invocation is not
hidden by the failure " +
+ "of the listener that started it") {
+ val fatal = new LinkageError("no message")
+ // Both for a failing completion listener and for a failing pre-completion
listener.
+ Seq(true, false).foreach { preCompletion =>
+ val context = TaskContext.empty()
+ addFailureListenerThrowing(context)(new MessageFailingException(fatal))
+ val failing = new TaskCompletionListener {
+ override def onTaskCompletion(context: TaskContext): Unit = throw new
Exception("listener")
+ }
+ if (preCompletion) {
+ context.addTaskPreCompletionListener(failing)
+ } else {
+ context.addTaskCompletionListener(failing)
+ }
+ // It propagates, so that the executor sees it, instead of being a
suppressed exception of
+ // the listener's failure.
+ val error = intercept[LinkageError] {
+ context.markTaskCompleted(None)
+ }
+ assert(error eq fatal, s"preCompletion=$preCompletion")
+ }
+ }
+
+ 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(loggingListener(listenerThreads,
s"completion $i"))
+ }
+ val (release, holder) = holdListenerInvocation(context)()
+ val interruptedAfterCompletion = new AtomicReference[Option[Boolean]](None)
+ val completion = startDaemonThread("completion") {
+ context.markTaskCompleted(None)
+
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.isAlive ||
+ !completion.isInterrupted && completion.getState ==
Thread.State.TIMED_WAITING)
+ }
+ assert(completion.isAlive, "completion returned instead of waiting")
+ assert(listenerThreads.isEmpty)
+ } finally {
+ release.countDown()
+ }
+ joinThread(completion)
+ joinThread(holder)
+ 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.listenerHandoverWarnIntervalMs = 100
+ val (release, holder) = holdListenerInvocation(context)()
+ val logAppender = new LogAppender("listener handover wait")
+ var completion: DaemonThread = null
+ try {
+ withLogAppender(logAppender, Seq(classOf[TaskContextImpl].getName),
Some(Level.WARN)) {
+ completion =
startDaemonThread("completion")(context.markTaskCompleted(None))
+ eventually(timeout(1.minute)) {
+ assert(!completion.isAlive ||
+ logAppender.loggingEvents.count(_.getLevel == Level.WARN) >= 2)
+ }
+ assert(completion.isAlive, "completion returned instead of waiting")
+ }
+ } finally {
+ release.countDown()
+ }
+ joinThread(completion)
+ joinThread(holder)
+ val warnings = logAppender.loggingEvents.filter(_.getLevel ==
Level.WARN).toSeq
+ warnings.foreach { event =>
+ assert(event.getMessage.getFormattedMessage.contains("until thread
listener-holder"))
+ }
+ // The first warning carries the holder's stack trace, blocked in its
failure listener.
+ val holderStack = warnings.head.getThrown
+ assert(holderStack.getMessage.contains("listener-holder"))
+ assert(holderStack.getStackTrace.exists(_.getMethodName ==
"onTaskFailure"))
+ // The later ones do not walk the stack again.
+ warnings.tail.foreach { event => assert(event.getThrown == null) }
+ }
+
+ test("SPARK-60222: a failing handover warning does not end the wait") {
Review Comment:
Added "a failing handover warning is reported with the failures of the
completion listeners, unless one of them is fatal": the non-fatal variant
checks that the failure to warn carries the `TaskCompletionListenerException`
with the completion listener's failure, and fails without the catch at L312;
the other one is the `OutOfMemoryError` variant from item 2.
##########
core/src/main/scala/org/apache/spark/TaskContextImpl.scala:
##########
@@ -346,10 +537,31 @@ private[spark] class TaskContextImpl(
}
}
+ /**
+ * Invokes and removes the listeners in `listeners`, in reverse order of
registration.
+ *
+ * Other threads cannot invoke listeners while this thread does, so they
leave the listeners
+ * they add, or whose invocation they start, meanwhile. Those in `listeners`
run here like the
+ * others. If this is the completion listener invocation of
`markTaskCompleted()`, the failure
+ * listeners left to it (the task has failed) run before the completion
listeners that have
+ * not run yet, e.g. so that a failure listener that aborts a state store
update runs before
+ * the completion listener that releases the state store, if that one has
not run yet. This is
+ * the order of a failure listener invocation that a failing completion
listener starts, and,
+ * likewise, the failures of these failure listeners are reported with those
of the completion
+ * listeners. Any other invocation does not run the listeners of another
kind left to it, since
Review Comment:
Reworded: the sentence is now about the listeners of another kind that other
threads left to the invocation, and says that a listener that fails there still
starts the failure listeners on that thread (see `runListener`).
##########
core/src/main/scala/org/apache/spark/TaskContextImpl.scala:
##########
@@ -288,27 +383,123 @@ private[spark] class TaskContextImpl(
exception
}
- private def invokeTaskCompletionListeners(error: Option[Throwable]): Unit = {
+ /**
+ * Waits until the thread invoking listeners hands the invocation over to
`currentThread`, which
+ * has registered itself as `nextListenerInvocationThread`.
+ *
+ * Interrupts do not stop the wait, and are passed on once it is done. The
wait of
+ * `PipelinedWriterTask.awaitExit()` in `BasePythonRunner` drops them
instead, because it is a
+ * listener itself and the listeners right after it must still be able to
clean up. This wait
+ * is not a listener but part of `markTaskCompleted()`, so it leaves the
interrupt status (e.g.
Review Comment:
Good point. Added a sentence to the scaladoc of
`awaitListenerInvocationHandover` and to the note on the wait in the
description: with a kill thread as holder, its interrupt comes once
`markInterrupted()` has returned, after the handover, so with `interruptThread`
it may come while the completion listeners run on the task thread.
--
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]