Страничка курса: https://maxcom.github.io/scala-course-2022/
Akka - набор библиотек.
Сегодня рассмотрим ядро - акторы Akka.
Модель акторов - 1973 год,
использовалась как модель для описания параллельных систем.
Позже стала применяться в качестве базы для практических реализаций.
Популярность получила благодаря Erlang:
языку программирования и платформе,
разработанной Ericsson (1987 г.)
Erlang динамический + другая модель многопоточности
Дальше будем говорить про реализацию модели в библиотеке Akka.
Подключаем в проект: build.sbt
libraryDependencies +=
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.19"
libraryDependencies += // или другой backend Slf4j
"ch.qos.logback" % "logback-classic" % "1.2.11"
Два варианта API:
"Классические" акторы в лекции 2018 года.
Правильные import'ы:
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
Не перепутайте с javadsl и "классическими" api
Разделяемое изменяемое состояние + параллелизм = проблемы
Разделяемое изменяемое состояние + параллелизм =
«Share nothing» архитектура
Актор — асинхронный объект
Объект | Актор | |
---|---|---|
Вызов | counter.incr(n) | counter ! Incr(n) |
Запрос | counter.get() ⇒ Int |
counter.ask(Get) ⇒ Future[Int] |
В ООП логика моделируется взаимодействием объектов.
В модели акторов - взаимодействием акторов путем обмена сообщениями.
В ООП методы можно вызывать из разных потоков.
Потокобезопасность?
У актора обработка сообщений строго последовательна.
Разные акторы работают независимо в разных потоках.
Общение двух акторов последовательно
(очередь не
перемешивается).
Пример: актор счетчика
object Counter {
sealed trait Protocol
case class Incr(n: Long) extends Protocol
case class Get(replyto: ActorRef[Long]) extends Protocol
def behavior: Behavior[Protocol] = ???
}
Актор с изменяемым состоянием
def behavior: Behavior[Protocol] = Behaviors.setup { ctx =>
// изменяемое состояние
var value: Long = 0
Behaviors.receiveMessage {
case Incr(n) =>
value += n
Behaviors.same // не меняем behavior
case Get(replyto) =>
replyto ! value
Behaviors.same
}
}
"Immutable" актор
private def counter(value: Long): Behavior[Protocol] =
Behaviors.receiveMessage {
case Incr(n) =>
counter(value + n) // новый behavior
case Get(replyto) =>
replyto ! value
Behaviors.same
}
def behavior: Behavior[Protocol] = counter(0)
"Immutable" предпочтительнее, но не всегда эффективен
mutable.Queue и многие другие структуры эффективнее immutable
Запускаем Akka
object Main extends App {
val system: ActorSystem[Counter.Protocol] = // корневой актор
ActorSystem(Counter.behavior, "counter") // "counter" - имя
// оправка в system это отправка корневому актору
system ! Counter.Incr(1)
system.terminate()
}
запуск множества акторов будет дальше
Используем Ask
import akka.actor.typed.scaladsl.AskPattern._
object Main extends App {
implicit val system: ActorSystem[Counter.Protocol] = ???
// ^^^^^^ ask создаем временные акторы
system ! Counter.Incr(1)
implicit val timeout: Timeout = 10.seconds
val result: Future[Long] = system ? Counter.Get
println(Await.result(result, 1.minutes))
system.terminate()
}
Приватное состояние - не только переменные.
Актор может скрывать за собой внешние сущности:
Актор должен иметь приватное состояние
Исключение - отказоустойчивость или балансировка нагрузки
Что можно отправлять актору?
Изменяемые объекты посылать нельзя!
Можно ли посылать функции?
Можно ли посылать внешние объекты, вроде коннектов к БД, сокетов, т.п.?
Пример актора: классификатор с обучением
Один актор на классификацию и обучение - просто, но не эффективно
Актор для обучения, get запрос для модели
(нужна immutable модель или функция копирования)
Акторы могут быть расположены в разных виртуальных машинах и на разных серверах.
Взаимодействие акторов по сети не похоже на RPC (Remote Procedure Call).
context.spawn(HelloWorld(), "greeter")
(об этом позже)
SpawnProtocol - актор, порождающий акторы
implicit val system: ActorSystem[SpawnProtocol.Command] =
ActorSystem(SpawnProtocol(), "root")
val counterF: Future[ActorRef[Counter.Protocol]] =
system.ask(Spawn(Counter.behavior, "counter", Props.empty, _))
// _ для replyTo, иногда нужно явно указать тип
Факт: «Титаник» утонул из-за плохого разделения на отсеки.
Указываем диспетчер при создании актора
context.spawn(yourBehavior, "BlockingDispatcher",
DispatcherSelector.blocking())
context.spawn(yourBehavior, "DispatcherFromConfig",
DispatcherSelector.fromConfig("your-dispatcher"))
application.conf
default-blocking-io-dispatcher {
type = "Dispatcher"
executor = "thread-pool-executor"
throughput = 1 // переключение после N сообщений
thread-pool-executor {
fixed-pool-size = 16
}
}
Обращаемся к асинхронным API в акторе
Пример
object CacheActor {
trait Protocol
// отдать из кеша
case class Get(replyto: ActorRef[String])
extends Protocol
// обновить кеш и отдать новое
case class RefreshAndGet(replyto: ActorRef[String])
extends Protocol
def behavior(source: => Future[String]): Behavior[Protocol] =
???
}
def behavior(source: => Future[String]): Behavior[Protocol] =
Behaviors.setup { ctx =>
var current = "undefined"
Behaviors.receiveMessage {
case Get(replyto) =>
replyto ! current
Behaviors.same
case RefreshAndGet(replyto) =>
import ctx.executionContext // для onComplete
source.onComplete(???)
Behaviors.same
}
}
Можно ли из onComplete менять state актора?
добавляем приватный Refresh(value)
case RefreshAndGet(replyto) =>
import ctx.executionContext
source.onComplete {
case Success(v) =>
ctx.self ! Refresh(v)
replyto ! v
// или Refresh(v, replyto) и отправка после Refresh
case Failure(ex) => // handle failure
}
Behaviors.same
case Refresh(value) =>
current = value
Behaviors.same
pipeTo: посылаем результат себе самому
// преобразуем результат в протокол
ctx.pipeToSelf(source) {
case Success(v) =>
Refresh(v)
case Failure(ex) =>
RefreshFailed(ex)
}
AskPattern внутри актора: "context.ask"
// преобразуем результат в протокол
ctx.ask(sourceActor, Get) {
case Success(v) =>
Refresh(v)
case Failure(ex) =>
RefreshFailed(ex)
}
Пример: откладываем Get до окончания Refresh
В синхронном варианте проблемы нет.
Задаем stash и state машину
def behavior(source: => Future[String]): Behavior[Protocol] =
Behaviors.setup { ctx =>
var current = "undefined"
Behaviors.withStash(capacity = 1000) { stash =>
def working: Behavior[Protocol] = ??? // ждем запрос
def waiting: Behavior[Protocol] = ??? // ждем refresh
working
}
}
обработка запроса
def working: Behavior[Protocol] =
Behaviors.receiveMessagePartial {
case Get(replyto) =>
replyto ! current
Behaviors.same
case RefreshAndGet(replyto) =>
ctx.pipeToSelf(source) {
case Success(v) =>
Refresh(v)
case Failure(ex) =>
RefreshFailed(ex)
}
waiting // переход в waiting
}
ожидание Refresh
def waiting: Behavior[Protocol] = Behaviors.receiveMessage {
case Refresh(value) =>
current = value
stash.unstashAll(working)
case RefreshFailed(ex) => // нужна обработка
stash.unstashAll(working)
case other =>
// помним, что stash может переполниться
stash.stash(other)
Behaviors.same
}
Какой размер Stash?
Сколько не жалко
Сколько имеет смысл накапливать
Число операций O(N2)
N - размер Stash
Иногда эффективнее накапливать
запросы
в другой структуре
Deadline - точка во времени
import scala.concurrent.duration._
val deadline: Deadline = 5.minutes.fromNow
deadline.hasTimeLeft() // проверки
deadline.isOverdue()
// величина таймаута, например для ask
val timeout: Duration = deadline.timeLeft
еще их можно использовать для вычисления длительности операций
Совет: в REST передавайте таймаут в заголовке
Плохая ситуация: клиент делает retry раньше, чем заканчивается обработка
Таймеры
Behaviors.withTimers { timers =>
// key для уникальности таймера
// msg - отправляемое сообщение
timers.startSingleTimer(key = CheckTimeout,
msg = CheckTimeout,
1.minute)
timers.startPeriodicTimer(key = Tick, msg = Tick, 1.minute)
timers.cancel(Tick)
timers.cancelAll()
Зачем?
Receive Timeout - реакция на отсутствие сообщений
Защитный механизм для коротокоживущих акторов
ctx.setReceiveTimeout(5.minutes, ReceiveTimeout)
// отправляет ReceiveTimeout при отсутствии активности
Игнорирует сообщения, расширяющие NotInfluenceReceiveTimeout
В ООП и ФП ошибки обрабатывает тот,
кто вызывает функцию.
Правильно ли это?
Пример: виды ошибок в автомате с газировкой:
Родитель - супервизор дочернего актора
DriverActor → ConnectionActor → TransactionActor
По умолчанию: остановка.
Не забываем про это!!!
Задаем Supervisor
def behavior(source: => Future[String]): Behavior[Protocol] =
Behaviors
.supervise[Protocol](...)
.onFailure[Throwable](SupervisorStrategy.restart)
разная стратегия для разных исключений
Пример: ConnectionActor
Behaviors
.supervise[Protocol](...)
.onFailure[ConnectionResetException]
(SupervisorStrategy.restartWithBackoff(
minBackoff = 100 millis,
maxBackoff = 30 seconds,
randomFactor = 0.2))
.onFailure[DatabaseWasDeletedException]
(SupervisorStrategy.stop)
Пример: один запрос – три иерархии.
Ожидание остановки актора:
context.watch(anotherActorRef)
Ловим сообщение Terminated(actorRef)
.receiveMessage {
...
}.receiveSignal {
case (ctx, Terminated(actor)) =>
Behaviors.same
} // другие сигналы например: PostStop, PreRestart
Не обработали: работает «death pact» — останавливаемся вместе (DeathPactException).
В следующий раз:
Напоминаю: