从Akka流中准备适当的HTTP响应,这可能会产生错误情况

时间:2018-08-08 18:22:07

标签: scala akka akka-stream

我打算使用Akka Streams对琐碎的游戏(HTTPReq / HTTPResp)进行建模。在回合中,服务器会要求玩家猜测数字。服务器检查玩家的响应,如果服务器拥有的内容与玩家的猜测相同,则为玩家提供一个分数。

典型流程如下:

  • 玩家(已通过身份验证,已分配会话ID)请求开始一轮游戏
  • 服务器检查sessionID是否有效;如果不是,则通知玩家适当的消息
  • 服务器会生成一个数字并与RoundID一起提供给玩家

...等等。没什么特别的。

这是类型和流程的粗略安排:

import akka.{Done, NotUsed}
import akka.stream.scaladsl.{Flow, Keep, Source}
import java.util.Random

import akka.stream.scaladsl.Sink

import scala.concurrent.Future
import scala.util.{Failure, Success}

sealed trait GuessingGameMessageToAndFro

case class StartARound(sessionID: String) extends GuessingGameMessageToAndFro

case class RoundStarted(sessionID: String, roundID: Int) extends GuessingGameMessageToAndFro
case class NumberGuessed(sessionID: String, roundID: Int, guessedNo: Int) extends GuessingGameMessageToAndFro
case class CorrectNumberGuessed(sessionID: String, nextRoundID: Int) extends GuessingGameMessageToAndFro
case class FinalRoundScore(sessionID: String, finalScore: Int) extends GuessingGameMessageToAndFro

case class MissingSession(sessionID: String) extends GuessingGameMessageToAndFro
case class IncorrectNumberGuessed(sessionID: String, clientGuessed: Int, serverChose: Int) extends GuessingGameMessageToAndFro

object SessionService {
  def exists(m: StartARound) = if (m.sessionID.startsWith("1")) m else MissingSession(m.sessionID)
}

object NumberGenerator {
  def numberToOfferToPlayer(m: GuessingGameMessageToAndFro) = {
    m match {

      case StartARound(s)     =>  RoundStarted(s, new Random().nextInt())
      case MissingSession(s)  =>  m
      case _                  =>  throw new RuntimeException("Not yet implemented")
    }
  }
}

val sessionExistenceChecker: Flow[StartARound,GuessingGameMessageToAndFro,NotUsed]
      = Flow.fromFunction(m => SessionService.exists(m))

val guessNumberPreparator: Flow[GuessingGameMessageToAndFro,GuessingGameMessageToAndFro,_]
      = Flow.fromFunction(m => NumberGenerator.numberToOfferToPlayer(m))

val s1 = StartARound("123")

val k =
  Source
    .single(s1)
    .via(sessionExistenceChecker)
    .via(guessNumberPreparator)
    .toMat(Sink.head)(Keep.right)

val finallyObtained = k.run

finallyObtained.onComplete(v => {
  v match {
    case Success(x)    => //   Prepare proper HTTP Response
    case Failure(ex)   => //   Prepare proper HTTP Response
  }
})

我要通过 numberToOfferToPlayer()中的长模式匹配块的原因(我在这里显示了2,但是显然它的大小会随着每种可以流动的类型而增加)是因为像 sessionExistenceChecker 生成一个 MissingSession (这是一个错误条件)一样,它必须穿过其余的流,不变,直到到达Future [完成]阶段。实际上,问题更普遍:在任何阶段,适当的转换都应导致可接受的类型或错误类型(互斥)。如果我采用这种方法,则模式匹配块将以不必要的重复为代价扩散,甚至可能不难看。

我对我的这种解决方案感到不舒服。它变得冗长而笨拙。

不用说,我没有在此处显示Akka-HTTP面临的部分(包括Routes)。上面的代码可以使用路由处理程序轻松进行拼接。所以,我跳过了。

我的问题是:此类流的正确成语是什么?从概念上讲,如果一切都很好,则元素应继续沿流移动。但是,每当发生错误时,(错误)元素都应直接跳至最后阶段,并跳过其间的所有其他阶段。对此建模的公认方法是什么?

我已经看过许多Stackoverflow帖子,这些帖子表明在类似情况下,应该采用分区/合并方式。我知道如何采用这种方法,但是对于像我这样的简单案例,这似乎是不必要的工作。或者,我在这里完全不合时宜吗?

对指关节上的任何提示,摘要或说唱,都会表示赞赏。

1 个答案:

答案 0 :(得分:1)

使用PartialFunction

对于这个特殊的用例,我通常会同意分区和合并设置是“不必要的工作”。问题中提到的其他堆栈文章是针对您的用例的,其中您只有Flow个值可以组合,而不能操纵Flow中的基础逻辑。

当您能够修改基础逻辑时,就会存在一个更简单的解决方案。但是解决方案并不严格地在akka的范围内。相反,您可以利用scala本身提供的功能性编程构造。

如果将numberTofferToPlayer函数重写为PartialFunction

object NumberGenerator {
  val numberToOfferToPlayer : PartialFunction[GuessingGameMessageToAndFro, GuessingGameMessageToAndFro] = {
    case s : StartARound  =>  RoundStarted(s.sessionID, new Random().nextInt())
  }
}

然后可以将此PartialFunction提升为常规函数,如果消息为StartARound类型,则将应用逻辑;如果消息为任何其他类型,则仅转发该消息。

此提升是通过PartialFunction的applyOrElse方法与scala中预定义的identity函数结合完成的,该函数将输入作为输出返回(即“转发”输入):

import NumberGenerator.numberToOfferToPlayer

val messageForwarder : GuessingGameMessageToAndFro => GuessingGameMessageToAndFro = 
  identity[GuessingGameMessageToAndFro]

val guessNumberPreparator: Flow[GuessingGameMessageToAndFro,GuessingGameMessageToAndFro,_] =
  Flow fromFunction (numberToOfferToPlayer applyOrElse (_, messageForwarder))

更高的抽象水平

如果您要在其中添加几个PartialFunction,请将转发逻辑添加到:

val foo : PartialFunction[GuessingGameMessageToAndFro, GuessingGameMessageToAndFro] = {
  case r : RoundStarted => ???
}

val bar : PartialFunction[GuessingGameMessageToAndFro, GuessingGameMessageToAndFro] = {
  case n : NumberGuessed => ???
}

然后,您可以编写一个通用的提升器,以抽象出常规函数的创建:

val applyOrForward : PartialFunction[GuessingGameMessageToAndFro, GuessingGameMessageToAndFro] => GuessingGameMessageToAndFro => GuessingGameMessageToAndFro =
  ((_ : PartialFunction[Int, Int]) applyOrElse ((_ : GuessingGameMessageToAndFro), messageForwader).curried

此提升器将很好地清理您的代码:

val offerFlow = Flow fromFunction applyOrForward(numberToOfferToPlayer)

val fooFlow = Flow fromFunction applyOrForward(foo)

val barFlow = Flow fromFunction applyOrForward(bar)

然后可以按照问题描述的方式组合这些流:

val combinedFlow = offerFlow via fooFlow via barFlow

类似地,您可以通过首先组合PartialFunctions然后从组合中创建一个Flow来获得相同的结果。这对于在akka之外进行单元测试很有用:

val combinedPartial = numberToOfferToPlayer orElse foo orElse bar

//no akka test kit necessary
assert {
  val testError = MissingSession("testId")

  applyOrForward(combinedPartial)(testError) equals testError
}     

//nothing much to test
val otherCombinedFlow = Flow fromFunction applyOrForward(combinedPartial)