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 object ScriptCachingActor { /** * aka script representation */ type ScriptObject = Any type ScriptsMap = Map[os.Path, ScriptObject] type ScriptResult = Either[ScriptActor.Error, ScriptObject] sealed trait Command final case class Get( scriptPath: os.Path, requester: ActorRef[ScriptResult] ) 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] ) final case class State(scriptsMap: ScriptsMap) def apply(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(Props(ctx, scriptsRouter)).receiveMessage(state) } } } class ScriptCachingActor(props: ScriptCachingActor.Props) { import com.softwaremill.quicklens._ import ScriptCachingActor._ import Methods._ def receiveMessage(state: State): Behavior[Command] = Behaviors.receiveMessage { msg => msg match { case Get(scriptPath, requester) => getOrCompileScript( props.ctx, scriptPath, state.scriptsMap, props.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( props.ctx, scriptActor, scriptPath, requester ) Behaviors.same case GetMap(requester) => requester ! state.scriptsMap Behaviors.same case Put(scriptPath, script) => props.ctx.log.debug(s"Putting $script at path $scriptPath") val newState = state.modify(_.scriptsMap).using(_ + (scriptPath -> script)) props.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 } } } }