Страничка курса: https://maxcom.github.io/scala-course-2020/
Сообщение теряется при:
Только одна попытка доставки:
«at most once delivery»
Потеря сообщений - проблема "модели"?
Пример: кофейня готовит кофе и принимает платежи
Наша кофейня
использует идеальную БД
Счет каждого посетителя хранится в табличке
Списания - через UPDATE
Согласована ли связка ПО + БД?
Наивное предположение:
Что значит ошибка сети или таймаут?
Таймаут? Таймаут есть всегда
В асинхронной системе похожие проблемы:
Эти проблемы - проблемы как с акторами, так и без.
Решение проблемы
За одно отметим:
Сбой нельзя детектировать (в общем случае).
Сбой и таймаут не отличимы.
Не нужно решать проблему ненадежной доставки. Вместо этого нужно обеспечить требуемые гарантии при сбоях.
А еще доставка внутри JVM гарантирована.
Положительный ответ только после записи в хранилище или выполнения действия
Откладывание выполнения с гарантиями - передача "эстафетной палочки"
Пример: гарантированное выполнение независимых запросов
object Requester {
sealed trait Protocol
case class Request(id: UUID, query: String,
replyTo: ActorRef[String]) extends Protocol
private implicit val timeout: Timeout = 10.seconds
private val recheck: FiniteDuration = 1.second
def behavior(worker: ActorRef[Protocol]): Behavior[Protocol]
}
private case object Tick extends Protocol
Behaviors.withTimers { timers =>
timers.startTimerWithFixedDelay(Tick, recheck)
...
}
private case class Finished(request: Request, response: String)
extends Protocol
private case class Failed(id: UUID, ex: Throwable)
extends Protocol
def process(processing: Map[UUID, (Request, Deadline)]) = {
def start(ctx: ActorContext[Protocol])(rq: Request) = {
ctx.ask(worker, Request(rq.id, rq.query, _)) {
case Success(r) => Finished(rq, r)
case Failure(ex) => Failed(rq.id, ex)
}
rq.id -> (rq, timeout.duration.fromNow)
}
...
Behaviors.receive[Protocol] {
case (ctx, rq: Request) =>
ctx.ask(worker, Request(rq.id, rq.query, _)) {
case Success(r) => Finished(rq, r)
case Failure(ex) => Failed(rq.id, ex)
}
process(processing + start(ctx)(rq))
...
case (_, Finished(rq, response)) =>
rq.replyTo ! response
process(processing - rq.id)
case (_, Failed(id, ex)) =>
Behaviors.same // logging?
case (ctx, Tick) =>
val restarted = processing.values
.filter(_._2.isOverdue()).map(_._1)
process(processing ++ restarted.map(start(ctx)))
Особенности реализации:
Нужено сохранить порядок?
пронумеруем сообщения
Akka 2.6.4: готовая реализация для нескольких сценариев
Пример в сервисе индексации сообщений
«Happy path» — новые сообщений индексируем сразу; игнорируем сбои
«Self heal» – периодически проводим сверку и запускаем в обработку то, что не смогли обработать ранее.
«Предохранитель», которые можно вставлять между подсистемами.
«Разрывает цепь» при частых ошибках.
«Замыкает цепь» после таймаута и после успешных пробных запросов.
Можно использовать вне Akka.
Пример:
private val breaker = new CircuitBreaker(
scheduler = scheduler, // context.system.scheduler
maxFailures = 5,
callTimeout = 10.seconds,
resetTimeout = 1.minute
)
def process(task: Task): Future[Result] = {
breaker.withCircuitBreaker {
askAnotherService(task)
}
}
Проблема: быстрый producer + медленный consumer = Out Of Memory
Решение: выбрасываем то, что не успеваем обработать
Решение: замедляем producer'а (back pressure)
Раньше делали сами - считали подтверждения для оценки размера очереди.
Сейчас используем Akka Streams:
поговорим об этом на 8-м занятии.
Раздаем задачи пулу акторов
Два механизма использования:
Простые стратегии:
object Calculator {
case class Calc(x: Long, replyto: ActorRef[Long])
private def worker: Behavior[Calc] = Behaviors.receiveMessage {
case Calc(x, replyto) =>
replyto ! x*x
Behaviors.same
}
...
}
def calculator: Behavior[Calc] = Behaviors.setup[Calc] { ctx =>
val pool = Routers.pool(poolSize = 16)(
Behaviors.supervise(worker)
.onFailure[Exception](SupervisorStrategy.restart))
val router = ctx.spawn(pool, "worker-pool")
Behaviors.receiveMessage[Calc] { msg =>
router ! msg
Behaviors.same
}
}
Проблема - сообщения перемешиваются
ConsistentHashingRouting - роутинг по ключу на основе сообщения
В классической Akka есть еще интересные роутеры
ScatterGatherFirstCompletedRouting
Отправляем всем, используем первый ответ.
Улучшаем время отклика, но выполняем работу N раз
TailChoppingRouting
Отправляем первому, через короткий таймаут второму и продолжаем пока не получим первый ответ.
Улучшаем худшие времена отклика.
Напоминаю:
План задания
Как использовать акторы в контроллерах Play?
Play использует "классический" API
import akka.actor.ActorSystem
@Singleton // обязательно! иначе акторы будут размножаться
class Classifier @Inject()(cc: ControllerComponents,
actorSystem: ActorSystem)
extends AbstractController(cc) {
import akka.actor.typed.scaladsl.adapter._
val typedActor: ActorRef[Protocol] =
actorSystem.spawn(behavior, "name")