Akka(27): Stream:Use case-Connecting Slick-dbStream & Scalaz-stream-fs2

在以前的博文中我们介绍了Slick,它是一种FRM(Functional Relation Mapper)。有别于ORM,FRM的特点是函数式的语法可以支持灵活的对象组合(Query Composition)实现大规模的代码重复利用,但同时这些特点又影响了编程人员群体对FRM的接受程度,阻碍了FRM成为广为流行的一种数据库编程方式。所以我们只能从小众心态来探讨如何改善Slick现状,希望通过与某些Stream库集成,在Slick FRM的基础上恢复一些人们熟悉的Recordset数据库光标(cursor)操作方式,希望如此可以降低FRM数据库编程对函数式编程水平要求,能够吸引更多的编程人员接受FRM。刚好,在这篇讨论里我们希望能介绍一些Akka-Stream和外部系统集成对接的实际用例,把Slick数据库数据载入连接到Akka-Stream形成streaming-dataset应该是一个挺好的想法。Slick和Akka-Stream可以说是自然匹配的一对,它们都是同一个公司产品,都支持Reactive-Specification。Reactive系统的集成对象之间是通过公共界面Publisher来实现对接的。Slick提供了个Dababase.stream函数可以构建这个Publisher:

 /** Create a `Publisher` for Reactive Streams which, when subscribed to, will run the specified
      * `DBIOAction` and return the result directly as a stream without buffering everything first.
      * This method is only supported for streaming actions.
      *
      * The Publisher itself is just a stub that holds a reference to the action and this Database.
      * The action does not actually start to run until the call to `onSubscribe` returns, after
      * which the Subscriber is responsible for reading the full response or cancelling the
      * Subscription. The created Publisher can be reused to serve a multiple Subscribers,
      * each time triggering a new execution of the action.
      *
      * For the purpose of combinators such as `cleanup` which can run after a stream has been
      * produced, cancellation of a stream by the Subscriber is not considered an error. For
      * example, there is no way for the Subscriber to cause a rollback when streaming the
      * results of `someQuery.result.transactionally`.
      *
      * When using a JDBC back-end, all `onNext` calls are done synchronously and the ResultSet row
      * is not advanced before `onNext` returns. This allows the Subscriber to access LOB pointers
      * from within `onNext`. If streaming is interrupted due to back-pressure signaling, the next
      * row will be prefetched (in order to buffer the next result page from the server when a page
      * boundary has been reached). */
    final def stream[T](a: DBIOAction[_, Streaming[T], Nothing]): DatabasePublisher[T] = streamInternal(a, false)

这个DatabasePublisher[T]就是一个Publisher[T]:

/** A Reactive Streams `Publisher` for database Actions. */
abstract class DatabasePublisher[T] extends Publisher[T] { self =>
...
}

然后Akka-Stream可以通过Source.fromPublisher(publisher)构建Akka Source构件:

  /**
   * Helper to create [[Source]] from `Publisher`.
   *
   * Construct a transformation starting with given publisher. The transformation steps
   * are executed by a series of [[org.reactivestreams.Processor]] instances
   * that mediate the flow of elements downstream and the propagation of
   * back-pressure upstream.
   */
  def fromPublisher[T](publisher: Publisher[T]): Source[T, NotUsed] =
    fromGraph(new PublisherSource(publisher, DefaultAttributes.publisherSource, shape("PublisherSource")))

理论上Source.fromPublisher(db.stream(query))就可以构建一个Reactive-Stream-Source了。下面我们就建了例子来做示范:首先是Slick的铺垫代码boiler-code:

  val aqmraw = Models.AQMRawQuery
  val db = Database.forConfig("h2db")
  // aqmQuery.result returns Seq[(String,String,String,String)]
  val aqmQuery = aqmraw.map {r => (r.year,r.state,r.county,r.value)}
  // type alias
  type RowType = (String,String,String,String)
  // user designed strong typed resultset type. must extend FDAROW
  case class TypedRow(year: String, state: String, county: String, value: String) extends FDAROW
  // strong typed resultset conversion function. declared implicit to remind during compilation
  implicit def toTypedRow(row: RowType): TypedRow =
    TypedRow(row._1,row._2,row._3,row._4)

我们需要的其实就是aqmQuery,用它来构建DatabasePublisher:

  // construct DatabasePublisher from db.stream
  val dbPublisher: DatabasePublisher[RowType] = db.stream[RowType](aqmQuery.result)
  // construct akka source
  val source: Source[RowType,NotUsed] = Source.fromPublisher[RowType](dbPublisher)

