【问题标题】:Possible to offload route completion DSL to actors in akka-http?可以将路由完成 DSL 卸载到 akka-http 中的参与者吗?
【发布时间】:2017-08-27 15:20:33
【问题描述】:

我已经对 akka-http 实现感兴趣,但作为一种反模式让我印象深刻的一件事是为所有路由、参数解析、错误处理等创建 DSL。文档中给出的例子极其微不足道。然而,我在市场上看到了一个真实产品的路线,它是一个巨大的 10k 行文件,其中包含许多层次和大量的业务逻辑。现实世界的系统必须处理传递错误参数的用户,没有正确的权限等等,所以简单的 DSL 在现实生活中迅速爆炸。对我来说,最佳解决方案是将路线完成交给演员,每个演员都有相同的 api,然后他们将做完成路线所需的事情。这将分散逻辑并启用可维护的代码,但数小时后我无法管理它。使用低级 API,我可以传递 HttpRequest 并以旧方式处理它,但这使我无法使用 DSL 中的大多数工具。那么有没有一种方法可以将一些东西传递给一个演员,使其能够在那个时候继续 DSL,处理特定于路由的东西? IE。我说的是这样的事情:

  class MySlashHandler() extends Actor {
    def receive = {
      case ctxt: ContextOfSomeKind =>
        decodeRequest {
          // unmarshal with in-scope unmarshaller
          entity(as[Order]) { order =>
            sender ! "Order received"
          }
        context.stop(self)
    }
  }

  val route =
    pathEndOrSingleSlash {
      get { ctxt =>
        val actor = actorSystem.actorOf(Props(classOf[MySlashHandler]))
        complete(actor ? ctxt)
      }
    }

当然,这甚至不会编译。尽管我尽了最大的努力,但我还没有找到 ContextOfSomeKind 的类型,或者一旦我进入演员内部,如何重新输入 DSL。这可能是不可能的。如果不是,我认为我不喜欢 DSL,因为它鼓励了我认为可怕的编程方法。那么低级 API 的唯一问题是访问实体编组器,但我宁愿这样做,然后在单个源文件中创建一个大型应用程序。

【问题讨论】:

  • 我个人有将路由拆分为多个特征的习惯,这仅仅是因为在 IntelliJ IDEA 中无法在单个 10k 的路由文件中工作。
  • 即使可能,这对我来说也是一种反模式。
  • 嗯,我猜这是个人品味的问题 :) 我更喜欢可读性而不是严格遵循模式。但是,如果您找到更好的方法来组织复杂的 akka-http 路由,我将很乐意阅读它:)

标签: scala akka akka-http


【解决方案1】:

卸载路线完成

直接回答您的问题:Route 只不过是一个函数。 The definition being:

type Route = (RequestContext) => Future[RouteResult]

因此,您可以简单地编写一个函数来满足您的需求,例如将 RequestContext 发送给 Actor 并返回结果:

class MySlashHandler extends Actor {

  val routeHandler = (_ : RequestContext) => complete("Actor Complete")

  override def receive : Receive = {
    case requestContext : RequestContext => routeHandler(requestContext) pipeTo sender
  }
}

val actorRef : ActorRef = actorSystem actorOf (Props[MySlashHandler])

val route : Route = 
  (requestContext : RequestContext) => (actorRef ? requestContext).mapTo[RouteResult]

演员不能解决你的问题

您要解决的问题是现实世界的复杂性以及在代码中对这种复杂性进行建模。我同意这是一个问题,但Actor 不是您的解决方案。在您寻求的设计解决方案中避免使用 Actor 有几个原因:

  1. compelling arguments against putting business logic in Actors
  2. Akka-http 在后台使用 akka-stream。 Akka 流在底层使用 Actor。因此,您试图通过使用 Actors 来逃避基于可组合 Actors 的 DSL。水通常不是解决溺水者的办法...
  3. akka-http DSL 提供了大量编译时检查,一旦您恢复到 Actor 的非类型化 receive 方法,这些检查就会消失。你会得到更多的运行时错误,例如死信,使用 Actors。

路线组织

如前所述:Route 是一个简单的函数,是 scala 的构建块。仅仅因为您看到了一个草率的开发人员将所有逻辑保存在一个文件中的示例,并不意味着这是唯一的解决方案。

我可以在main 方法中编写我所有的应用程序代码,但这并不能使它成为好的函数式编程设计。同样,我可以在一个 for 循环中编写所有集合逻辑,但我通常使用 filter/map/reduce。

同样的组织原则也适用于路线。仅从最基本的角度来看,您可以根据方法类型分解 Route 逻辑:

