将元素从外部推送到fs2中的反应流

时间:2018-11-30 08:57:03

标签: scala akka-stream reactive-streams fs2

我有一个如下所示的外部Java API(即我无法更改它):

public interface Sender {
    void send(Event e);
}

我需要实现一个Sender,它接受​​每个事件,将其转换为JSON对象,将一些事件收集到一个包中,并通过HTTP发送到某个端点。所有这些都应异步完成,而不会send()阻塞调用线程,并使用一些固定大小的缓冲区,如果缓冲区已满,则丢弃新事件。

使用akka-streams,这非常简单:我创建了一个阶段图(使用akka-http发送HTTP请求),将其实例化,然后使用实例化的ActorRef将新事件推送到流中:< / p>

lazy val eventPipeline = Source.actorRef[Event](Int.MaxValue, OverflowStrategy.fail)
  .via(CustomBuffer(bufferSize))  // buffer all events
  .groupedWithin(batchSize, flushDuration)  // group events into chunks
  .map(toBundle)  // convert each chunk into a JSON message
  .mapAsyncUnordered(1)(sendHttpRequest)  // send an HTTP request
  .toMat(Sink.foreach { response =>
    // print HTTP response for debugging
  })(Keep.both)

lazy val (eventsActor, completeFuture) = eventPipeline.run()

override def send(e: Event): Unit = {
  eventsActor ! e
}

这里CustomBuffer是自定义的GraphStage,它与图书馆提供的Buffer非常相似,但是是根据我们的特定需求量身定制的;对于这个特定的问题可能并不重要。

如您所见,与非流代码中的流进行交互非常简单-!特性上的ActorRef方法是异步的,不需要调用任何其他机制。发送到角色的每个事件都将通过整个响应管道进行处理。此外,由于akka-http的实现方式,我什至免费获得连接池,因此服务器打开的连接不超过一个。

但是,我找不到正确使用FS2执行相同操作的方法。即使抛弃了缓冲(我可能需要编写一个自定义Pipe实现,它执行我们需要做的其他事情)和HTTP连接池的问题,我仍然停留在一个更基本的东西上,即如何将数据“从外部”推送到反应流。

我可以找到的所有教程和文档都假定整个程序发生在某些效果上下文中,通常是IO。这不是我的情况-Java库在未指定的时间调用send()方法。因此,我只是不能将所有内容都保留在一个IO动作中,我必须必须在send()方法中完成“ push”动作,并将反应性流作为单独的实体,因为我想聚合事件并希望合并HTTP连接(我相信这自然与响应流相关联)。

我假设我需要一些其他数据结构,例如Queue。 fs2确实确实具有某种fs2.concurrent.Queue,但是同样,所有文档都显示了如何在单个IO上下文中使用它,因此我假设这样做

val queue: Queue[IO, Event] = Queue.unbounded[IO, Event].unsafeRunSync()

,然后在流定义内使用queue,然后在send()方法内分别使用另外的unsafeRun调用:

val eventPipeline = queue.dequeue
  .through(customBuffer(bufferSize))
  .groupWithin(batchSize, flushDuration)
  .map(toBundle)
  .mapAsyncUnordered(1)(sendRequest)
  .evalTap(response => ...)
  .compile
  .drain

eventPipeline.unsafeRunAsync(...)  // or something

override def send(e: Event) {
  queue.enqueue(e).unsafeRunSync()
}

不是正确的方法,很可能甚至行不通。

所以,我的问题是,如何正确使用fs2解决问题?

3 个答案:

答案 0 :(得分:1)

我对这个库没有太多经验,但是看起来应该像这样:

import cats.effect.{ExitCode, IO, IOApp}
import fs2.concurrent.Queue

case class Event(id: Int)

class JavaProducer{

  new Thread(new Runnable {
    override def run(): Unit = {
      var id = 0
      while(true){
        Thread.sleep(1000)
        id += 1
        send(Event(id))
      }
    }
  }).start()

  def send(event: Event): Unit ={
    println(s"Original producer prints $event")
  }
}