有了dbPublisher就可以用Source.fromPublisher函数构建source了。现在我们试着运算这个Akka-Stream:

  implicit val actorSys = ActorSystem("actor-system")
  implicit val ec = actorSys.dispatcher
  implicit val mat = ActorMaterializer()

  source.take(6).map{row => toTypedRow(row)}.runWith(
    Sink.foreach(qmr => {
      println(s"州名: ${qmr.state}")
      println(s"县名:${qmr.county}")
      println(s"年份:${qmr.year}")
      println(s"取值:${qmr.value}")
      println("-------------")
    }))

  scala.io.StdIn.readLine()
  actorSys.terminate()

下面是运算结果:

州名: Alabama
县名:Elmore
年份:1999
取值:5
-------------
州名: Alabama
县名:Jefferson
年份:1999
取值:39
-------------
州名: Alabama
县名:Lawrence
年份:1999
取值:28
-------------
州名: Alabama
县名:Madison
年份:1999
取值:31
-------------
州名: Alabama
县名:Mobile
年份:1999
取值:32
-------------
州名: Alabama
县名:Montgomery
年份:1999
取值:15
-------------

显示我们已经成功的连接了Slick和Akka-Stream。

现在我们有了Reactive stream source,它是个akka-stream,该如何对接处于下游的scalaz-stream-fs2呢?我们知道:akka-stream是Reactive stream,而scalaz-stream-fs2是纯“拖式”pull-model stream,也就是说上面这个Reactive stream source必须被动等待下游的scalaz-stream-fs2来读取数据。按照Reactive-Stream规范,下游必须通过backpressure信号来知会上游是否可以发送数据状态,也就是说我们需要scalaz-stream-fs2来产生backpressure。scalaz-stream-fs2 async包里有个Queue结构:

/**
 * Asynchronous queue interface. Operations are all nonblocking in their
 * implementations, but may be ‘semantically‘ blocking. For instance,
 * a queue may have a bound on its size, in which case enqueuing may
 * block until there is an offsetting dequeue.
 */
trait Queue[F[_], A] { self =>
  /**
   * Enqueues one element in this `Queue`.
   * If the queue is `full` this waits until queue is empty.
   *
   * This completes after `a`  has been successfully enqueued to this `Queue`
   */
  def enqueue1(a: A): F[Unit]

  /**
   * Enqueues each element of the input stream to this `Queue` by
   * calling `enqueue1` on each element.
   */
  def enqueue: Sink[F, A] = _.evalMap(enqueue1)
  /** Dequeues one `A` from this queue. Completes once one is ready. */
  def dequeue1: F[A]
  /** Repeatedly calls `dequeue1` forever. */
  def dequeue: Stream[F, A] = Stream.bracket(cancellableDequeue1)(d => Stream.eval(d._1), d => d._2).repeat
...
}

这个结构支持多线程操作,也就是说enqueue和dequeue可以在不同的线程里操作。值得关注的是:enqueue会block,只有在完成了dequeue后才能继续。这个dequeue就变成了抵消backpressure的有效方法了。具体操作方法是:上游在一个线程里用enqueue发送一个数据元素,然后等待下游完成在另一个线程里的dequeue操作,完成这个循环后再进行下一个元素的enqueue。enqueue代表akka-stream向scalaz-stream-fs2发送数据,可以用akka-stream的Sink构件来实现:

 class FS2Gate[T](q: fs2.async.mutable.Queue[Task,Option[T]]) extends GraphStage[SinkShape[T]] {
  val in = Inlet[T]("inport")
  val shape = SinkShape.of(in)

  override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
    new GraphStageLogic(shape) with InHandler {
      override def preStart(): Unit = {
        pull(in)          //initiate stream elements movement
        super.preStart()
      }

      override def onPush(): Unit = {
        q.enqueue1(Some(grab(in))).unsafeRun()
        pull(in)
      }

      override def onUpstreamFinish(): Unit = {
        q.enqueue1(None).unsafeRun()
        println("the end of stream !")
        completeStage()
      }

      override def onUpstreamFailure(ex: Throwable): Unit = {
        q.enqueue1(None).unsafeRun()
        completeStage()
      }

      setHandler(in,this)

    }
}

以上这个akka-stream GraphStage描述了对上游每一个元素的enqueue动作。我们可以用scalaz-stream-fs2的flatMap来序列化运算两个线程里的enqueue和dequeue:

   val fs2Stream: Stream[Task,RowType] = Stream.eval(async.boundedQueue[Task,Option[RowType]](16))
     .flatMap { q =>
       Task(source.to(new FS2Gate[RowType](q)).run).unsafeRunAsyncFuture  //enqueue Task(new thread)
       pipe.unNoneTerminate(q.dequeue)      //dequeue in current thread
     }

