web-dev-qa-db-ja.com

値を再帰的に生成するフローからakkaストリームソースを作成する方法は?

ツリーのような形のAPIをトラバースする必要があります。たとえば、ディレクトリ構造や議論のスレッド。次のフローでモデル化できます。

type ItemId = Int
type Data = String
case class Item(data: Data, kids: List[ItemId])

def randomData(): Data = scala.util.Random.alphanumeric.take(2).mkString 

// 0 => [1, 9]
// 1 => [10, 19]
// 2 => [20, 29]
// ...
// 9 => [90, 99]
// _ => []
// NB. I don't have access to this function, only the itemFlow.
def nested(id: ItemId): List[ItemId] =
  if (id == 0) (1 to 9).toList
  else if (1 <= id && id <= 9) ((id * 10) to ((id + 1) * 10 - 1)).toList
  else Nil

val itemFlow: Flow[ItemId, Item, NotUsed] = 
  Flow.fromFunction(id => Item(randomData, nested(id)))

このデータをトラバースするにはどうすればよいですか?私は次の作業をしました:

import akka.NotUsed
import akka.actor.ActorSystem
import akka.stream._
import akka.stream.scaladsl._

import scala.concurrent.Await
import scala.concurrent.duration.Duration

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

val loop = 
  GraphDSL.create() { implicit b =>
    import GraphDSL.Implicits._

    val source = b.add(Flow[Int])
    val merge  = b.add(Merge[Int](2))
    val fetch  = b.add(itemFlow) 
    val bcast  = b.add(Broadcast[Item](2))

    val kids   = b.add(Flow[Item].mapConcat(_.kids))
    val data   = b.add(Flow[Item].map(_.data))

    val buffer = Flow[Int].buffer(100, OverflowStrategy.dropHead)

    source ~> merge ~> fetch           ~> bcast ~> data
              merge <~ buffer <~ kids  <~ bcast

    FlowShape(source.in, data.out)
  }

val flow = Flow.fromGraph(loop)


Await.result(
  Source.single(0).via(flow).runWith(Sink.foreach(println)),
  Duration.Inf
)

system.terminate()

ただし、バッファ付きのフローを使用しているため、ストリームは完了しません。

上流が完了し、バッファリングされた要素が排出されると完了します

Flow.buffer

グラフサイクル、活性、デッドロック セクションを何度も読みましたが、それでも答えを見つけるのに苦労しています。

これにより、ライブロックが作成されます。

import Java.util.concurrent.atomic.AtomicInteger

def unfold[S, E](seed: S, flow: Flow[S, E, NotUsed])(loop: E => List[S]): Source[E, NotUsed] = {
  // keep track of how many element flows, 
  val remaining = new AtomicInteger(1) // 1 = seed

  // should be > max loop(x)
  val bufferSize = 10000

  val (ref, publisher) =
    Source.actorRef[S](bufferSize, OverflowStrategy.fail)
      .toMat(Sink.asPublisher(true))(Keep.both)
      .run()

  ref ! seed

  Source.fromPublisher(publisher)
    .via(flow)
    .map{x =>
      loop(x).foreach{ c =>
        remaining.incrementAndGet()
        ref ! c
      }
      x
    }
    .takeWhile(_ => remaining.decrementAndGet > 0)
}

編集:ソリューションをテストするためにgitリポジトリを追加しました https://github.com/MasseGuillaume/source-unfold

23

私は自分のGraphStageを作成してこの問題を解決しました。

import akka.NotUsed
import akka.stream._
import akka.stream.scaladsl._
import akka.stream.stage.{GraphStage, GraphStageLogic, OutHandler}

import scala.concurrent.ExecutionContext

import scala.collection.mutable
import scala.util.{Success, Failure, Try}

import scala.collection.mutable

