Akka Streams: состояние в потоке

Я хочу прочитать несколько больших файлов, используя Akka Streams для обработки каждой строки. Представьте, что каждый ключ состоит из (identifier → value). Если новый идентификатор найден, я хочу сохранить его и его значение в базе данных; в противном случае, если идентификатор уже был найден при обработке потока строк, я хочу сохранить только значение. Для этого я думаю, что мне нужен какой-то рекурсивный поток с отслеживанием состояния, чтобы сохранить идентификаторы, которые уже были найдены на Map. Я думаю, что я получил бы в этом потоке пару (newLine, contextWithIdentifiers).

Я только начал изучать Akka Streams. Я полагаю, что могу справиться с обработкой без сохранения состояния, но понятия не имею, как сохранить contextWithIdentifiers. Буду признателен за любые указатели в правильном направлении.

Ответ 1

Может быть, что-то вроде statefulMapConcat может помочь вам:

import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import akka.stream.scaladsl.{Sink, Source}
import scala.util.Random._
import scala.math.abs
import scala.concurrent.ExecutionContext.Implicits.global

implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()

//encapsulating your input
case class IdentValue(id: Int, value: String)
//some random generated input
val identValues = List.fill(20)(IdentValue(abs(nextInt()) % 5, "valueHere"))

val stateFlow = Flow[IdentValue].statefulMapConcat{ () =>
  //state with already processed ids
  var ids = Set.empty[Int]
  identValue => if (ids.contains(identValue.id)) {
    //save value to DB
    println(identValue.value)
    List(identValue)
  } else {
    //save both to database
    println(identValue)
    ids = ids + identValue.id
    List(identValue)
  }
}

Source(identValues)
  .via(stateFlow)
  .runWith(Sink.seq)
  .onSuccess { case identValue => println(identValue) }

Ответ 2

Несколько лет спустя, вот реализация, которую я написал, если вам нужно только отображение 1-к-1 (не 1-к-N):

import akka.stream.stage.{GraphStage, GraphStageLogic}
import akka.stream.{Attributes, FlowShape, Inlet, Outlet}

object StatefulMap {
  def apply[T, O](converter: => T => O) = new StatefulMap[T, O](converter)
}

class StatefulMap[T, O](converter: => T => O) extends GraphStage[FlowShape[T, O]] {
  val in = Inlet[T]("StatefulMap.in")
  val out = Outlet[O]("StatefulMap.out")
  val shape = FlowShape.of(in, out)

  override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) {
    val f = converter
    setHandler(in, () => push(out, f(grab(in))))
    setHandler(out, () => pull(in))
  }
}

Тест (и демо):

  behavior of "StatefulMap"

  class Counter extends (Any => Int) {
    var count = 0

    override def apply(x: Any): Int = {
      count += 1
      count
    }
  }

  it should "not share state among substreams" in {
    val result = await {
      Source(0 until 10)
        .groupBy(2, _ % 2)
        .via(StatefulMap(new Counter()))
        .fold(Seq.empty[Int])(_ :+ _)
        .mergeSubstreams
        .runWith(Sink.seq)
    }
    result.foreach(_ should be(1 to 5))
  }