这个函数返回fs2.Stream[Task,RowType],是一种运算方案,我们必须run来实际运算:

  fs2Stream.map{row => toTypedRow(row)}
      .map(qmr => {
      println(s"州名: ${qmr.state}")
      println(s"县名:${qmr.county}")
      println(s"年份:${qmr.year}")
      println(s"取值:${qmr.value}")
      println("-------------")
    }).run.unsafeRun

通过测试运行,我们成功的为scalaz-stream-fs2实现了data streaming。

下面是本次示范的源代码:

import slick.jdbc.H2Profile.api._
import com.bayakala.funda._
import api._

import scala.language.implicitConversions
import scala.concurrent.duration._
import akka.actor._
import akka.stream._
import akka.stream.scaladsl._
import akka.stream.stage._
import slick.basic.DatabasePublisher
import akka._
import fs2._
import akka.stream.stage.{GraphStage, GraphStageLogic}

 class FS2Gate[T](q: fs2.async.mutable.Queue[Task,Option[T]]) extends GraphStage[SinkShape[T]] {
  val in = Inlet[T]("inport")
  val shape = SinkShape.of(in)

  override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
    new GraphStageLogic(shape) with InHandler {
      override def preStart(): Unit = {
        pull(in)          //initiate stream elements movement
        super.preStart()
      }

      override def onPush(): Unit = {
        q.enqueue1(Some(grab(in))).unsafeRun()
        pull(in)
      }

      override def onUpstreamFinish(): Unit = {
        q.enqueue1(None).unsafeRun()
        println("end of stream !!!!!!!")
        completeStage()
      }

      override def onUpstreamFailure(ex: Throwable): Unit = {
        q.enqueue1(None).unsafeRun()
        completeStage()
      }

      setHandler(in,this)

    }
}

object AkkaStreamSource extends App {

  val aqmraw = Models.AQMRawQuery
  val db = Database.forConfig("h2db")
  // aqmQuery.result returns Seq[(String,String,String,String)]
  val aqmQuery = aqmraw.map {r => (r.year,r.state,r.county,r.value)}
  // type alias
  type RowType = (String,String,String,String)
  // user designed strong typed resultset type. must extend FDAROW
  case class TypedRow(year: String, state: String, county: String, value: String) extends FDAROW
  // strong typed resultset conversion function. declared implicit to remind during compilation
  implicit def toTypedRow(row: RowType): TypedRow =
    TypedRow(row._1,row._2,row._3,row._4)
  // construct DatabasePublisher from db.stream
  val dbPublisher: DatabasePublisher[RowType] = db.stream[RowType](aqmQuery.result)
  // construct akka source
  val source: Source[RowType,NotUsed] = Source.fromPublisher[RowType](dbPublisher)

  implicit val actorSys = ActorSystem("actor-system")
  implicit val ec = actorSys.dispatcher
  implicit val mat = ActorMaterializer()

  /*
  source.take(10).map{row => toTypedRow(row)}.runWith(
    Sink.foreach(qmr => {
      println(s"州名: ${qmr.state}")
      println(s"县名:${qmr.county}")
      println(s"年份:${qmr.year}")
      println(s"取值:${qmr.value}")
      println("-------------")
    })) */

   val fs2Stream: Stream[Task,RowType] = Stream.eval(async.boundedQueue[Task,Option[RowType]](16))
     .flatMap { q =>
       Task(source.to(new FS2Gate[RowType](q)).run).unsafeRunAsyncFuture  //enqueue Task(new thread)
       pipe.unNoneTerminate(q.dequeue)      //dequeue in current thread
     }

  fs2Stream.map{row => toTypedRow(row)}
      .map(qmr => {
      println(s"州名: ${qmr.state}")
      println(s"县名:${qmr.county}")
      println(s"年份:${qmr.year}")
      println(s"取值:${qmr.value}")
      println("-------------")
    }).run.unsafeRun

  scala.io.StdIn.readLine()
  actorSys.terminate()

}
时间: 2024-10-29 08:41:23

Akka(27): Stream:Use case-Connecting Slick-dbStream & Scalaz-stream-fs2的相关文章

[易学易懂系列|rustlang语言|零基础|快速入门|(27)|实战4:从零实现BTC区块链]

项目实战 实战4:从零实现BTC区块链 我们今天来开发我们的BTC区块链系统. 简单来说,从数据结构的角度上来说,区块链,就是区块组成的链. 以下就是BTC区块链典型的结构: 那最小单元就是区块:block. 这个block包含两部分:区块头,区块体. 我们先忽略Merkle树,先简化所有数据结构,只保留最基本的数据结构. 那区块头,就包含:时间截:前一个区块地址 区块体,就包含交易数据,我们用一个vector来存储. 代码如下 : ///交易结构体 #[derive(Clone, Hash,

Akka(17): Stream:数据流基础组件-Source,Flow,Sink简介