class HackedProducer(queue: Queue[IO, Event]) extends JavaProducer {
  override def send(event: Event): Unit = {
    println(s"Hacked producer pushes $event")
    queue.enqueue1(event).unsafeRunSync()
    println(s"Hacked producer pushes $event - Pushed")
  }

}

object Test extends IOApp{
  override def run(args: List[String]): IO[ExitCode] = {
    val x: IO[Unit] = for {
      queue <- Queue.unbounded[IO, Event]
      _ = new HackedProducer(queue)
      done <- queue.dequeue.map(ev => {
        println(s"Got $ev")
      }).compile.drain
    } yield done
    x.map(_ => ExitCode.Success)
  }

}

答案 1 :(得分:1)

考虑以下示例:

import cats.implicits._
import cats.effect._
import cats.effect.implicits._
import fs2._
import fs2.concurrent.Queue

import scala.concurrent.ExecutionContext
import scala.concurrent.duration._

object Answer {
  type Event = String

  trait Sender {
    def send(event: Event): Unit
  }

  def main(args: Array[String]): Unit = {
    val sender: Sender = {
      val ec = ExecutionContext.global
      implicit val cs: ContextShift[IO] = IO.contextShift(ec)
      implicit val timer: Timer[IO] = IO.timer(ec)

      fs2Sender[IO](2)
    }

    val events = List("a", "b", "c", "d")
    events.foreach { evt => new Thread(() => sender.send(evt)).start() }
    Thread sleep 3000
  }

  def fs2Sender[F[_]: Timer : ContextShift](maxBufferedSize: Int)(implicit F: ConcurrentEffect[F]): Sender = {
    // dummy impl
    // this is where the actual logic for batching
    //   and shipping over the network would live
    val consume: Pipe[F, Event, Unit] = _.evalMap { event =>
      for {
        _ <- F.delay { println(s"consuming [$event]...") }
        _ <- Timer[F].sleep(1.seconds)
        _ <- F.delay { println(s"...[$event] consumed") }
      } yield ()
    }

    val suspended = for {
      q <- Queue.bounded[F, Event](maxBufferedSize)
      _ <- q.dequeue.through(consume).compile.drain.start
      sender <- F.delay[Sender] { evt =>
        val enqueue = for {
          wasEnqueued <- q.offer1(evt)
          _ <- F.delay { println(s"[$evt] enqueued? $wasEnqueued") }
        } yield ()
        enqueue.toIO.unsafeRunAsyncAndForget()
      }
    } yield sender

    suspended.toIO.unsafeRunSync()
  }
}

主要思想是使用fs2中的并发队列。请注意,上面的代码演示了Sender接口和main中的逻辑都不能更改。只能替换Sender接口的实现。

答案 2 :(得分:0)

我们可以创建一个有界队列,该队列将消耗来自发送方的元素并使它们可用于 fs2 流处理。


import cats.effect.IO
import cats.effect.std.Queue

import fs2.Stream

trait Sender[T]:
    def send(e: T): Unit

object Sender:
     def apply[T](bufferSize: Int): IO[(Sender[T], Stream[IO, T])] =
         for
             q <- Queue.bounded[IO, T](bufferSize)
         yield
             val sender: Sender[T] = (e: T) => q.offer(e).unsafeRunSync()
             def stm: Stream[IO, T] = Stream.eval(q.take) ++ stm
             (sender, stm)

然后我们将有两个端点 - 一个用于 Java 世界,将新元素发送到 Sender。另一个 - 用于 fs2 中的流处理。

class TestSenderQueue:

    @Test def testSenderQueue: Unit =
        val (sender, stream) = Sender[Int](1)
          .unsafeRunSync()// we have to run it preliminary to make `sender` available to external system
        
        val processing = 
            stream
                .map(i => i * i)
                .evalMap{ ii => IO{ println(ii)}}
        sender.send(1)
                
        processing.compile.toList.start//NB! we start processing in a separate fiber
            .unsafeRunSync() // immediately right now.
        sender.send(2)
        Thread.sleep(100)
        (0 until 100).foreach(sender.send)
        println("finished")

请注意,我们在当前线程中推送数据,并且必须在单独的线程(.start)中运行 fs2。