Страничка курса: https://maxcom.github.io/scala-course-2020/
Преобразования потоков данных.
Асинхронная обработка
Возникает проблемы:
При синхронной обработке скорость
регулируется автоматически.
Back pressure можно делать на акторах, но сложно.
Reactive streams - API для интеграции, не для конечного пользователя.
В идеальном мире нам нужно писать только преобразование.
Позже посмотрим интеграцию с другим асинхронным кодом.
https://doc.akka.io/docs/akka/current/stream/stream-composition.html
Два API для сборки цепочек:
Пример - замена Future.sequence
def func(x: Int): Future[Int] = ???
val input: Seq[Int] = Seq.range(1, 10000)
val output: Future[Seq[Int]] = Future.sequence(input.map(func))
проблема - неограниченный параллелизм, перегрузка ExecutionContext
libraryDependencies +=
"com.typesafe.akka" %% "akka-stream" % "2.6.4"
версия должна соответствовать версии Akka
// классическая ActorSystem (можно и typed)
implicit val actorSystem: ActorSystem = ActorSystem()
def func(x: Int): Future[Int] = ???
val input: List[Int] = List.range(1, 10000)
val output: Future[Seq[Int]] =
Source(input).mapAsync(10)(func).runWith(Sink.seq)
// 10 потоков
Materialization - запуск интерпретации.
Например runWith, но есть и другие варианты.
val source: Source[Int, NotUsed] = Source(input)
// final class Source[+Out, +Mat]
Mat - materialized value
Пример Source с materialized value
val source: Source[Int, ActorRef] =
Source.actorRef[Int](1000, OverflowStrategy.dropTail)
ActorRef возникает при материализации.
val flow: Flow[Int, Int, NotUsed] = Flow[Int].mapAsync(10)(func)
val sink: Sink[Int, Future[Seq[Int]]] = Sink.seq
val output: Future[Seq[Int]] = source.via(flow).runWith(sink)
Более 100 операторов
flatMap?
Группировки
есть еще аналоги с "весом" элемента
Группировка:
def processBatch(v: Seq[Int]): Future[Seq[Int]] = ???
source
.groupedWithin(1000, 1 minute) // до 1000, в течении минуты
.mapAsync(16)(processBatch)
.mapConcat(identity) // поток Seq[Int] в поток Int
.runWith(Sink.ignore)
обработка "пачками" часто эффективнее
Ограничение скорости
throttle(elements: Int, per: FiniteDuration)
Несколько источников:
val source1 = Source(Seq.range(1, 1000))
val source2 = Source(Seq.range(1, 1000))
// один за другим
source1.concat(source2)
// по 10 из каждого по порядку
source1.interleave(source2, 10)
// в порядке готовности
source1.intersperse(source2)
Sink
Stream завершается при возникновении ошибки.
Можно работать с Try в потоке.
Рестарт при сбоях
RestartSource.onFailuresWithBackoff(
minBackoff = 100 millis,
maxBackoff = 10 minutes,
randomFactor = 0.2)(() => source)
бывают еще RestartFlow и RestartSink
Интеграция через Source.queue
val source: Source[Int, SourceQueueWithComplete[Int]] =
Source.queue[Int](bufferSize = 1000,
OverflowStrategy.backpressure)
// backpressure или выбрасываем элементы
val queue: SourceQueueWithComplete[Int] = source
.to(Sink.foreach(println))
.run()
// очередь - "материализованное" значение
val result: Future[QueueOfferResult] = queue.offer(1000)
// при backpressure нужно ждать вычисления Future
Source.unfold
// числа Фибоначчи
val fib: Source[Int, NotUsed] = Source.unfold(0 -> 1) {
case (a, _) if a > 10000000 => None
case (a, b) => Some((b -> (a + b)) -> a)
}
есть асинхронный аналог - unfoldAsync
Интеграция с акторами
libraryDependencies +=
"com.typesafe.akka" %% "akka-stream-typed" % "2.6.4"
// T - тип значения
// M - протокол актора
// A - тип Ack
def actorRefWithBackpressure[T, M, A](
ref: ActorRef[M],
messageAdapter: (ActorRef[A], T) => M,
onInitMessage: ActorRef[A] => M,
ackMessage: A,
onCompleteMessage: M,
onFailureMessage: Throwable => M): Sink[T, NotUsed]
Напоминаю: