ツリーのような形の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()
ただし、バッファ付きのフローを使用しているため、ストリームは完了しません。
上流が完了し、バッファリングされた要素が排出されると完了します
グラフサイクル、活性、デッドロック セクションを何度も読みましたが、それでも答えを見つけるのに苦労しています。
これにより、ライブロックが作成されます。
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
私は自分の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)
}
}
未完了の原因
ストリームが完了しない原因は、「バッファでフローを使用する」ためではないと思います。 この質問 と同様の実際の原因は、デフォルトパラメータeagerClose=False
とのマージがsource
とbuffer
の両方で待機しているという事実です。 (マージ)が完了する前に完了します。しかし、バッファはマージが完了するのを待っています。したがって、マージはバッファを待機しており、バッファはマージを待機しています。
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独自のシステムのキューイングに行くことを真剣に検討しています。