//GetLogic.scala
object GetLogic {
  val getRoute = get {
    complete("get received")
  }  
}

//PutLogic.scala
object PutLogic {
  val putRoute = put {
    complete("put received")
  }
}

另一个常见的组织原则是让你的业务逻辑与你的路由逻辑分开:

object BusinessLogic {

  type UserName = String
  type UserId = String     

  //isolated business logic 
  val dbLookup(userId : UserId) : UserName = ???

  val businessRoute = get {
    entity(as[String]) { userId => complete(dbLookup(userId)) } 
  }
}

然后可以将这些全部组合到您的 main 方法中:

val finalRoute : Route = 
  GetLogic.getRoute ~ PutLogic.putRoute ~ BusinessLogic.businessRoute

路由 DSL 可能会产生误导,因为它有时看起来有点像魔术,但在它下面只是简单的旧函数,scala 可以很好地组织和隔离......

【讨论】:

  • “Actor 不应该有业务逻辑,应该只是一个并发层,”你说。如果我尝试过,我不能再不同意这种说法了。我已经部署了一个使用演员的中型系统和另一个处理难以置信的数据量的超大型系统,它在商用硬件上以令人难以置信的速度运行。如果您仅将参与者视为并发层,那么我认为要么您没有对它们进行足够深入的探索,要么您在其他方面错过了重点。演员不仅仅是类固醇的未来,他们有很多你还没有挖掘的能力。
  • @RobertSimmonsJr.您指的是“并发层”之外的哪些 Actor 功能?您可以利用所有邮箱队列、消息传递、状态机等,而无需在 Actor 类定义中包含业务代码。将业务逻辑保留在一个类中而将 Actor 逻辑保留在另一个类中会遗漏什么功能?请注意,我的论点是不是完全避免使用Actor,而是以正确的方式使用它们...
  • :-) 只有 570 个字符?在部署承受重负载的应用程序时,您可以构建利用消息处理范式来大规模加速事物的系统。我为 DFS 构建了一个 REAL WORLD 业务系统,该系统在 9 节点商品集群上运行,每天为 TB 级实时、快速变化、财务完美的数据提供服务。在 SO 评论中无法解释。参与者包含所有业务逻辑。您不能只采用 Spring MVC 风格的应用程序并将其移植到 actor 并期望得到好的结果,您必须以不同的方式思考。许多高管已经看到并感到惊讶。
  • 我完全同意 Actor 模型非常适合“大规模加速事物的消息处理范式”。这如何转化为 Actor 类定义应该包含业务逻辑?如果您查看我引用的示例链接,我可以使用 Actors 并且仍然将逻辑保留在 Actor 代码之外。您一直在为使用 Actors 提供令人信服的案例,而不是为将代码粘贴在其中的令人信服的案例。有区别...
  • 因为参与者和消息是业务逻辑的组成部分。系统不仅仅是调用spring-MVC风格的“控制器”来实现逻辑,实际工作不是通过命令式设计而是通过消息处理设计来完成的。就像我说的,它不适合 570 char cmets。当然,我也受到 NDA 限制。书中的例子太琐碎了,琐碎得可怜。该系统是一个强大的系统。但同样,你必须抛弃你所知道的关于后端服务器设计的一切,然后重新开始。老实说,这是最难的部分。
【解决方案2】:

上周我也遇到了这样的问题。最终我来到this blog,并决定按照那里描述的方式走。

我创建了一个自定义指令,使我可以将请求上下文传递给 Actor。

   def imperativelyComplete(inner: ImperativeRequestContext => Unit): Route = { ctx: RequestContext =>
       val p = Promise[RouteResult]()
       inner(new ImperativeRequestContext(ctx, p))
       p.future
   }

现在我可以像这样在我的 Routes 文件中使用它:

val route = 
    put {
        imperativelyComplete { ctx =>
            val actor = actorSystem.actorOf(Props(classOf[RequestHandler], ctx))
            actor ! HandleRequest
        }
    }

我的 RequestHandler Actor 如下所示:

class RequestHandler(ctx: ImperativeRequestContext) extends Actor {
    def receive: Receive = {
        case handleRequest: HandleRequest =>
            someActor ! DoSomething() // will return SomethingDone to the sender
        case somethingDone: SomethingDone =>
            ctx.complete("Done handling request")
            context.stop(self)
    }
}

我希望这能引导您找到更好的解决方案。我不确定这个解决方案是否应该是可行的方法,但到目前为止,它对我来说非常有效。

【讨论】:

    猜你喜欢
    • 2018-10-01
    • 1970-01-01
    • 2017-10-24
    • 1970-01-01
    • 2016-09-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多