Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.DS_Store
.bsp/
.idea/
.vscode/
.metals/
project/
target/
*.iml
Expand Down
152 changes: 113 additions & 39 deletions core/src/main/scala/scroll/internal/rpa/RolePlayingAutomaton.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
package scroll.internal.rpa

import akka.actor.Actor
import akka.actor.ActorRef
import akka.actor.ActorSystem
import akka.actor.FSM
import akka.actor.LoggingFSM
import akka.actor.Props
import scroll.internal.compartment.impl.Compartment
import scroll.internal.rpa.RolePlayingAutomaton.RPAData
import scroll.internal.rpa.RolePlayingAutomaton.RPAState
import scroll.internal.rpa.RolePlayingAutomaton.Start
import scroll.internal.rpa.RolePlayingAutomaton.Stop
import scroll.internal.rpa.RolePlayingAutomaton.Uninitialized
import scroll.internal.rpa.RolePlayingAutomaton._
import zio.{ Fiber, Queue, Runtime, Unsafe, ZIO }

import scala.reflect.ClassTag
import scala.reflect.classTag
Expand Down Expand Up @@ -43,27 +34,58 @@ object RolePlayingAutomaton {

case object Terminate extends RPAData

protected class RPABuilder[T <: AnyRef: ClassTag]() {
final case class Event(data: RPAData, stateData: RPAData)

infix def For(comp: Compartment): ActorRef =
ActorSystem().actorOf(Props(classTag[T].runtimeClass, comp), s"rpa_${comp.hashCode()}")
final case class Transition(state: RPAState, data: RPAData = Uninitialized)

trait RPARef {
def !(data: RPAData): Unit
}

protected class RPABuilder[T <: RolePlayingAutomaton: ClassTag]() {

infix def For(comp: Compartment): RPARef = {
val instance = instantiate[T](comp)
instance.ref
}

}

def Use[T <: RolePlayingAutomaton: ClassTag]: RPABuilder[T] = new RPABuilder[T]()

private def instantiate[T <: RolePlayingAutomaton: ClassTag](comp: Compartment): T = {
val clazz = classTag[T].runtimeClass
val constructors = clazz.getDeclaredConstructors.toList
val withComp = constructors.find { ctor =>
val params = ctor.getParameterTypes
params.length == 1 && params.head.isAssignableFrom(comp.getClass)
}

val ctor = withComp.orElse(constructors.find(_.getParameterCount == 0)).getOrElse {
throw new IllegalArgumentException(
s"Unable to instantiate ${clazz.getName} for compartment ${comp.getClass.getName}"
)
}

ctor.setAccessible(true)
if (ctor.getParameterCount == 1) {
ctor.newInstance(comp).asInstanceOf[T]
} else {
ctor.newInstance().asInstanceOf[T]
}
}

def Use[T <: AnyRef: ClassTag]: RPABuilder[T] = new RPABuilder[T]()
}

/** Use this trait to implement your own [[scroll.internal.compartment.impl.Compartment]] specific role playing
* automaton. Please read the documentation for [[akka.actor.FSM]] carefully, since the features from that are
* applicable for role playing automatons.
* automaton. This implementation uses ZIO to run a lightweight, single-threaded event loop.
*
* Remember to call <code>run()</code> when you want to start this automaton in your
* [[scroll.internal.compartment.impl.Compartment]] instance.
*
* This automaton will always start in state [[scroll.internal.rpa.RolePlayingAutomaton.Start]], so hook in there.
*
* Final state is always [[scroll.internal.rpa.RolePlayingAutomaton.Stop]], that will terminate the actor system for
* this [[akka.actor.FSM]].
* Final state is always [[scroll.internal.rpa.RolePlayingAutomaton.Stop]], which terminates the internal loop.
*
* Use the factory method <code>RolePlayingAutomaton.Use</code> to gain an instance of your specific FSM, e.g.:
*
Expand Down Expand Up @@ -95,32 +117,84 @@ object RolePlayingAutomaton {
* Some predefined event types for messaging are available in the companion object. You may want to define your own
* states and event types. Simply use a companion object for this as well.
*/
trait RolePlayingAutomaton extends Actor with LoggingFSM[RPAState, RPAData] {
trait RolePlayingAutomaton {

/** Starts this automaton. Needs to be called first! Will set the initial state to
* [[scroll.internal.rpa.RolePlayingAutomaton.Start]].
*/
def run(): Unit = {
log.debug(s"Starting RPA '${self.path}'")
startWith(Start, Uninitialized)
initialize()
}
private val runtime = Runtime.default

/** Stops this automaton. Will set state to [[scroll.internal.rpa.RolePlayingAutomaton.Stop]] and terminates the actor
* system for this [[akka.actor.FSM]].
*/
def halt(): State = {
val _ = context.system.terminate()
stop()
private val queue: Queue[RPAData] = Unsafe.unsafe { implicit u =>
runtime.unsafe.run(Queue.unbounded[RPAData]).getOrThrow()
}

when(Stop) {
FSM.NullFunction
private var currentState: RPAState = Start
private var currentData: RPAData = Uninitialized
private var handlers: Map[RPAState, PartialFunction[Event, Transition]] = Map.empty
private var transitionHandler: PartialFunction[(RPAState, RPAState), Unit] = PartialFunction.empty
private var fiber: Option[Fiber.Runtime[Nothing, Unit]] = None

protected val self: RPARef = new RPARef {
override def !(data: RPAData): Unit = enqueue(data)
}

onTransition { case _ -> Stop =>
log.debug(s"Stopping RPA '${self.path}'")
val _ = halt()
private[rpa] def ref: RPARef = self

/** Register event handler for a state. */
def when(state: RPAState)(handler: PartialFunction[Event, Transition]): Unit =
handlers = handlers.updated(state, handler)

/** Register transition handler. Multiple handlers can be chained. */
def onTransition(handler: PartialFunction[(RPAState, RPAState), Unit]): Unit =
transitionHandler = transitionHandler.orElse(handler)

def goto(state: RPAState, data: RPAData = Uninitialized): Transition = Transition(state, data)

/** Starts this automaton. Needs to be called first! Will set the initial state to
* [[scroll.internal.rpa.RolePlayingAutomaton.Start]].
*/
def run(): Unit = start()

/** Stops this automaton by interrupting the processing fiber. */
def halt(): Unit = {
currentState = Stop
fiber.foreach { f =>
Unsafe.unsafe { implicit u =>
runtime.unsafe.run(f.interrupt).getOrThrow()
}
}
fiber = None
}

private def start(): Unit =
if (fiber.isEmpty) {
currentState = Start
currentData = Uninitialized
val loop = processLoop
val startedFiber = Unsafe.unsafe { implicit u =>
runtime.unsafe.run(loop.forkDaemon).getOrThrow()
}
fiber = Some(startedFiber)
}

private def enqueue(data: RPAData): Unit =
Unsafe.unsafe { implicit u =>
runtime.unsafe.run(queue.offer(data)).getOrThrow()
}

private def processLoop: ZIO[Any, Nothing, Unit] =
queue.take.flatMap { message =>
val state = currentState
val data = currentData
handlers.get(state) match {
case Some(handler) if handler.isDefinedAt(Event(message, data)) =>
val Transition(nextState, nextData) = handler(Event(message, data))
currentState = nextState
currentData = nextData
if (transitionHandler.isDefinedAt(state -> nextState)) {
transitionHandler(state -> nextState)
}
if (nextState == Stop) ZIO.unit else processLoop
case _ =>
processLoop
}
}

}
14 changes: 7 additions & 7 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ trait Dependencies {

object v {
val scalaVersion = "3.8.1"
val akkaVersion = "2.8.8"
val zioVersion = "2.1.24"
val scalatestVersion = "3.2.19"
val chocoVersion = "4.10.18"
val guavaVersion = "33.5.0-jre"
Expand All @@ -18,12 +18,12 @@ trait Dependencies {
}

val coreDependencies = Seq(
"com.typesafe.akka" %% "akka-actor" % v.akkaVersion,
"com.google.guava" % "guava" % v.guavaVersion,
"org.choco-solver" % "choco-solver" % v.chocoVersion,
"org.eclipse.emf" % "org.eclipse.emf.common" % v.emfcommonVersion,
"org.eclipse.emf" % "org.eclipse.emf.ecore" % v.emfecoreVersion,
"org.eclipse.uml2" % "org.eclipse.uml2.uml" % v.umlVersion
"dev.zio" %% "zio" % v.zioVersion,
"com.google.guava" % "guava" % v.guavaVersion,
"org.choco-solver" % "choco-solver" % v.chocoVersion,
"org.eclipse.emf" % "org.eclipse.emf.common" % v.emfcommonVersion,
"org.eclipse.emf" % "org.eclipse.emf.ecore" % v.emfecoreVersion,
"org.eclipse.uml2" % "org.eclipse.uml2.uml" % v.umlVersion
)

val coreDependenciesOverrides = Seq(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package scroll.tests.parameterized

import akka.actor.actorRef2Scala
import org.scalatest.concurrent.Waiters._
import org.scalatest.time.SpanSugar._
import scroll.internal.rpa.RolePlayingAutomaton
Expand Down