package wow.doge.mygame.scriptsystem import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.PoolRouter import akka.actor.typed.scaladsl.Routers import wow.doge.mygame.state.ScriptActor import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.ActorContext import akka.actor.typed.Behavior import akka.util.Timeout import scala.util.Success import scala.util.Failure import akka.actor.typed.SupervisorStrategy import akka.actor.typed.LogOptions import org.slf4j.event.Level import com.typesafe.scalalogging.Logger object ScriptCachingActor { /** * aka script representation */ type ScriptObject = Any type ScriptsMap = Map[os.Path, ScriptObject] type ScriptResult = Either[ScriptActor.Error, ScriptObject] sealed trait Command /** * @param scriptPath path of the script to compile * @param requester return address of the asking actor * @param force if true, forces script compilation even if a previous cached version exists */ final case class Get( scriptPath: os.Path, requester: ActorRef[ScriptResult], force: Boolean = false ) extends Command final case class GetMap(requester: ActorRef[ScriptsMap]) extends Command final case class Put(scriptPath: os.Path, script: ScriptObject) extends Command private[scriptsystem] final case object NoOp extends Command private[scriptsystem] final case class DelegateToChild( scriptActor: ActorRef[ScriptActor.Command], scriptPath: os.Path, requester: ActorRef[ScriptResult] ) extends Command // final case class Props( // ctx: ActorContext[Command], // scriptActor: ActorRef[ScriptActor.Command] // ) { // def create(state: State = State(Map.empty)): Behavior[Command] = // Behaviors.logMessages { // Behaviors.setup { ctx => // val pool = ScriptActorPool(4) // val scriptsRouter = ctx.spawn(pool, "script-actors-pool") // new ScriptCachingActor(this) // .receiveMessage(state) // } // } // } final case class State(scriptsMap: ScriptsMap) def apply(state: State = State(Map.empty)): Behavior[Command] = Behaviors.logMessages( LogOptions() .withLevel(Level.TRACE) .withLogger( Logger[ScriptCachingActor].underlying ), Behaviors.setup { ctx => val pool = ScriptActorPool(4) val scriptsRouter = ctx.spawn(pool, "script-actors-pool") new ScriptCachingActor(ctx, scriptsRouter).receiveMessage(state) } ) } class ScriptCachingActor( ctx: ActorContext[ScriptCachingActor.Command], scriptActor: ActorRef[ScriptActor.Command] ) { import com.softwaremill.quicklens._ import ScriptCachingActor._ import Methods._ def receiveMessage(state: State): Behavior[Command] = Behaviors.receiveMessage { msg => msg match { case Get(scriptPath, requester, force) => if (force) ctx.self ! DelegateToChild(scriptActor, scriptPath, requester) else getOrCompileScript( ctx, scriptPath, state.scriptsMap, scriptActor, requester ) Behaviors.same case DelegateToChild(scriptActor, scriptPath, requester) => import scala.concurrent.duration._ implicit val timeout = Timeout(15.seconds) // child ! ScriptActor.CompileAny(scriptPath, requester) askChildForScriptCompilation( ctx, scriptActor, scriptPath, requester ) Behaviors.same case GetMap(requester) => requester ! state.scriptsMap Behaviors.same case Put(scriptPath, script) => ctx.log.debug(s"Putting $script at path $scriptPath") val newState = state.modify(_.scriptsMap).using(_ + (scriptPath -> script)) ctx.log.trace(newState.toString()) receiveMessage(state = newState) case NoOp => Behaviors.same } } } object ScriptActorPool { def apply( poolSize: Int ): PoolRouter[ScriptActor.Command] = Routers.pool(poolSize = poolSize)( // make sure the workers are restarted if they fail Behaviors .supervise(ScriptActor()) .onFailure[Exception](SupervisorStrategy.restart) ) } private[scriptsystem] object Methods { import ScriptCachingActor._ def getOrCompileScript( ctx: ActorContext[Command], scriptPath: os.Path, scriptsMap: ScriptsMap, scriptActor: ActorRef[ScriptActor.Command], requester: ActorRef[ScriptResult] ) = { scriptsMap .get(scriptPath) .fold { ctx.log.debug("Delegating to child") ctx.self ! DelegateToChild( scriptActor, scriptPath, requester ) } { s => ctx.log.debug("Getting script from cache") requester ! Right(s) } } def askChildForScriptCompilation( ctx: ActorContext[Command], scriptActor: ActorRef[ScriptActor.Command], scriptPath: os.Path, requester: ActorRef[ScriptResult] )(implicit timeout: Timeout) = { ctx.ask(scriptActor, ScriptActor.CompileAny(scriptPath, _)) { case Success(value) => requester ! value value.fold( err => { ctx.log.error(err.reason) NoOp }, res => { Put(scriptPath, res) } ) case Failure(exception) => { ctx.log.error(exception.getMessage()) NoOp } } } }