Страничка курса: https://maxcom.github.io/scala-course-2022/
Преобразования потоков данных.
Асинхронная обработка
Проблемы, которые нужно решить:
При синхронной обработке скорость
регулируется автоматически.
Back pressure можно делать на акторах, но сложно.
Не делаем сложные протоколы, используем Streams
Reactive streams - API для интеграции,
не для
конечного пользователя.
https://www.reactive-streams.org/
Входит в JDK 9+
В идеальном мире нам нужно писать только преобразование.
Позже посмотрим интеграцию с другим асинхронным кодом.
https://doc.akka.io/docs/akka/current/stream/stream-composition.html
Два API для сборки цепочек:
Подключаем в проект
libraryDependencies +=
"com.typesafe.akka" %% "akka-stream" % "2.6.19"
версия должна соответствовать версии Akka
можно использовать в приложениях без акторов
// не путаем с javadsl
import akka.stream.scaladsl._
Пример - замена 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
// классическая 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 - запуск интерпретации. Включает оптимизации.
val source: Source[Int, NotUsed] = Source(input)
// final class Source[+Out, +Mat]
Mat - materialized value, возникает при запуске
может быть у любого компонента,
но обычно у Source/Sink
Пример Source с materialized value
val source: Source[Int, SourceQueueWithComplete[Int]] =
Source.queue[Int](bufferSize = 1000,
OverflowStrategy.backpressure)
очередь возникает при материализации
val (queue, result):
(SourceQueueWithComplete[Int], Future[Done]) =
source
.toMat(Sink.foreach(println))(Keep.both)
.run()
Разбираем на компоненты
val source: Source[Int, NotUsed] = Source(List.range(1, 10000))
val flow: Flow[Int, Int, NotUsed] = Flow[Int].mapAsync(10)(func)
val sink: Sink[Int, Future[Seq[Int]]] = Sink.seq[Int]
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 = ???
val source2 = ???
// один за другим
source1.concat(source2)
// по 10 из каждого по порядку
source1.interleave(source2, 10)
// в порядке готовности
source1.intersperse(source2)
Sink
Stream завершается при возникновении ошибки.
Можно работать с Try в потоке.
Рестарт при сбоях
RestartSource.onFailuresWithBackoff(
RestartSettings(
minBackoff = 100 millis,
maxBackoff = 10 minutes,
randomFactor = 0.2))(() => source)
бывают еще RestartFlow и RestartSink
val source: Source[Int, SourceQueueWithComplete[Int]] =
Source.queue[Int](bufferSize = 1000,
OverflowStrategy.backpressure)
val (queue, newSource) = source.preMaterialize()
val func: (Int => Future[Int]) = ???
val safeSource = RestartSource.onFailuresWithBackoff(
RestartSettings(
minBackoff = 100.millis,
maxBackoff = 10.minutes,
randomFactor = 0.2))(() => newSource.mapAsync(1)(func))
Пример: поточная отдача JSON в Akka HTTP
(подробнее в 12-лекции курса 2020)
get {
val source: Source[Data, NotUsed] = getDatabaseStream(user)
complete(source) // порождает Json массив
}
что с ошибками?
Ошибки вместо пустых ответов:
def dataOrFail[T]
(source: Source[T, NotUsed]): Future[Source[T, NotUsed]] = {
source
.prefixAndTail(1) // дает Source из одного элемента
.runWith(Sink.head)
.map { case (first, rest) => // (Seq[T], Source[T, NotUsed]
Source(first).concat(rest)
}
}
(future падает при ошибке получения 1-го элемента)
Интеграция через Source.queue
val source: Source[Int, SourceQueueWithComplete[Int]] =
Source.queue[Int](bufferSize = 1000,
OverflowStrategy.backpressure)
// dropHead | dropTail | dropBuffer | fail
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
(Пример: scroll запросы к Elasticsearch)
Интеграция с акторами
// для typed акторов
libraryDependencies +=
"com.typesafe.akka" %% "akka-stream-typed" % "2.6.19"
ActorSink
// 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]
стрим наблюдает за жизнью актора
Напоминаю: