本专栏的项目代码在Github上,如果感兴趣的话,麻烦点点Star,谢谢啦。
在 Classic Akka 中,Akka 为我们提供了基础的 Actor 类 AbstractActor 供我们继承使用,还有其衍生的类型 AbstractActorWithXXX UntypedAbstractActor,带有 With 类型的,一般是混合了某种特质,例如 Stash,Timers,而 Untyped则是抹去了消息类型,提供了一个统一的接口 onReceive来供我们接收消息,消息的类型需要我们自己去处理。
在我们的设计中,Actor 既需要有 Stash 功能,也要有 Timers 的功能,因为 Actor 启动的时候,需要把消息暂存起来,然后等数据加载完成之后,才处理消息。定时器是必不可少的功能,许多业务都需要使用定时器。同时我们也要使 Actor 具备执行外部脚本的功能,这个非常重要,因为线上经常遇到打补丁、修玩家数据的情况。
但是 Akka 为我们提供的只有 With 单一特质的抽象 Actor,没有提供类似于 AbstractActorWithStashAndTimers 这种类型,所以需要我们自己再封装一下。
Kotlin 协程支持
在实际的业务中,我们经常会给不同的 Actor 发送 RPC 请求,为了避免回调地狱的出现,我们在 Actor 中引入了 Kotlin 的协程来避免这一种情况。
data class ActorCoroutineRunnable(val runnable: Runnable) : Message, Runnable by runnable
fun ActorRef.safeActorCoroutineScope(): TrackingCoroutineScope {
val dispatcher = Executor { tell(ActorCoroutineRunnable(it)) }.asCoroutineDispatcher()
return TrackingCoroutineScope(dispatcher + SupervisorJob())
}
class TrackingCoroutineScope(context: CoroutineContext) : CoroutineScope {
private val job = Job(context[Job])
override val coroutineContext: CoroutineContext = context + job
private val jobs = ConcurrentHashMap.newKeySet<Job>()
fun launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newJob = (CoroutineScope::launch)(this, context, start, block)
jobs.add(newJob)
newJob.invokeOnCompletion { jobs.remove(newJob) }
return newJob
}
fun getAllJobs(): Set<Job> = jobs
}
以上,我们会在 Actor 中启动一个 CoroutineScope ,里面默认的协程调度器就是 Actor 本身,我们向自己的 Actor 发送 Runnable 消息来驱动协程中不同状态的运行,这样就避免了 Actor 的线程安全问题。不过,我们无法避免一个 Actor 中的数据在挂起函数返回之后,仍然满足业务需要的这种情况,下面的代码会给一个类似的例子:
player.launch {
val playerEntity = player.manager.get<PlayerMem>().player
check(playerEntity.level == 10)//必须是要玩家等级为10才做某些操作
/**
*向World询问一些额外的状态,此函数流程被挂起
*/
player.askWorld<XXXResp>(XXXAction(playerEntity.worldId))
/**
* 再次检查玩家等级为10级,才继续往下做操作,因为玩家等级可能在挂起的过程中改变
*/
check(playerEntity.level == 10)
}
为什么要这么设计?
有的人会说,在协程启动的时候,暂存其它类型的消息,协程执行完成之后,再处理其它消息不就好了。这确实是一种解决方案,但是会有一些致命的问题,例如因为 RPC 调用接收方因为异常没有给调用方回包,那么这个 Actor 就会一直卡住直到协程超时,这个时候从客户端的表现来说可能就是转圈,如果玩家等得不耐烦了,可能会选择重启游戏,但是这样并不能解决问题,因为这个 Actor 还是因为协程卡住的状态,所以玩家也无法登录。再如 Actor 会有一些后台逻辑,不只是处理玩家的请求,如果在启动协程的时候,整个 Actor 卡住,那么这些后台逻辑就不能得到执行,这样也是不行的。综上,我们选择不阻塞 Actor 执行的方式来启动协程,坏处是在挂起函数返回之后,一些业务逻辑判断可能需要再次判断一下。
基于以上思路,我们在 Actor 中封装两个用于启动协程的方法,与 Kotlin 自带的有所区别,加入了一个默认的超时时间,在实际的业务处理中,一般都要处理 RPC 超时这种情况。
fun launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
timeout: Duration? = 3.minutes,
block: suspend CoroutineScope.() -> Unit
): Job {
return if (timeout == null) {
coroutineScope.launch(context, start, block)
} else {
coroutineScope.launch(context, start) {
withTimeout(timeout, block)
}
}
}
fun <T> async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
return coroutineScope.async(context, start, block)
}
定时器设计
由于我们只能在 Actor 中混入一种特质,我们选择了 AbstractActorWithStash ,我们单独在这个 Actor 中启动一个子 Actor,继承 AbstractActorWithTimers 来实现定时器的功能。
data class TimersInteraction(val block: (TimerScheduler) -> Unit)
class TimersActor : AbstractActorWithTimers() {
companion object {
fun props(): Props = Props.create(TimersActor::class.java)
}
override fun createReceive(): Receive {
return receiveBuilder()
.match(TimersInteraction::class.java) { it.block(timers) }
.matchAny { context.parent.tell(it, self) }
.build()
}
}
基于以上的需求,我们再次封装一次后的 Actor 如下
abstract class StatefulActor : AbstractActorWithStash() {
val logger = actorLogger()
val coroutineScope = self.safeActorCoroutineScope()
private lateinit var timers: ActorRef
override fun preStart() {
super.preStart()
timers = context.actorOf(TimersActor.props(), "timers")
}
fun cancel(key: Any) {
val interaction = TimersInteraction { it.cancel(key) }
timers.tell(interaction)
}
fun cancelAll() {
val interaction = TimersInteraction { it.cancelAll() }
timers.tell(interaction)
}
suspend fun isTimerActive(key: Any): Boolean {
val channel = Channel<Boolean>(1)
val interaction = TimersInteraction { channel.trySend(it.isTimerActive(key)) }
timers.tell(interaction)
return channel.receive()
}
fun startSingleTimer(key: Any, msg: Any, timeout: Duration) {
val interaction = TimersInteraction { it.startSingleTimer(key, msg, timeout) }
timers.tell(interaction)
}
fun startTimerAtFixedRate(key: Any, msg: Any, initialDelay: Duration, interval: Duration) {
val interaction = TimersInteraction { it.startTimerAtFixedRate(key, msg, initialDelay, interval) }
timers.tell(interaction)
}
fun startTimerAtFixedRate(key: Any, msg: Any, interval: Duration) {
val interaction = TimersInteraction { it.startTimerAtFixedRate(key, msg, interval) }
timers.tell(interaction)
}
fun startTimerWithFixedDelay(key: Any, msg: Any, initialDelay: Duration, delay: Duration) {
val interaction = TimersInteraction { it.startTimerWithFixedDelay(key, msg, initialDelay, delay) }
timers.tell(interaction)
}
fun startTimerWithFixedDelay(key: Any, msg: Any, delay: Duration) {
val interaction = TimersInteraction { it.startTimerWithFixedDelay(key, msg, delay) }
timers.tell(interaction)
}
}
在主 Actor 中,封装一下 Timers 中的方法,转发给子 Actor 进行定时操作,子 Actor 定时完成之后,直接把消息发送给它的父 Actor,这样就能触发定时了。
Actor 脚本支持
我们不希望每增加一种类型的 Actor,都要去处理如何在 Actor 中执行脚本的问题,我们希望直接把这一部分封装好,新加的 Actor 直接继承这个抽象的 Actor,就可以获得这个功能了。为此,我们需要重写 Actor 中的部分方法,对于执行脚本部分的消息,直接拦截掉。
override fun aroundReceive(receive: PartialFunction<Any, BoxedUnit>?, msg: Any?) {
when (msg) {
is ActorCoroutineRunnable -> {
handleRunnable<ActorCoroutineRunnable> { msg.run() }
}
is ActorNamedRunnable -> {
handleRunnable<ActorNamedRunnable> { msg.block() }
}
is ExecuteActorScript -> {
node.scriptActor.forward(CompileActorScript(msg.uid, msg.script, self), context)
}
is ExecuteActorFunction -> {
try {
msg.function.invoke(this, msg.extra)
sender.tell(ExecuteScriptResult(msg.uid, true), self)
} catch (e: Exception) {
sender.tell(ExecuteScriptResult(msg.uid, false), self)
logger.error(e, "{} failed to execute actor function", self)
}
}
else -> {
super.aroundReceive(receive, msg)
}
}
}
- ExecuteActorScript 二进制的脚本文件,可以是jar或者groovy,需要先发送到 ScriptActor 进行处理,得到可执行的 Function,才能在 Actor 中进行执行。
- ExecuteActorFunction 处理完之后的脚本 Function,在目标 Actor 中执行。
- ActorCoroutineRunnable 协程支持部分,无关
最终 Actor 设计
abstract class StatefulActor<N>(val node: N) : AbstractActorWithStash() where N : Node {
val logger = actorLogger()
val coroutineScope = self.safeActorCoroutineScope()
private lateinit var timers: ActorRef
override fun preStart() {
super.preStart()
timers = context.actorOf(TimersActor.props(), "timers")
}
override fun aroundReceive(receive: PartialFunction<Any, BoxedUnit>?, msg: Any?) {
when (msg) {
is ActorCoroutineRunnable -> {
handleRunnable<ActorCoroutineRunnable> { msg.run() }
}
is ActorNamedRunnable -> {
handleRunnable<ActorNamedRunnable> { msg.block() }
}
is ExecuteActorScript -> {
node.scriptActor.forward(CompileActorScript(msg.uid, msg.script, self), context)
}
is ExecuteActorFunction -> {
try {
msg.function.invoke(this, msg.extra)
sender.tell(ExecuteScriptResult(msg.uid, true), self)
} catch (e: Exception) {
sender.tell(ExecuteScriptResult(msg.uid, false), self)
logger.error(e, "{} failed to execute actor function", self)
}
}
else -> {
super.aroundReceive(receive, msg)
}
}
}
private inline fun <reified T> handleRunnable(runnable: () -> Unit) {
try {
runnable()
} catch (e: Exception) {
logger.error(e, "{} failed to execute {}", self.path(), T::class.java)
}
}
fun cancel(key: Any) {
val interaction = TimersInteraction { it.cancel(key) }
timers.tell(interaction)
}
fun cancelAll() {
val interaction = TimersInteraction { it.cancelAll() }
timers.tell(interaction)
}
suspend fun isTimerActive(key: Any): Boolean {
val channel = Channel<Boolean>(1)
val interaction = TimersInteraction { channel.trySend(it.isTimerActive(key)) }
timers.tell(interaction)
return channel.receive()
}
fun startSingleTimer(key: Any, msg: Any, timeout: Duration) {
val interaction = TimersInteraction { it.startSingleTimer(key, msg, timeout) }
timers.tell(interaction)
}
fun startTimerAtFixedRate(key: Any, msg: Any, initialDelay: Duration, interval: Duration) {
val interaction = TimersInteraction { it.startTimerAtFixedRate(key, msg, initialDelay, interval) }
timers.tell(interaction)
}
fun startTimerAtFixedRate(key: Any, msg: Any, interval: Duration) {
val interaction = TimersInteraction { it.startTimerAtFixedRate(key, msg, interval) }
timers.tell(interaction)
}
fun startTimerWithFixedDelay(key: Any, msg: Any, initialDelay: Duration, delay: Duration) {
val interaction = TimersInteraction { it.startTimerWithFixedDelay(key, msg, initialDelay, delay) }
timers.tell(interaction)
}
fun startTimerWithFixedDelay(key: Any, msg: Any, delay: Duration) {
val interaction = TimersInteraction { it.startTimerWithFixedDelay(key, msg, delay) }
timers.tell(interaction)
}
fun launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
timeout: Duration? = 3.minutes,
block: suspend CoroutineScope.() -> Unit
): Job {
return if (timeout == null) {
coroutineScope.launch(context, start, block)
} else {
coroutineScope.launch(context, start) {
withTimeout(timeout, block)
}
}
}
fun <T> async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
return coroutineScope.async(context, start, block)
}
fun execute(name: String, block: () -> Unit) {
self tell ActorNamedRunnable(name, block)
}
fun fireEvent(event: Event) {
self tell event
}
}
结尾
对于 ScriptActor 中的脚本加载和执行部分,后面会单独进行说明。