1

I noticed while updating my TornadoFX version from 1.7.12 to 1.7.14 that one of my tests broke. Things seemed to go off the rails when a runAsyncWithProgress in the View under test was rejected by a ThreadPoolExecutor with status Terminated. I saw in the release notes for 1.7.13 there was a change "internal thread pools are shut down on app exit". Setting the TornadoFX version to 1.7.13 resulted in the same failure, confirming my suspicion that it was related to the above change.

I wrote a simple app to demonstrate this bug.

// src/main/kotlin/me/carltonwhitehead/AsyncBugApp.kt
class AsyncBugApp : App(MainView::class)

/**
 * The main method is needed to support the mvn jfx:run goal.
 */
fun main(args: Array<String>) {
    Application.launch(AsyncBugApp::class.java, *args)
}

class MainView : View("Async Bug App") {
    val controller: MainController by inject()
    override val root = pane {
        button("Robot-click to repeat bug") {
            id = "bug"
            action {
                runAsync {
                    controller.onAction("button clicked")
                }
            }
        }
    }
}

class MainController : Controller() {
    fun onAction(message: String) {
        println(message)
    }
}

And the test

// src/test/kotlin/me/carltonwhitehead/AsyncBugAppTest.kt
@RunWith(Parameterized::class)
class AsyncBugAppTest(val rounds: Int) {

    companion object {
        @JvmStatic
        @Parameterized.Parameters
        fun data() : Collection<Array<Int>> {
            return listOf(arrayOf(1), arrayOf(1))
        }
    }

    lateinit var robot: FxRobot
    lateinit var app: App

    @RelaxedMockK
    lateinit var controller: MainController

    @Rule @JvmField
    val timeout = Timeout(10, TimeUnit.SECONDS)

    @Before
    fun before() {
        MockKAnnotations.init(this)

        FxToolkit.registerPrimaryStage()
        app = AsyncBugApp()
        app.scope.set(controller)
        FxToolkit.setupApplication { app }
        robot = FxRobot()
        println("rounds = $rounds")
    }

    @After
    fun after() {
        FxToolkit.cleanupStages()
        FxToolkit.cleanupApplication(app)
    }

    @Test()
    fun itShouldSurviveRunAsyncMultipleTimes() {
        val latch = CountDownLatch(rounds)
        every { controller.onAction(any()) }.answers { latch.countDown() }

        var i = 0
        while(i <= rounds) {
            robot.clickOn("#bug")
            i++
        }

        latch.await()
        verify(exactly = rounds) { controller.onAction(any()) }
    }
}

The first time that test executes, it passes. The second time, it hangs, because the runAsync is rejected with the below stack trace.

--- Exception in Async Thread ---
java.util.concurrent.RejectedExecutionException: Task tornadofx.FXTask@a315135 rejected from java.util.concurrent.ThreadPoolExecutor@75bde925[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 2]
    java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2104)
    java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:848)
    java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1397)
    tornadofx.AsyncKt.task(Async.kt:76)
    tornadofx.AsyncKt.task(Async.kt:69)
    tornadofx.Component.runAsync(Component.kt:272)
    tornadofx.Component.runAsync$default(Component.kt:988)
    me.carltonwhitehead.MainView$root$1$1$1.invoke(AsyncBugApp.kt:21)
    me.carltonwhitehead.MainView$root$1$1$1.invoke(AsyncBugApp.kt:15)
    tornadofx.ControlsKt$action$2.handle(Controls.kt:513)
    tornadofx.ControlsKt$action$2.handle(Controls.kt)
    javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
    javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    javafx.base/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
    javafx.base/javafx.event.Event.fireEvent(Event.java:198)
    javafx.graphics/javafx.scene.Node.fireEvent(Node.java:8863)
    javafx.controls/javafx.scene.control.Button.fire(Button.java:200)
    javafx.controls/com.sun.javafx.scene.control.behavior.ButtonBehavior.mouseReleased(ButtonBehavior.java:206)
    javafx.controls/com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274)
    javafx.base/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
    javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
    javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    javafx.base/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
    javafx.base/javafx.event.Event.fireEvent(Event.java:198)
    javafx.graphics/javafx.scene.Scene$MouseHandler.process(Scene.java:3876)
    javafx.graphics/javafx.scene.Scene$MouseHandler.access$1300(Scene.java:3604)
    javafx.graphics/javafx.scene.Scene.processMouseEvent(Scene.java:1874)
    javafx.graphics/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2613)
    javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:397)
    javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:295)
    java.base/java.security.AccessController.doPrivileged(Native Method)
    javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:434)
    javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
    javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:433)
    javafx.graphics/com.sun.glass.ui.View.handleMouseEvent(View.java:556)
    javafx.graphics/com.sun.glass.ui.View.notifyMouse(View.java:942)
    javafx.graphics/com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    javafx.graphics/com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:277)
    java.base/java.lang.Thread.run(Thread.java:844)

I suspect I might be doing something wrong with the TestFX/TornadoFX app lifecycle. Each test cycle is creating a new app instance, so I'm confused why the ThreadPoolExecutor would be retained. Any suggestions?

Repo at https://github.com/carltonwhitehead/fx-async-bug-test

  • This is probably related to the last line of your test (after the `latch.await()`). I'd imagine that `controller.onAction` tries to do something asynchronously, but the test runner is not waiting for it and tears down the test environemt (that's the reason for latch being present in the test in the first place). – oakad Jan 30 '18 at 03:25
  • 1
    I've committed a fix that will reinitialize the thread pools on each App.start() call :) – Edvin Syse Jan 30 '18 at 07:39

0 Answers0