diff --git a/.gitignore b/.gitignore index d2fb731d..c76e1b15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store .bsp/ .idea/ +.vscode/ +.metals/ project/ target/ *.iml diff --git a/core/src/main/scala/scroll/internal/rpa/RolePlayingAutomaton.scala b/core/src/main/scala/scroll/internal/rpa/RolePlayingAutomaton.scala index 82dc2639..cf8df429 100644 --- a/core/src/main/scala/scroll/internal/rpa/RolePlayingAutomaton.scala +++ b/core/src/main/scala/scroll/internal/rpa/RolePlayingAutomaton.scala @@ -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 @@ -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 run() 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 RolePlayingAutomaton.Use to gain an instance of your specific FSM, e.g.: * @@ -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 + } + } + } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 660f8d04..e726d29b 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -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" @@ -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( diff --git a/tests/src/test/scala/scroll/tests/parameterized/RolePlayingAutomatonTest.scala b/tests/src/test/scala/scroll/tests/parameterized/RolePlayingAutomatonTest.scala index 910fc26a..9a5972a5 100644 --- a/tests/src/test/scala/scroll/tests/parameterized/RolePlayingAutomatonTest.scala +++ b/tests/src/test/scala/scroll/tests/parameterized/RolePlayingAutomatonTest.scala @@ -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