FunDA(5)- Reactive Streams:Play with Iteratees
FunDA的设计目标就是把后台数据库中的数据搬到内存里,然后进行包括并行运算的数据处理,最后可能再对后台数据库进行更新。如果需要把数据搬到内存的话,那我们就必须考虑内存是否能一次性容纳所有的数据,有必要配合数据处理分部逐步读入,这就是Reactive Stream规范主要目的之一。所以在设计FunDA的数据源(Source)之前必须要考虑实现reacive-data-stream。Slick 3.x版在功能上的突破之一就是实现了对Reactive-Stream API的支持。遗憾的是新版的Slick并没有提供针对data-stream的具体操作函数,官方文档提到可以通过akka-stream或者Play-Iteratee-Reactive-Stream来实现对data-stream的处理操作。Slick是通过db.stream构建一个DatabasePublisher类型来实现Reactive-Stream接口的。Play则提供了stream.IterateeStreams.publisherToEnumerator(SlickDatabasePubliser)转换函数,能够把DatabasePublisher转成Reactive-Stream的数据源(Source)。Play是通过Iteratee来实现对Reactive-Stream的处理操作。我们就在这节讨论一下有关Iteratee的一些原理。在示范前我们必须在build.sbt中增加依赖:"com.typesafe.play" % "play-iteratees-reactive-streams_2.11" % "2.6.0"。所谓Reactive从字面来解释就是互动。Reacive-Stream是指数据产生方(producer)和数据使用方(consumer)之间的互动。大体上是producer通知consumer数据准备完毕可以读取、consumer通知producer读取数据的具体状态,提示是否可以发送数据。下面我们就把Reactive-Stream的基础原理给大家介绍一下:一般我们需要从一个Stream里获取数据时,可以用下面这个界面的read:
trait InputStream {
def read(): Byte
}
这是一种典型的同步操作:read会占用线程直到获取这个Byte。我们可以用callback函数形式来解决这个问题:把一个读取函数传给目标Stream,以一种被动形式来获取这个Byte:
trait InputStreamHandler {
def onByte(byte: Byte)
}
我们想办法把onByte传给Stream作为一种callback函数。当Stream有了Byte后调用这个onByte函数,在这个onByte函数里是收到Byte后应该进行的运算。不过收到这个Byte代表我们程序状态的一个转变,所以我们可以把上面这个界面写成函数式的:
trait InputStreamHandler {
def onByte(byte: Byte): InputStreamHandler
}
由于状态可能转变,所以我们把当前这个有变化的对象传出来。下面是一个界面实现的例子:
class consume(data: Seq[Byte]) extends InputStreamHandler {
def onByte(byte: Byte) = new consume(data :+ byte)
}
这个例子里我们把读取的Byte汇集到一个Seq里。但是假如Stream准备好了数据后调用我们的callback函数onByte,而我们无法立即完成函数内的运算,导致调用方线程阻塞,影响整个Stream的运转。我们可以用Future来解决这个问题:
trait InputStreamHandle {
def onByte(byte: Byte): Future[InputStreamHandle]
}
这样调用方可以立即返回了。不过,调用方如何把数据发送状态通知数据读取方呢?比如已经完成所有数据发送。我们需要把调用方返回的数据再细化点:
trait Input[+E]
case class EL[E](e: E) extends Input[E]
case object EOF extends Input[Nothing]
case object Empty extends Input[Nothing]
现在这个返回数据是个Input[E]了,是带状态的。返回数据具体类型EL,EOF,Empty从字面就可以理解它们代表的状态了。我们的界面变成了这样:
trait InputStreamHandler[E] {
def onInput(input: Input[E]): Future[InputStreamHandler[E]]
}
界面实现例子变成下面这样:
class consume(data: Seq[Byte]) extends InputStreamHandler[Byte] {
def onInput(input: Input[Byte]) = input match {
case EL(byte) => Future.successful(new consume(data :+ byte))
case _ => Future.successful(this)
}
}
上面这个例子中返回Future很是别扭,我们可以这样改善界面InputStreamHandler定义:
trait InputStreamHandler[E] {
def onByte[B](cont: (Input[E] => InputStreamHandler[E]) => Future[B]): Future[B]
}
现在我们可以这样实现那个例子:
class consume(data: Seq[Byte]) extends InputStreamHandler[Byte] {
def onByte[B](cont: (Input[Byte] => InputStreamHandler[Byte]) => Future[B]) = cont {
case EL(byte) => new consume(data :+ byte)
case _ => this
}
}
现在用起来顺手多了吧。从上面这些例子中我们可以得出一种“推式”流模式(push-model-stream): 由目标stream向读取方推送数据。但Reactive-Stream应该还具备反向通告机制,比如读取方如何通知目标stream已经完成读取操作或者暂时无法再接受数据、又或者可以接受数据了。
现在我们对Reactive-Streams有了个大概的印象:这个模式由两方组成,分别是:数据源(在push-model中就是数据发送方)以及数据消耗方,分别对应了Iteratee模式的Enumerator和Iteratee。也就是说:Enumerator负责发送,Iteratee负责接收。用Iteratee实现Reactive-Streams时必须实现Enumerator和Iteratee之间的双向通告机制。实际上Iteratee描述了如何消耗Enumerator传过来的数据:比如把数据串接起来(concat)或者相加汇总等。在消耗数据的过程中Iteratee也必须负责与Enumerator沟通以保证数据传输的顺利进行。那么Iteratee又应该如何与Enumerator沟通呢?为了实现这种沟通功能,我们再设计一个trait:
trait Step[E,+A]
case class Done[+A,E](a: A, remain: Input[E]) extends Step[E,A]
case class Cont[E,+A](k: Input[E] => InputStreamHandler[E,A]) extends Step[E,A]
case class Error[E](msg: String, loc:Input[E]) extends Step[E,Nothing]
Step代表Iteratee的操作状态:Done代表完成,返回运算结果A,remain是剩余的输入、Cont代表可以用k来获取数据、Error返回错误信息msg以及出错地点loc。现在我们可以重新定义InputStreamHandler:
trait InputStreamHandler[E,A] {
def onInput[A](step: Step[E,A] => Future[A]): Future[A]
}
界面实现例子Consume如下:
class Consume(data: Seq[Byte]) extends InputStreamHandler[Byte,Seq[Byte]] {
def onInput(step: Step[Byte,Seq[Byte]] => Future[Seq[Byte]]) = step(Cont {
case EL(byte) => new Consume(data :+ byte)
case EOF => new InputStreamHandler[Byte,Seq[Byte]] {
def onInput(step: Step[Byte,Seq[Byte]] => Future[Seq[Byte]]) = step(Done(data,Empty))
}
case Empty => this
})
}
这个版本最大的区别在于当收到Stream发送的EOF信号后返回Done通知完成操作,可以使用运算结果data了。这个InputStreamHandler就是个Iteratee,它描述了如何使用(消耗)接收到的数据。我们可以把界面定义命名为下面这样:
trait Iteratee[E,+A] {
def onInput[B](folder: Step[E,A] => Future[B]): Future[B]
}
实际上Iteratee模式与下面这个函数很相像:
def foldLeft[F[_],A,B](ax: F[A])(z: B)(f: (B,A) => B): B
F[A]是个数据源,我们不需要理会它是如何产生及发送数据的,我们只关注如何去处理收到的数据。在这个函数里(B,A)=>B就是具体的数据消耗方式。foldLeft代表了一种推式流模式(push-model-stream)。至于如何产生数据源,那就是Enumerator要考虑的了。
好了,我们先看看Iteratee正式的类型款式:Iteratee[E,A],E是数据元素类型,A是运算结果类型。trait Iteratee 有一个抽象函数:
/**
* Computes a promised value B from the state of the Iteratee.
*
* The folder function will be run in the supplied ExecutionContext.
* Exceptions thrown by the folder function will be stored in the
* returned Promise.
*
* If the folder function itself is synchronous, it's better to
* use `pureFold()` instead of `fold()`.
*
* @param folder a function that will be called on the current state of the iteratee
* @param ec the ExecutionContext to run folder within
* @return the result returned when folder is called
*/
def fold[B](folder: Step[E, A] => Future[B])(implicit ec: ExecutionContext): Future[B]
不同功能的Iteratee就是通过定义不同的fold函数构成的。fold是个callback函数提供给Enumerator。folder的输入参数Step[E,A]代表了当前Iteratee的三种可能状态:
object Step {
case class Done[+A, E](a: A, remaining: Input[E]) extends Step[E, A]
case class Cont[E, +A](k: Input[E] => Iteratee[E, A]) extends Step[E, A]
case class Error[E](msg: String, input: Input[E]) extends Step[E, Nothing]
}
当状态为Cont[E,A]时,Enumerator就会用这个k: Input[E]=> Iteratee[E,A]函数把Input[E]推送给Iteratee。我们从一个简单的Enumerator就可以看出:
/**
* Creates an enumerator which produces the one supplied
* input and nothing else. This enumerator will NOT
* automatically produce Input.EOF after the given input.
*/
def enumInput[E](e: Input[E]) = new Enumerator[E] {
def apply[A](i: Iteratee[E, A]): Future[Iteratee[E, A]] =
i.fold {
case Step.Cont(k) => eagerFuture(k(e))
case _ => Future.successful(i)
}(dec)
}
或者:
/**
* Create an Enumerator from a set of values
*
* Example:
* {{{
* val enumerator: Enumerator[String] = Enumerator("kiki", "foo", "bar")
* }}}
*/
def apply[E](in: E*): Enumerator[E] = in.length match {
case => Enumerator.empty
case => new Enumerator[E] {
def apply[A](i: Iteratee[E, A]): Future[Iteratee[E, A]] = i.pureFoldNoEC {
case Step.Cont(k) => k(Input.El(in.head))
case _ => i
}
}
case _ => new Enumerator[E] {
def apply[A](i: Iteratee[E, A]): Future[Iteratee[E, A]] = enumerateSeq(in, i)
}
}
-----
private def enumerateSeq[E, A]: (Seq[E], Iteratee[E, A]) => Future[Iteratee[E, A]] = { (l, i) =>
l.foldLeft(Future.successful(i))((i, e) =>
i.flatMap(it => it.pureFold {
case Step.Cont(k) => k(Input.El(e))
case _ => it
}(dec))(dec))
}
我们可以通过定义fold函数来获取不同功能的Iteratee。下面就是一个直接返回恒量值Iteratee的定义过程:
val doneIteratee = new Iteratee[String,Int] {
def fold[B](folder: Step[String,Int] => Future[B])(implicit ec: ExecutionContext): Future[B] = {
folder(Step.Done(,Input.EOF))
}
}
这个Iteratee不会消耗任何输入,直接就返回21。实际上我们可以直接用Done.apply来构建这个doneIteratee:
val doneIteratee = Done[String,Int](,Input.Empty)
我们也可以定义一个只消耗一个输入元素的Iteratee:
val consumeOne = new Iteratee[String,String] {
def fold[B](folder: Step[String,String] => Future[B])(implicit ec: ExecutionContext): Future[B] = {
folder(Step.Cont {
case Input.EOF => Done("OK",Input.EOF)
case Input.Empty => this
case Input.El(e) => Done(e,Input.EOF)
})
}
}
同样,我们也可以用Cont构建器来构建这个consumeOne:
val consumeOne1 = Cont[String,String](in => Done("OK",Input.EOF))
从上面这些例子里我们可以推敲folder函数应该是在Enumerator里定义的,看看下面这个Enumerator例子:
val enumerator = new Enumerator[String] {
// some messages
val items = to map (i => i.toString)
var index = override def apply[A](i: Iteratee[String, A]):
Future[Iteratee[String, A]] = {
i.fold(
// the folder
{
step => {
step match {
// iteratee is done, so no more messages
// to send
case Step.Done(result, remaining) => {
println("Step.Done")
Future(i)
} // iteratee can consume more
case Step.Cont(k: (Input[String] => Iteratee[String, A]))
=> {
println("Step.Cont")
// does enumerator have more messages ?
if (index < items.size) {
val item = items(index)
println(s"El($item)")
index += // get new state of iteratee
val newIteratee = k(Input.El(item)) // recursive apply
apply(newIteratee)
} else {
println("EOF")
Future(k(Input.EOF))
}
} // iteratee is in error state
case Step.Error(message, input: Input[String]) => {
println("Step.Error")
Future(i)
}
}
}
})
}
}
下面我们示范一个完整的例子:
val userIteratee = new Iteratee[String, Unit] {
override def fold[B](folder: (Step[String, Unit]) => Future[B])
(implicit ec: ExecutionContext): Future[B] = {
// accumulator
val buffer: ListBuffer[String] = ListBuffer() // the step function
def stepFn(in: Input[String]): Iteratee[String, Unit] = {
in match {
case Input.Empty => this
case Input.EOF => Done({
println(s"Result ${buffer.mkString("--")}")
}, Input.Empty)
case Input.El(el) => {
buffer += el
Cont(stepFn)
}
}
} // initial state -> iteratee ready to accept input
folder(Step.Cont(stepFn))
}
} //> userIteratee : play.api.libs.iteratee.Iteratee[String,Unit] = demo.worksheet.iteratee2$$anonfun$main$1$$anon$3@4f063c0a
val usersEnum = Enumerator("Tiger","John","Jimmy","Kate","Chris")
//> usersEnum : play.api.libs.iteratee.Enumerator[String] = play.api.libs.iteratee.Enumerator$$anon$19@51cdd8a
(usersEnum |>>> userIteratee) //> Result Tiger--John--Jimmy--Kate--Chris res0: scala.concurrent.Future[Unit] = Success(())
Enumerator usersEnum把输入推送给userIteratee、userIteratee在完成时直接把它们印了出来。在play-iterate库Iteratee对象里有个fold函数(Iteratee.fold)。这是个通用的函数,可以轻松实现上面这个userIteratee和其它的汇总功能Iteratee。Iteratee.fold函数款式如下:
def fold[E, A](state: A)(f: (A, E) => A): Iteratee[E, A]
我们可以用这个fold函数来构建一个相似的Iteratee:
val userIteratee2 = Iteratee.fold(List[String]())((st, el:String) => st :+ el)
//> userIteratee2 : play.api.libs.iteratee.Iteratee[String,List[String]] = Cont(<function1>)
(usersEnum |>>> userIteratee2).foreach {x => println(x)}
//| List(Tiger, John, Jimmy, Kate, Chris)
下面是另外两个用fold函数的例子:
val inputLength: Iteratee[String,Int] = {
Iteratee.fold[String,Int]() { (length, chars) => length + chars.length }
//> inputLength : play.api.libs.iteratee.Iteratee[String,Int] = Cont(<function1>)
}
Await.result((usersEnum |>>> inputLength),Duration.Inf)
//> res1: Int = 23
val consume: Iteratee[String,String] = {
Iteratee.fold[String,String]("") { (result, chunk) => result ++ chunk }
//> consume : play.api.libs.iteratee.Iteratee[String,String] = Cont(<function1 >)
}
Await.result((usersEnum |>>> consume),Duration.Inf)
//> res2: String = TigerJohnJimmyKateChris
从以上的练习里我们基本摸清了定义Iteratee的两种主要模式:
1、构建新的Iteratee,重新定义fold函数,如上面的userIteratee及下面这个上传大型json文件的例子:
object ReactiveFileUpload extends Controller {
def upload = Action(BodyParser(rh => new CsvIteratee(isFirst = true))) {
request =>
Ok("File Processed")
}
} case class CsvIteratee(state: Symbol = 'Cont, input: Input[Array[Byte]] = Empty, lastChunk: String = "", isFirst: Boolean = false) extends Iteratee[Array[Byte], Either[Result, String]] {
def fold[B](
done: (Either[Result, String], Input[Array[Byte]]) => Promise[B],
cont: (Input[Array[Byte]] => Iteratee[Array[Byte], Either[Result, String]]) => Promise[B],
error: (String, Input[Array[Byte]]) => Promise[B]
): Promise[B] = state match {
case 'Done =>
done(Right(lastChunk), Input.Empty) case 'Cont => cont(in => in match {
case in: El[Array[Byte]] => {
// Retrieve the part that has not been processed in the previous chunk and copy it in front of the current chunk
val content = lastChunk + new String(in.e)
val csvBody =
if (isFirst)
// Skip http header if it is the first chunk
content.drop(content.indexOf("\r\n\r\n") + )
else content
val csv = new CSVReader(new StringReader(csvBody), ';')
val lines = csv.readAll
// Process all lines excepted the last one since it is cut by the chunk
for (line <- lines.init)
processLine(line)
// Put forward the part that has not been processed
val last = lines.last.toList.mkString(";")
copy(input = in, lastChunk = last, isFirst = false)
}
case Empty => copy(input = in, isFirst = false)
case EOF => copy(state = 'Done, input = in, isFirst = false)
case _ => copy(state = 'Error, input = in, isFirst = false)
}) case _ =>
error("Unexpected state", input) } def processLine(line: Array[String]) = WS.url("http://localhost:9200/affa/na/").post(
toJson(
Map(
"date" -> toJson(line()),
"trig" -> toJson(line()),
"code" -> toJson(line()),
"nbjours" -> toJson(line().toDouble)
)
)
)
}
二、直接定义Cont:
/**
* Create an iteratee that takes the first element of the stream, if one occurs before EOF
*/
def head[E]: Iteratee[E, Option[E]] = { def step: K[E, Option[E]] = {
case Input.Empty => Cont(step)
case Input.EOF => Done(None, Input.EOF)
case Input.El(e) => Done(Some(e), Input.Empty)
}
Cont(step)
}
及:
def fileIteratee(file: File): Iteratee[String, Long] = {
val helper = new FileNIOHelper(file) def step(totalLines: Long)(in: Input[String]): Iteratee[String, Long] = in match {
case Input.EOF | Input.Empty =>
if(debug) println("CLOSING CHANNEL")
helper.close()
Done(totalLines, Input.EOF)
case Input.El(line) =>
if(debug) println(line)
helper.write(line)
Cont[String, Long](i => step(totalLines+)(i))
}
// initiates iteration by initialize context and first state (Cont) and launching iteration
Cont[String, Long](i => step(0L)(i))
} }
FunDA(5)- Reactive Streams:Play with Iteratees的更多相关文章
- FunDA(6)- Reactive Streams:Play with Iteratees、Enumerator and Enumeratees
在上一节我们介绍了Iteratee.它的功能是消耗从一些数据源推送过来的数据元素,不同的数据消耗方式代表了不同功能的Iteratee.所谓的数据源就是我们这节要讨论的Enumerator.Enumer ...
- FunDA(7)- Reactive Streams to fs2 Pull Streams
Reactive-Stream不只是简单的push-model-stream, 它还带有“拖式”(pull-model)性质.这是因为在Iteratee模式里虽然理论上由Enumerator负责主动推 ...
- FunDA(9)- Stream Source:reactive data streams
上篇我们讨论了静态数据源(Static Source, snapshot).这种方式只能在预知数据规模有限的情况下使用,对于超大型的数据库表也可以说是不安全的资源使用方式.Slick3.x已经增加了支 ...
- FunDA(8)- Static Source:保证资源使用安全 - Resource Safety
我们在前面用了许多章节来讨论如何把数据从后台数据库中搬到内存,然后进行逐行操作运算.我们选定的解决方案是把后台数据转换成内存中的数据流.无论在打开数据库表或从数据库读取数据等环节都涉及到对数据库表这项 ...
- FunDA(4)- 数据流内容控制:Stream data element control
上节我们探讨了通过scalaz-stream-fs2来驱动一套数据处理流程,用fs2的Pipe类型来实现对数据流的逐行操作.本篇讨论准备在上节讨论的基础上对数据流的流动和元素操作进行优化完善.如数据流 ...
- FunDA(14)- 示范:并行运算,并行数据库读取 - parallel data loading
FunDA的并行数据库读取功能是指在多个线程中同时对多个独立的数据源进行读取.这些独立的数据源可以是在不同服务器上的数据库表,又或者把一个数据库表分成几个独立部分形成的独立数据源.当然,并行读取的最终 ...
- FunDA(3)- 流动数据行操作:FDAPipeLine operations using scalaz-stream-fs2
在上节讨论里我们介绍了数据行流式操作的设想,主要目的是把后台数据库的数据载入前端内存再拆分为强类型的数据行,这样我们可以对每行数据进行使用和处理.形象点描述就是对内存里的一个数据流(data-stre ...
- FunDA(17)- 示范:异常处理与事后处理 - Exceptions handling and Finalizers
作为一个能安全运行的工具库,为了保证占用资源的安全性,对异常处理(exception handling)和事后处理(final clean-up)的支持是不可或缺的.FunDA的数据流FDAPipeL ...
- FunDA(15)- 示范:任务并行运算 - user task parallel execution
FunDA的并行运算施用就是对用户自定义函数的并行运算.原理上就是把一个输入流截分成多个输入流并行地输入到一个自定义函数的多个运行实例.这些函数运行实例同时在各自不同的线程里同步运算直至耗尽所有输入. ...
随机推荐
- jQuery使用大全
我的程序人生 提供基于Lesktop的IM二次开发,联系QQ:76159179 CnBlogs Home New Post Contact Admin Rss Posts - 476 Article ...
- 2018.09.11 bzoj47214721: [Noip2016]蚯蚓(单调队列)
传送门 好题. 目测只会多带一个log2(n+m)" role="presentation" style="position: relative;"& ...
- 破解Unity5.3.4f1
破解的目的是将受限的个人版变为全功能的Pro版,破解后就可以使用所有功能了,界面也变成了黑色的主题. 破解网址(支持最新版的5.3.4f1): http://www.ceeger.com/forum/ ...
- 疯狂安装oracle 12c,此版本没有scott这个用户
今天要学习oracle,然后寻思下个吧,结果出现了很多问题,在此分享一下,搞疯了,太痛苦了,学的教程是用的 Oracle 11g,我去官网下载的Oracle 12g,文件很大,好不容易装好了,寻思就这 ...
- 安装hue-3.11.0
1) 配置HDFS HttpFS和WebHDFS 如果HDFS是HA方式部署的,则只能使用HttpFS,而不能用WebHDFS. 2) 安装依赖: apr-iconv-1.2.1 confuse-3. ...
- String 简介
特性: 1.字符串的不可改变性 String s="a"+"b"+"c"; 创建了四个对象 在方法常量池中存储 a“,”b“,”c“, ...
- python logging 实现的进程安全的文件回滚日志类
python标准库中的logging模块在记录日志时经常会用到,但在实际使用发现它自带的用于本地日志回滚的类 logging.handlers.RotatingFileHandler 在多进程环境下会 ...
- (连通图)Network--POJ--3694
链接: http://poj.org/problem?id=3694 http://acm.hust.edu.cn/vjudge/contest/view.action?cid=82833#probl ...
- windows下C++实现遍历本地文件
1.假设本地 d:/ 下存放着0.txt,1.txt两个文件 2.开发工具VS,开发语言C++,怎么遍历得到两个文件呢? 废话不多,具体代码请看下面: /** * 入参:文件存放文件夹路径,例如D:\ ...
- Java面向接口编程【精品博客】
我们从生活中去理解面向接口编程,以下举例四个案例来理解: 案例一(汽车案例): /** * 汽车标准接口 * @author Liudeli */ public interface ICar { /* ...