scalajavafxakkaakka-streamscalafx

Monitoring Akka Streams Sources with ScalaFX


What I'm trying to solve is the following case:
Given an infinite running Akka Stream I want to be able to monitor certain points of the stream. The best way I could think of where to send the messages at this point to an Actor which is also a Source. This makes it very flexible for me to then connect either individual Sources or merge multiple sources to a websocket or whatever other client I want to connect. However in this specific case I'm trying to connect ScalaFX with Akka Source but it is not working as expected.

When I run the code below both counters start out ok but after a short while one of them stops and never recovers. I know there are special considerations with threading when using ScalaFX but I don't have the knowledge enough to understand what is going on here or debug it. Below is a minimal example to run, the issue should be visible after about 5 seconds.

My question is:

How could I change this code to work as expected?

import akka.NotUsed

import scalafx.Includes._
import akka.actor.{ActorRef, ActorSystem}
import akka.stream.{ActorMaterializer, OverflowStrategy, ThrottleMode}
import akka.stream.scaladsl.{Flow, Sink, Source}

import scalafx.application.JFXApp
import scalafx.beans.property.{IntegerProperty, StringProperty}
import scalafx.scene.Scene
import scalafx.scene.layout.BorderPane
import scalafx.scene.text.Text
import scala.concurrent.duration._

/**
  * Created by henke on 2017-06-10.
  */
object MonitorApp extends JFXApp {

  implicit val system = ActorSystem("monitor")
  implicit val mat = ActorMaterializer()

  val value1 = StringProperty("0")
  val value2 = StringProperty("0")

  stage = new JFXApp.PrimaryStage {
    title = "Akka Stream Monitor"
    scene = new Scene(600, 400) {
      root = new BorderPane() {
        left = new Text {
          text <== value1
        }
        right = new Text {
          text <== value2
        }
      }
    }
  }

  override def stopApp() = system.terminate()

  val monitor1 = createMonitor[Int]
  val monitor2 = createMonitor[Int]

  val marketChangeActor1 = monitor1
    .to(Sink.foreach{ v =>
      value1() = v.toString
    }).run()

  val marketChangeActor2 = monitor2
    .to(Sink.foreach{ v =>
      value2() = v.toString
    }).run()

  val monitorActor = Source[Int](1 to 100)
    .throttle(1, 1.second, 1, ThrottleMode.shaping)
    .via(logToMonitorAndContinue(marketChangeActor1))
    .map(_ * 10)
    .via(logToMonitorAndContinue(marketChangeActor2))
    .to(Sink.ignore).run()

  def createMonitor[T]: Source[T, ActorRef] = Source.actorRef[T](Int.MaxValue, OverflowStrategy.fail)

  def logToMonitorAndContinue[T](monitor: ActorRef): Flow[T, T, NotUsed] = {
    Flow[T].map{ e =>
      monitor ! e
      e
    }
  }
}

Solution

  • It seems that you assign values to the properties (and therefore affect the UI) in the actor system threads. However, all interaction with the UI should be done in the JavaFX GUI thread. Try wrapping value1() = v.toString and the second one in Platform.runLater calls.

    I wasn't able to find a definitive statement about using runLater to interact with JavaFX data except in the JavaFX-Swing integration document, but this is quite a common thing in UI libraries; same is also true for Swing with its SwingUtilities.invokeLater method, for example.