Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ class StreamingQueryListenerBus(sparkListenerBus: LiveListenerBus)
sparkListenerBus.post(s)
// post to local listeners to trigger callbacks
postToAll(s)
case t: QueryTerminatedEvent =>
// run all the listeners synchronized before removing the id from the list
postToAll(t)

@zsxwing zsxwing Feb 21, 2017

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will post QueryTerminatedEvent to all listeners directly in the current thread. Hence, the listener may see QueryProgressEvent after QueryTerminatedEvent.

You can override postToAll. It's fine to remove final from the postToAll method.

activeQueryRunIds.synchronized { activeQueryRunIds -= t.runId }
case _ =>
sparkListenerBus.post(event)
}
Expand Down Expand Up @@ -112,7 +116,6 @@ class StreamingQueryListenerBus(sparkListenerBus: LiveListenerBus)
case queryTerminated: QueryTerminatedEvent =>
if (shouldReport(queryTerminated.runId)) {
listener.onQueryTerminated(queryTerminated)
activeQueryRunIds.synchronized { activeQueryRunIds -= queryTerminated.runId }
}
case _ =>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,93 @@ class StreamingQueryListenerSuite extends StreamTest with BeforeAndAfter {
}
}

testQuietly("multiple listeners, check trigger events are generated correctly") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to add a regression test rather than copying the above test. Most of this test is testing the same thing in "single listener, check trigger events are generated correctly". It doesn't make sense. How about this:

  test("SPARK-19594: all of listeners should receive QueryTerminatedEvent") {
    val df = MemoryStream[Int].toDS().as[Long]
    val listeners = (1 to 5).map(_ => new EventCollector)
    try {
      testStream(df, OutputMode.Append)(
        StartStream(),
        StopStream,
        AssertOnQuery { query =>
          eventually(Timeout(streamingTimeout)) {
            listeners.foreach(listener => assert(listener.terminationEvent !== null))
            listeners.foreach(listener => assert(listener.terminationEvent.id === query.id))
            listeners.foreach(listener => assert(listener.terminationEvent.runId === query.runId))
            listeners.foreach(listener => assert(listener.terminationEvent.exception === None))
          }
          listeners.foreach(listener => listener.checkAsyncErrors())
          listeners.foreach(listener => listener.reset())
          true
        }
      )
    } finally {
      listeners.foreach(spark.streams.removeListener)
    }
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with that (with a small fix)
need to add:
listeners.foreach(listener => spark.streams.addListener(listener))

val clock = new StreamManualClock
val inputData = new MemoryStream[Int](0, sqlContext)
val df = inputData.toDS().as[Long].map { 10 / _ }
val numOfListenersToTest = 5
val listeners = (1 to numOfListenersToTest).map(_ => new EventCollector)
try {
// No events until started
listeners.foreach(listener => spark.streams.addListener(listener))
listeners.foreach(listener => assert(listener.startEvent === null))
listeners.foreach(listener => assert(listener.progressEvents.isEmpty))
listeners.foreach(listener => assert(listener.terminationEvent === null))

testStream(df, OutputMode.Append)(

// Start event generated when query started
StartStream(ProcessingTime(100), triggerClock = clock),
AssertOnQuery { query =>
listeners.foreach(listener => assert(listener.startEvent !== null))
listeners.foreach(listener => assert(listener.startEvent.id === query.id))
listeners.foreach(listener => assert(listener.startEvent.runId === query.runId))
listeners.foreach(listener => assert(listener.startEvent.name === query.name))
listeners.foreach(listener => assert(listener.progressEvents.isEmpty))
listeners.foreach(listener => assert(listener.terminationEvent === null))
true
},

// Progress event generated when data processed
AddData(inputData, 1, 2),
AdvanceManualClock(100),
CheckAnswer(10, 5),
AssertOnQuery { query =>
listeners.foreach(listener => assert(listener.progressEvents.nonEmpty))
// SPARK-18868: We can't use query.lastProgress, because in progressEvents, we filter
// out non-zero input rows, but the lastProgress may be a zero input row trigger
val lastNonZeroProgress = query.recentProgress.filter(_.numInputRows > 0).lastOption
.getOrElse(fail("No progress updates received in StreamingQuery!"))
listeners.foreach(listener =>
assert(listener.progressEvents.last.json === lastNonZeroProgress.json))
listeners.foreach(listener => assert(listener.terminationEvent === null))
true
},

// Termination event generated when stopped cleanly
StopStream,
AssertOnQuery { query =>
eventually(Timeout(streamingTimeout)) {
listeners.foreach(listener => assert(listener.terminationEvent !== null))
listeners.foreach(listener => assert(listener.terminationEvent.id === query.id))
listeners.foreach(listener => assert(listener.terminationEvent.runId === query.runId))
listeners.foreach(listener => assert(listener.terminationEvent.exception === None))
}
listeners.foreach(listener => listener.checkAsyncErrors())
listeners.foreach(listener => listener.reset())
true
},

// Termination event generated with exception message when stopped with error
StartStream(ProcessingTime(100), triggerClock = clock),
AddData(inputData, 0),
AdvanceManualClock(100),
ExpectFailure[SparkException](),
AssertOnQuery { query =>
eventually(Timeout(streamingTimeout)) {
listeners.foreach(listener => assert(listener.terminationEvent !== null))
listeners.foreach(listener => assert(listener.terminationEvent.id === query.id))
listeners.foreach(listener => assert(listener.terminationEvent.exception.nonEmpty))
// Make sure that the exception message reported through listener
// contains the actual exception and relevant stack trace
listeners.foreach(listener =>
assert(!listener.terminationEvent.exception.get.contains("StreamingQueryException")))
listeners.foreach(listener =>
assert(
listener.terminationEvent.exception.get.contains("java.lang.ArithmeticException")))
listeners.foreach(listener =>
assert(
listener.terminationEvent.exception.get.contains("StreamingQueryListenerSuite")))
}
listeners.foreach(listener => listener.checkAsyncErrors())
true
}
)
} finally {
listeners.foreach(listener => spark.streams.removeListener(listener))
}
}

test("adding and removing listener") {
def isListenerActive(listener: EventCollector): Boolean = {
listener.reset()
Expand Down