def unfoldTree[S, E](seeds: List[S], 
                     flow: Flow[S, E, NotUsed],
                     loop: E => List[S],
                     bufferSize: Int)(implicit ec: ExecutionContext): Source[E, NotUsed] = {
  Source.fromGraph(new UnfoldSource(seeds, flow, loop, bufferSize))
}

object UnfoldSource {
  implicit class MutableQueueExtensions[A](private val self: mutable.Queue[A]) extends AnyVal {
    def dequeueN(n: Int): List[A] = {
      val b = List.newBuilder[A]
      var i = 0
      while (i < n) {
        val e = self.dequeue
        b += e
        i += 1
      }
      b.result()
    }
  }
}

class UnfoldSource[S, E](seeds: List[S],
                         flow: Flow[S, E, NotUsed],
                         loop: E => List[S],
                         bufferSize: Int)(implicit ec: ExecutionContext) extends GraphStage[SourceShape[E]] {

  val out: Outlet[E] = Outlet("UnfoldSource.out")
  override val shape: SourceShape[E] = SourceShape(out)

  override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) with OutHandler {  
    // Nodes to expand
    val frontier = mutable.Queue[S]()
    frontier ++= seeds

    // Nodes expanded
    val buffer = mutable.Queue[E]()

    // Using the flow to fetch more data
    var inFlight = false

    // Sink pulled but the buffer was empty
    var downstreamWaiting = false

    def isBufferFull() = buffer.size >= bufferSize

    def fillBuffer(): Unit = {
      val batchSize = Math.min(bufferSize - buffer.size, frontier.size)
      val batch = frontier.dequeueN(batchSize)
      inFlight = true

      val toProcess =
        Source(batch)
          .via(flow)
          .runWith(Sink.seq)(materializer)

      val callback = getAsyncCallback[Try[Seq[E]]]{
        case Failure(ex) => {
          fail(out, ex)
        }
        case Success(es) => {
          val got = es.size
          inFlight = false
          es.foreach{ e =>
            buffer += e
            frontier ++= loop(e)
          }
          if (downstreamWaiting && buffer.nonEmpty) {
            val e = buffer.dequeue
            downstreamWaiting = false
            sendOne(e)
          } else {
            checkCompletion()
          }
          ()
        }
      }

      toProcess.onComplete(callback.invoke)
    }
    override def preStart(): Unit = {
      checkCompletion()
    }

    def checkCompletion(): Unit = {
      if (!inFlight && buffer.isEmpty && frontier.isEmpty) {
        completeStage()
      }
    } 

    def sendOne(e: E): Unit = {
      Push(out, e)
      checkCompletion()
    }

    def onPull(): Unit = {
      if (buffer.nonEmpty) {
        sendOne(buffer.dequeue)
      } else {
        downstreamWaiting = true
      }

      if (!isBufferFull && frontier.nonEmpty) {
        fillBuffer()
      }
    }

    setHandler(out, this)
  }
}
3

未完了の原因

ストリームが完了しない原因は、「バッファでフローを使用する」ためではないと思います。 この質問 と同様の実際の原因は、デフォルトパラメータeagerClose=Falseとのマージがsourcebufferの両方で待機しているという事実です。 (マージ)が完了する前に完了します。しかし、バッファはマージが完了するのを待っています。したがって、マージはバッファを待機しており、バッファはマージを待機しています。

eagerCloseマージ

マージの作成時にeagerClose=Trueを設定できます。ただし、eager closeを使用すると、残念ながら一部の子のItemId値が照会されなくなる可能性があります。

間接解

ツリーのレベルごとに新しいストリームを具体化すると、再帰をストリームの外側に抽出できます。

itemFlowを使用してクエリ関数を作成できます。

val itemQuery : Iterable[ItemId] => Future[Seq[Data]] = 
  (itemIds) => Source.apply(itemIds)
                     .via(itemFlow)
                     .runWith(Sink.seq[Data])

このクエリ関数は、再帰的なヘルパー関数の内部でラップできるようになりました。