在大数据程序流行的今天,许多程序都面临着共同的难题:程序输入数据趋于无限大,抵达时间又不确定.一般的解决方法是采用回调函数(callback-function)来实现的,但这样的解决方案很容易造成“回调地狱(callback hell)”,即所谓的“goto-hell”:程序控制跳来跳去很难跟踪,特别是一些变量如果在回调函数中更改后产生不可预料的结果.数据流(stream)是一种解决问题的有效编程方式.Stream是一个抽象概念,能把程序数据输入过程和其它细节隐蔽起来,通过申明方式把数据处理过程

从程序员到项目经理(27):怎样给领导汇报工作【转载】

如果有一天领导叫你汇报一下项目状况,你会怎样来回答呢?在项目汇报方面,我总结有三种类型的项目经理,看看你是属于哪一种: 第一种,报喜不报忧.这种项目经理就像和珅一样圆滑,传达给领导的永远是好消息:进展总是顺利的,团队一定是和谐的,客户必定是满意的,天下永远是太平的,仿佛天气永远是晴空万里,永远不会刮风下雨似的.领导听了往往也是眉开眼笑,点头赞许. 第二种恰好相反,总是显得忧心忡忡.也许是由于项目经理有很强的危机感,每次汇报必然是听上去大问题套着小问题,项目危机重重,好像天快塌下来了似的.这种项目

Linux命令(27):df&du命令-磁盘空间查看

Linux df命令用于检查系统磁盘空间占用情况 Linux du命令用于显示目录或文件的大小 Linux df命令 语法:df [OPTION]... [FILE]... 常用选项: -h 以K,M,G为单位,提高信息的可读性. -k 以KB大小为单位显示 -m 以MB大小为单位显示 -a 列出所有的文件系统分区,包含0大小的文件系统分区 -i 列出文件系统分区的inode信息 -T 显示磁盘分区的文件系统类型 常用选项:-hT,查看系统分区情况,并显示文件系统类型           -ih

STL笔记(6)标准库:标准库中的排序算法

STL笔记(6)标准库:标准库中的排序算法 标准库:标准库中的排序算法The Standard Librarian: Sorting in the Standard Library Matthew Austern http://www.cuj.com/experts/1908/austern.htm?topic=experts 用泛型算法进行排序    C++标准24章有一个小节叫“Sorting and related operations”.它包含了很多对已序区间进行的操作,和三个排序用泛型

Java知多少(34)final关键字:阻止继承和多态

在 Java 中,声明类.变量和方法时,可使用关键字 final 来修饰.final 所修饰的数据具有“终态”的特征,表示“最终的”意思.具体规定如下: final 修饰的类不能被继承. final 修饰的方法不能被子类重写. final 修饰的变量(成员变量或局部变量)即成为常量,只能赋值一次. final 修饰的成员变量必须在声明的同时赋值,如果在声明的时候没有赋值,那么只有 一次赋值的机会,而且只能在构造方法中显式赋值,然后才能使用. final 修饰的局部变量可以只声明不赋值,然后再进行

Android Animation学习(二) ApiDemos解析:基本Animatiors使用

Animator类提供了创建动画的基本结构,但是一般使用的是它的子类: ValueAnimator.ObjectAnimator.AnimatorSet ApiDemos中Animation部分是单独的一个包. 下面代码来自ApiDemos中的AnimationCloning类,加了一个使用ValueAnimator的动画,还有一些注释. 完整的项目见:URL:https://github.com/mengdd/AnimationApiDemos.git package com.example.

Hadoop经典案例Spark实现(七)——日志分析:分析非结构化文件

相关文章推荐 Hadoop经典案例Spark实现(一)--通过采集的气象数据分析每年的最高温度 Hadoop经典案例Spark实现(二)--数据去重问题 Hadoop经典案例Spark实现(三)--数据排序 Hadoop经典案例Spark实现(四)--平均成绩 Hadoop经典案例Spark实现(五)--求最大最小值问题 Hadoop经典案例Spark实现(六)--求最大的K个值并排序 Hadoop经典案例Spark实现(七)--日志分析:分析非结构化文件 1.需求:根据tomcat日志计算ur

Python接口测试实战4(下) - 框架完善:用例基类,用例标签,重新运行上次失败用例

如有任何学习问题,可以添加作者微信:lockingfree 课程目录 Python接口测试实战1(上)- 接口测试理论 Python接口测试实战1(下)- 接口测试工具的使用 Python接口测试实战2 - 使用Python发送请求 Python接口测试实战3(上)- Python操作数据库 Python接口测试实战3(下)- unittest测试框架 Python接口测试实战4(上) - 接口测试框架实战 Python接口测试实战4(下) - 框架完善:用例基类,用例标签,重新运行上次失败用例