val recQuery : (Iterable[ItemId], Iterable[Data]) => Future[Seq[Data]] = 
  (itemIds, currentData) => itemQuery(itemIds) flatMap { allNewData =>
      val allNewKids = allNewData.flatMap(_.kids).toSet

      if(allNewKids.isEmpty)
        Future.successful(currentData ++ allNewData)
      else
        recQuery(allNewKids, currentData ++ data)
  }

作成されるストリームの数は、ツリーの最大深度に等しくなります。

残念ながら、Futuresが関係しているため、この再帰関数は末尾再帰ではなく、ツリーが深すぎると「スタックオーバーフロー」が発生する可能性があります。

ああ、Akkaストリームのサイクルの喜び。私は非常に類似した問題を抱えていましたが、それを深くハッキーな方法で解決しました。おそらくそれはあなたのために役立つでしょう。

Hacky Solution:

  // add a graph stage that will complete successfully if it sees no element within 5 seconds
  val timedStopper = b.add(
    Flow[Item]
      .idleTimeout(5.seconds)
      .recoverWithRetries(1, {
        case _: TimeoutException => Source.empty[Item]
      }))

  source ~> merge ~> fetch ~> timedStopper ~> bcast ~> data
  merge <~ buffer <~ kids <~ bcast

これが行うことは、最後の要素がtimedStopperステージを通過してから5秒後に、そのステージがストリームを正常に完了するということです。これは、ストリームをidleTimeoutで失敗するTimeoutExceptionを使用して達成され、次にrecoverWithRetriesを使用してその失敗を正常に完了します。 (私はそれがハックだったと述べました)。

要素間に5秒を超える可能性がある場合、またはストリームが「実際に」完了してからAkkaがそれを取得するまでに長い待ち時間がない場合は、これは明らかに適切ではありません。ありがたいことに、どちらも私たちの懸念事項ではありませんでした。

非ハックソリューション

残念ながら、タイムアウトを介さずにこれを行うには、非常に複雑な方法しか考えられません。

基本的に、2つのことを追跡できる必要があります。

  • まだバッファ内にある要素、またはバッファに送信中の要素はありますか
  • 入ってくるソースはオープンですか

両方の質問に対する回答がnoの場合にのみストリームを完了します。ネイティブAkkaビルディングブロックはおそらくこれを処理できません。ただし、カスタムグラフステージの場合もあります。オプションは、Mergeの代わりとなるものを記述して、バッファーの内容を知る何らかの方法を提供するか、受信したIDとブロードキャストがバッファーに送信しているIDの両方を追跡することです。 。問題は、カスタムグラフステージは、このようなステージ間でロジックを混在させる場合はもちろんのこと、最高のタイミングで書くのは特に快適ではないということです。

警告

Akkaストリームは、サイクル、特に完了の計算方法ではうまく機能しません。その結果、発生する問題はこれだけではありません。

たとえば、非常によく似た構造で発生した問題は、ソースの障害が正常に完了したストリームとして扱われ、成功したFutureが具体化されることでした。問題は、デフォルトでは、失敗したステージはそのダウンストリームで失敗しますが、そのアップストリームをキャンセルすることです(これらのステージの正常終了としてカウントされます)。あなたが持っているようなサイクルで、キャンセルは一方のブランチに伝播し、もう一方のブランチに障害が発生するため、結果は競合になります。また、シンクでエラーが発生した場合にどうなるかを確認する必要もあります。ブロードキャストのキャンセル設定によっては、キャンセルが上向きに伝搬せず、ソースが要素を引き込み続ける可能性があります。

最後のオプションの1つは、ストリームで再帰ロジックを処理しないようにすることです。極端な例として、ネストされたすべてのアイテムを一度に引き出してFlowステージに入れる単一の末尾再帰メソッドを作成する方法がある場合は、問題を解決できます。もう1つは、Kafka独自のシステムのキューイングに行くことを真剣に検討しています。

0
Astrid