Skip to content

Latest commit

 

History

History
701 lines (537 loc) · 43.1 KB

p15-actors-failure.md

File metadata and controls

701 lines (537 loc) · 43.1 KB

Глава 15: Обработка ошибок в системе акторов

В предыдущей главе мы познакомились с моделью акторов — вторым основополагающим понятием параллельных вычислений. Она дополняет модель, основанную на компонуемых асинхронных вычислениях (Future и Promise). Мы узнали как акторы определяются, как отправлять актору сообщения, как сообщения обрабатываются, как актор может обновлять изменяемое состояние при обработке сообщений или отправлять ответное сообщение отправителю сообщения.

Надеюсь этого материала было вполне достаточно для того, чтобы заинтересовать вас моделью акторов. Но мы не коснулись ряда ключевых понятий, необходимых для построения более менее серьёзных приложений, основанных на акторах.

Модель акторов предназначена для построения отказоустойчивых приложений. в этой статье мы посмотрим как реагировать на ошибки в приложениях, основанных на акторах. Обработка ошибок в системах акторов сильно отличается от традиционного подхода, принятого при построении серверов.

Способ обработки ошибок тесно связан с понятиями Akka и некоторыми элементами, из которых состоит система акторов в Akka. Поэтому в данной статье мы продолжим изучение реализации модели акторов в Akka.

Иерархии акторов

Перед тем как обратиться к тому, что происходит когда возникает ошибка, необходимо усвоить одно из ключевых понятий в модели акторов. Это понятие лежит в основе построения отказоустойчивых приложений. Акторы образуют иерархии.

Но что это значит? Во первых, это означает что у каждого актора есть актор-родитель, и каждый актор может создавать дочерние акторы. Родительские акторы наблюдают за своими детьми, точно так же как и в настоящей жизни. Они заботятся о них, помогают им подняться на ноги, если они споткнутся. Вскоре мы узнаем как это происходит.

Актор-охранник

В прошлой статье мы успели определить два актора — для бармена и посетителя. Я не буду повторять определения. Они очень простые, давайте сосредоточимся на том, как создавались значения для акторов:

import akka.actor.ActorSystem

val system = ActorSystem("Coffeehouse")
val barista = system.actorOf(Props[Barista], "Barista")
val customer = system.actorOf(Props(classOf[Customer], barista), "Customer")

Мы создаём два актора вызовом метода actorOf, что определён на значении типа ActorSystem.

Кто является родителем для этих двух акторов? Система акторов? Не совсем верно, но близко. Система акторов сама не является актором, но в ней определён так называемый актор-охранник (guardian actor), который является родителем для всех корневых акторов, то есть тех, которыx мы создаём вызовом метода actorOf на ActorSystem.

В нашей системе должно быть совсем немного таких акторов. Оправдано иметь лишь несколько корневых акторов, каждый из которых делегирует большую часть своей работы дочерним акторам.

Пути для акторов

Иерархическая структура системы акторов становится очевидной, если мы взглянем на пути для созданных акторов. Пути — это по сути URL для акторов, по которым мы можем ссылаться на акторы. Мы можем получить путь к актору вызовом метода path на его ссылке ActorRef:

barista.path // => akka.actor.ActorPath = akka://Coffeehouse/user/Barista
customer.path // => akka.actor.ActorPath = akka://Coffeehouse/user/Customer

За протоколом akka следует имя нашей системы акторов, потом имя пользовательского актора-охранника, и наконец имя, которое мы дали актору при создании, в вызове actorOf. В случае распределённых систем для акторов, работающих на удалённых машинах мы увидим имя хоста и порт.

Пути к акторам могут быть использованы для поиска акторов. к примеру вместо того, чтобы передать Customer ссылку на актор бармена, мы могли бы найти этот актор вызовом метода actorSelection на ActorContext, передав путь в качестве аргумента:

context.actorSelection("../Barista")

Однако лучше передать зависимость от актора в конструктор посетителя, так как мы делали это раньше. Зависимость от зашитых в код путей к акторам может привести к возникновению багов, и такой код гораздо труднее изменять.

Пример иерархии

Давайте разовьём наш пример с кафетерием, для того чтобы разбираться как родительские акторы могут следить за дочерними и как это может помочь нам в повышении отказоустойчивости приложения. Определим дочерний актор для бармена, теперь бармен сможет передать часть работы своему дочернему актору.

Нам следовало бы определить сразу несколько дочерних акторов под разные задачи бармена, но мы не будем распыляться и немного упростим нашу модель.

Предположим что в баре есть кассовый аппарат (Register), который создаёт чеки и обновляет счётчик общих продаж за день. Вот пробная версия такого актора:

import akka.actor._

object Register {
  sealed trait Article
  case object Espresso extends Article
  case object Cappuccino extends Article
  case class Transaction(article: Article)
}

class Register extends Actor {
  import Register._
  import Barista._

  var revenue = 0
  val prices = Map[Article, Int](Espresso -> 150, Cappuccino -> 250)

  def receive = {
    case Transaction(article) =>
      val price = prices(article)
      sender ! createReceipt(price)
      revenue += price
  }

  def createReceipt(price: Int): Receipt = Receipt(price)
}

Он содержит неизменяемый ассоциативный массив для цен по каждому товару и переменную, которая обозначает общий доход. Как только он получает сообщение Transaction, он обновляет счётчик дохода и возвращает чек отправителю.

Актор Register будет дочерним актором бармена, поэтому мы создадим его не из системы акторов, а из актора Barista. Исходная версия нашего родительского актора выглядит так:

object Barista {
  case object EspressoRequest
  case object ClosingTime
  case class EspressoCup(state: EspressoCup.State)

  object EspressoCup {
    sealed trait State
    case object Clean extends State
    case object Filled extends State
    case object Dirty extends State
  }

  case class Receipt(amount: Int)
}

class Barista extends Actor {
  import Barista._
  import Register._
  import EspressoCup._
  import context.dispatcher
  import akka.util.Timeout
  import akka.pattern.ask
  import akka.pattern.pipe
  import concurrent.duration._

  implicit val timeout = Timeout(4.seconds)

  val register = context.actorOf(Props[Register], "Register")

  def receive = {
    case EspressoRequest =>
      val receipt = register ? Transaction(Espresso)
      receipt.map((EspressoCup(Filled), _)).pipeTo(sender)
    case ClosingTime => context.stop(self)
  }
}

Сначала мы определили тип сообщений для нашего бармена. Чашка эспрессо EspressoCup может быть в одном из состояний, этот набор зафиксирован с помощью ключевого слова sealed.

Но посмотрим, что происходит в классе Barista. Мы импортировали dispatcher, ask и pipe, объявили неявную переменную timeout, потому что мы пользуемся синхронным обменом сообщений с помощью ?. Как только мы получаем EspressoRequest, мы спрашиваем у нашего актора чек, отправив сообщение Transaction. Затем мы наливаем эспрессо в чашку, присоединяем чек и перенаправляем отправителю заказа. Он получит пару (EspressoCup, Receipt). Мы рассмотрели довольно типичный сценарий для приложений основанных на акторах. В акторе мы распределили обязанности между дочерними акторами и затем собрали полученные данные.

Также обратите внимание на то, как мы создали дочерний актор вызовом actorOf на ActorContext вместо ActorSystem. После этого созданный актор становится дочерним для того актора, который вызвал метод actorOf на своём контексте, а не корневым актором, для которого родителем является актор-охранник.

Наконец определим актор для посетителя Customer. Он является корневым также как и актор бармена:

object Customer {
  case object CaffeineWithdrawalWarning
}

class Customer(coffeeSource: ActorRef) extends Actor with ActorLogging {
  import Customer._
  import Barista._
  import EspressoCup._
  def receive = {
    case CaffeineWithdrawalWarning => coffeeSource ! EspressoRequest
    case (EspressoCup(Filled), Receipt(amount)) =>
      log.info(s"yay, caffeine for ${self}!")
  }
}

Это определение не так интересно для рассматриваемых в данной главе вопросов. Стоит обратить внимание на лишь на то, как мы воспользовались трэйтом ActorLogging. Он позволяет нам писать сообщения в лог вместо консоли.

Теперь если мы создадим систему акторов с барменом и двумя посетителями, мы можем напоить двух наших кофеманов, чашечкой чёрного золота:

import Customer._

val system = ActorSystem("Coffeehouse")

val barista = system.actorOf(Props[Barista], "Barista")
val customerJohnny = system.actorOf(Props(classOf[Customer], barista), "Johnny")
val customerAlina = system.actorOf(Props(classOf[Customer], barista), "Alina")

customerJohnny ! CaffeineWithdrawalWarning
customerAlina ! CaffeineWithdrawalWarning

После запуска этого примера Вы увидите два сообщения в логе от двух радостных посетителей.

Падать или не падать?

Но в этой статье мы заинтересованы совсем не в радостных посетителях, нам интересно узнать, что происходит, когда что-то идёт не так.

Наш кассовый аппарат не так надёжен как нам бы хотелось. Он может зажевать бумагу. Давайте добавим исключение для этого случая, в объект-компаньон для Register:

class PaperJamException(msg: String) extends Exception(msg)

Теперь давайте изменим метод createReceipt в нашем акторе Register:

def createReceipt(price: Int): Receipt = {
  import util.Random
  if (Random.nextBoolean())
    throw new PaperJamException("OMG, not again!")
  Receipt(price)
}

Теперь при обработке сообщения Transaction, наш актор Register будет выдавать исключение в половине случаев.

Как это скажется на нашей системе акторов? К счастью Akka — очень надёжна, исключения не приведут к падению приложения, вместо этого родительский актор будет оповещён о том, что с дочерним актором что-то случилось. И в этом случае у родительского актора может быть несколько вариантов разрешения ситуации.

Стратегии супервизора

Актор оповещается об исключениях, которые произошли в работе дочерних акторов, но не с помощью обработки специальных сообщений в функции Receive. Так мы бы смешали логику актора с логикой обработки исключений. В Akka эти части чётко разделены.

Каждый актор определяет стратегию супервизора (SupervisorStrategy), в которой говорится о том как Akka должна реагировать на определённые ошибки, которые происходят в дочерних акторах.

Есть две основные стратегии: OneForOneStrategy и AllForOneStrategy. Первая означает, что возникающее в некотором дочернем акторе исключение касается только этого актора, вторая говорит о том, что исключение затрагивает все дочерние акторы. Выбор стратегии зависит от приложения.

Вне зависимости от выбранной стратегии нам также нужно задать Decider или частично определённую функцию: PartialFunction[Throwable, Directive], которая ставит в соответствие исключениям директивы, которые говорят Akka что делать при возникновении ошибки.

Директивы

Список доступных директив:

sealed trait Directive
case object Resume extends Directive
case object Restart extends Directive
case object Stop extends Directive
case object Escalate extends Directive
  • Resume: В этом случае мы считаем, что дочерний актор — симулирует и ничего страшного не случилось. В этом случае дочерние акторы продолжат обрабатывать дальнейшие сообщения, словно ничего не случилось.

  • Restart: Эта директива приведёт к перезапуску актора или акторов. Она применяется в том случае, если мы предполагаем, что возникновение исключения нарушило внутреннее состояние актора и он нуждается в перезапуске. Перезапустив актор мы надеемся на то, что он начнёт работу с нормального изначального состояния.

  • Stop: Мы останавливаем актор. Он не будет перезапущен.

  • Escalate: В этом случае мы не знаем, что делать и передаём исключение следующему родительскому актору в иерархии, надеясь на то, что они лучше знают что к чему. В этом случае сам родительский актор, который передал исключение выше может быть перезапущен своим родительским актором. Родительский актор может решать только за своих дочерних акторов.

Стратегия по умолчанию

Нам не нужно задавать стратегии для всех акторов. Пока мы не определяли стратегий супервизора. В таком случае будет использована стратегия по умолчанию:

final val defaultStrategy: SupervisorStrategy = {
  def defaultDecider: Decider = {
    case _: ActorInitializationException  Stop
    case _: ActorKilledException          Stop
    case _: Exception                     Restart
  }
  OneForOneStrategy()(defaultDecider)
}

Это означает, что для всех исключений кроме ActorInitializationException или ActorKilledException, соответствующий дочерний актор будет перезапущен.

Поэтому при возникновении PaperJamException в нашем кассовом аппарате, стратегия супервизора (в акторе бармена) перезапустит актор Register, ведь мы не переопределили стратегию по умолчанию.

При этом мы увидим исключение в стэке вызовов в логе, но мы не заметим, что актор Register бы перезапущен.

Давайте проверим, что это действительно так. Но для этого нам придётся узнать о жизненном цикле актора.

Жизненный цикл актора

Для того чтобы понять, что на самом деле делают директивы в стратегии супервизора, необходимо немного разбираться в жизненном цикле актора. При создании в методе actorOf актор запускается. он может быть перезапущен неограниченное количество раз, если с ним произойдут какие-то проблемы. И наконец, актор может быть остановлен, что ведёт к его окончательной смерти.

Есть несколько методов, которые вызываются на разных этапах жизненного цикла актора. Мы можем их переопределять. Также важно знать о том, как они определены по умолчанию. Давайте вкратце посмотрим на них:

  • preStart: Вызывается перед запуском актора, в нём мы можем выполнить некоторую логику связанную с инициализацией. Определение по умолчанию ничего не содержит.

  • postStop: Ничего не содержит по умолчанию. Позволяет нам освободить ресурсы. Вызывается после того как для актора вызывается метод stop.

  • preRestart: Вызывается сразу перед перезапуском, упавшего актора. По умолчанию останавливает все дочерние акторы и вызывает postStop для освобождения ресурсов.

  • postRestart: Вызывается сразу после перезапуска актора. По умолчанию просто вызывает preStart.

Это означает, что по умолчанию перезапуск актора ведёт к перезапуску его дочерних акторов. Иногда это то, что нам нужно, но если это не так мы можем изменить это поведение с помощью переопределения этих методов.

Давайте проверим перезапускается ли наш актор Register. Добавим вывод сообщения в лог к методу postRestart. Также добавим наследование от трэйта ActorLogging:

override def postRestart(reason: Throwable) {
  super.postRestart(reason)
  log.info(s"Restarted because of ${reason.getMessage}")
}

Теперь если мы отправим двум посетителям кучу сообщений CaffeineWithdrawalWarning, мы обязательно увидим одно из этих сообщений в логах.

Смерть актора

Часто в перезапуске актора нет смысла. К примеру, если актор общается по сети с каким-нибудь сервисом и этот сервис в данный момент не доступен. В этом случае было бы здорово уметь рассказать Akka о том как часто необходимо перезапускать актор в течение некоторого времени. Если предел исчерпан, то актор будет остановлен, что приведёт к окончательной гибели актора. Этот предел может быть указан в конструкторе стратегии супервизора:

import scala.concurrent.duration._
import akka.actor.OneForOneStrategy
import akka.actor.SupervisorStrategy.Restart
OneForOneStrategy(10, 2.minutes) {
  case _ => Restart
}

Теперь то мы можем восстанавливаться самостоятельно?

Итак теперь наша система работает гладко и восстанавливается, когда этот чёртов кассовый аппарат клинит. Но так ли это? Давайте изменим печать в логи:

override def postRestart(reason: Throwable) {
  super.postRestart(reason)
  log.info(s"Restarted, and revenue is $revenue cents")
}

Также давайте добавим печати в логи к нашей частично определённой функции Receive:

def receive = {
  case Transaction(article) =>
    val price = prices(article)
    sender ! createReceipt(price)
    revenue += price
    log.info(s"Revenue incremented to $revenue cents")
}

Ох! Что-то явно не так! Из логов видно, что общий счёт увеличивается до тех пор пока бумага не заклинивается и не происходит перезапуск актора Register. После перезапуска общий счёт снова обнуляется. Потому что перезапуск ведёт к тому, что прежнее значение актора отбрасывается и новое создаётся таким же способом как актор был создан изначально в методе actorOf с помощью Props.

Конечно мы можем изменить стратегию супервизора так, чтобы при исключении PaperJamException мы бы продолжали работу актора:

val decider: PartialFunction[Throwable, Directive] = {
  case _: PaperJamException => Resume
}
override def supervisorStrategy: SupervisorStrategy =
  OneForOneStrategy()(decider.orElse(SupervisorStrategy.defaultStrategy.decider))

Теперь актор не перезапускается при возникновении исключения PaperJamException и состояние не обнуляется.

Ядро ошибки

Итак мы нашли решение для нашей проблемы сохранения состояния для Register. Не так ли?

Да, но иногда, простое продолжение работы актора может оказаться не лучшим решением. Предположим, что нам всё-таки нужно перезапутстить актор, иначе мы не сможем убрать зажёванную бумагу. Мы можем имитировать эту ситуацию с помощью дополнительного логического значения, которое говорит нам: произошёл ли сбой в работе кассового аппарата или нет. Давайте изменим определение для Register следующим образом:

class Register extends Actor with ActorLogging {
  import Register._
  import Barista._

  var revenue = 0
  val prices = Map[Article, Int](Espresso -> 150, Cappuccino -> 250)
  var paperJam = false

  override def postRestart(reason: Throwable) {
    super.postRestart(reason)
    log.info(s"Restarted, and revenue is $revenue cents")
  }

  def receive = {
    case Transaction(article) =>
      val price = prices(article)
      sender ! createReceipt(price)
      revenue += price
      log.info(s"Revenue incremented to $revenue cents")
  }

  def createReceipt(price: Int): Receipt = {
    import util.Random
    if (Random.nextBoolean()) paperJam = true
    if (paperJam) throw new PaperJamException("OMG, not again!")
    Receipt(price)
  }
}

Также уберём стратегию супервизора из актора Barista.

Теперь зажёванная бумага остаётся до тех пор пока мы не перезапустим актор. Но мы не можем сделать этого без потери важной информации — общего счёта.

Самое время воспользоваться шаблоном "ядро ошибки" (error kernel), это просто рекомендация, которая говорит о том, что если актор, содержит важное состояние, то он должен передавать работу, которая может привести к исключениям дочерним акторам. Так мы предотвратим потерю состояния. Иногда имеет смысл выделять по одному дочернему актору для каждой опасной задачи, но в этом нет особой необходимости.

Суть этого шаблона проектирования в том, чтобы держать важное состояние как можно выше в иерархии акторов, а задачи, которые могут приводить к ошибкам — как можно ниже.

Давайте применим этот шаблон к актору Register. Мы будем хранить изменяемый общий счёт в акторе Register, но перенесём поведение, которое может привести к исключениям, а именно печать чеков, в дочерний актор, который мы так и назовём ReceiptPrinter. Посмотрим на его определение:

object ReceiptPrinter {
  case class PrintJob(amount: Int)
  class PaperJamException(msg: String) extends Exception(msg)
}

class ReceiptPrinter extends Actor with ActorLogging {
  var paperJam = false

  override def postRestart(reason: Throwable) {
    super.postRestart(reason)
    log.info(s"Restarted, paper jam == $paperJam")
  }

  def receive = {
    case PrintJob(amount) => sender ! createReceipt(amount)
  }

  def createReceipt(price: Int): Receipt = {
    if (Random.nextBoolean()) paperJam = true
    if (paperJam) throw new PaperJamException("OMG, not again!")
    Receipt(price)
  }
}

Снова мы эмитируем зажёванную бумагу с помощью логического значения и выбрасываем исключение каждый раз, когда кто-нибудь просит нас о том чтобы напечатать чек, если у нас есть зажёванная бумага. Почти весь код заимствован из предыдущего определения для Register. У нас только появился новый тип сообщений PrintJob.

Новый вариант программы лучше не только потому, что мы вынесли опасную часть программы из актора, который содержит обновляемое состояние, но и потому, что код стал намного проще, а значит и понятнее. Актор ReceiptPrinter отвечает только за одну задачу, также упростился и актор Register, который теперь занимается только тем, что обновляет общий счёт, передавая остальную часть работы дочернему актору:

class Register extends Actor with ActorLogging {
  import akka.pattern.ask
  import akka.pattern.pipe
  import context.dispatcher

  implicit val timeout = Timeout(4.seconds)

  var revenue = 0
  val prices = Map[Article, Int](Espresso -> 150, Cappuccino -> 250)
  val printer = context.actorOf(Props[ReceiptPrinter], "Printer")

  override def postRestart(reason: Throwable) {
    super.postRestart(reason)
    log.info(s"Restarted, and revenue is $revenue cents")
  }

  def receive = {
    case Transaction(article) =>
      val price = prices(article)
      val requester = sender
      (printer ? PrintJob(price)).map((requester, _)).pipeTo(self)
    case (requester: ActorRef, receipt: Receipt) =>
      revenue += receipt.amount
      log.info(s"revenue is $revenue cents")
      requester ! receipt
  }
}

Мы не запускаем новый ReceiptPrinter для каждого сообщения Transaction. Вместо этого мы пользуемся стратегией по умолчанию, которая перезапускает актор только при возникновении неполадки.

Следует пояснить новый способ обновления общего счёта. Сначала для получения чека мы посылаем запрос к printer. Мы преобразуем Future к кортежу, который содержит ответ и того кто отправил исходное сообщение Transaction. После этого мы перенаправляем этот кортеж себе. При обработке этого сообщения мы наконец-то можем обновить счётчик и отправить сообщение тому, кто отправил исходное сообщение.

Причина такого длинного пути заключается в том, что мы хотим завериться в успешной печати чека. Мы идём в обход, потому что очень важно не изменять внутреннее состояние актора в Future. При этом мы можем быть уверены, что изменение состояния происходит в том же потоке вычисления, в котором выполняется сам актор.

По той же причине нам необходимо присвоить значение sender некоторой переменной. При вызове map мы больше не находимся в контексте нашего актора. Поскольку sender это метод, он скорее всего вернёт ссылку на какой-нибудь другой актор, а не на тот на который мы рассчитываем.

Ура! Теперь наш актор Register защищён от непредвиденных неполадок!

Конечно, сама идея совмещения обязанностей по печати чеков и обновлению общего счёта вызывает сомнения. Мы сделали это для демонстрации шаблона ядро ошибки, но было бы гораздо лучше разнести их, ведь они плохо сочетаются вместе.

Тайм-ауты

Также нам стоит задуматься о тайм-аутах. Если произойдёт тайм-аут в ReceiptPrinter, то возникнет исключение AskTimeoutException, которое дойдёт до актора Barista в виде не успешно завершённого Future.

Поскольку актор Barista просто вызывает map на этом Future (который сработает только если Future содержит Success) и затем перенаправляет результат (pipeTo) посетителю, то посетитель также получит Failure, которое будет содержать AskTimeoutException.

Но посетитель ни о чём не просит, поэтому для него такое сообщение будет сущей неожиданностью. И на данный момент он совсем не подготовлен к таким сообщениям. Давайте будем дружелюбны к нашем посетителям и будем отправлять в таком случае сообщение ComebackLater. Они понимают такие сообщения. Это приведёт к тому, что они попытаются запросить чашечку эспрессо, но позже. Это гораздо лучше того, что у нас есть сейчас. При настоящем подходе они никогда не узнают о том, что они не смогут получить кофе.

Для этого давайте восстановимся после исключения AskTimeoutException и отобразим его в сообщение ComebackLater. Частично определённая функция Receive для Barista примет вид:

def receive = {
  case EspressoRequest =>
    val receipt = register ? Transaction(Espresso)
    receipt.map((EspressoCup(Filled), _)).recover {
      case _: AskTimeoutException => ComebackLater
    } pipeTo(sender)
  case ClosingTime => context.system.shutdown()
}

Теперь актор Customer узнает о том, что он может обратиться за кофе позже. Так они рано или поздно дождутся желаемой порции.

Наблюдение за гибелью акторов

Другим немаловажным аспектом построения отказоустойчивых приложений является наблюдение за важными зависимостями — зависимости, которые не связаны с отношением родительских и дочерних акторов.

Иногда в нашей системе могут быть акторы, которые зависят друг от друга, но не связаны отношениями прямого родства (ни один из них не является дочерним для другого). Поэтому они не могут быть супервизорами. Но нам хотелось бы уметь наблюдать за такими акторами и иметь возможность среагировать, если с ними случится что-то плохое.

Представьте актор, который отвечает за доступ к базе данных. Нам бы хотелось знать о том, что с этим актором всё в порядке из других акторов приложения, которые могут в нём нуждаться. Возможно нам захочется переключиться в режим устранения неполадок, если с этим актором что-то случится. В других ситуациях мы могли бы запускать запасной актор вместо вышедшего из строя.

В любом случае нам нужно уметь наблюдать за гибелью актора. Можно сделать это вызовом метода watch, который определён на ActorContext. Давайте будем наблюдать из Customer за актором Barista, наши посетители сильно нуждаются в кофе, поэтому вполне оправдано заключить, что они зависят от него:

class Customer(coffeeSource: ActorRef) extends Actor with ActorLogging {
  import context.dispatcher

  context.watch(coffeeSource)

  def receive = {
    case CaffeineWithdrawalWarning => coffeeSource ! EspressoRequest
    case (EspressoCup(Filled), Receipt(amount)) =>
      log.info(s"yay, caffeine for ${self}!")
    case ComebackLater =>
      log.info("grumble, grumble")
      context.system.scheduler.scheduleOnce(300.millis) {
        coffeeSource ! EspressoRequest
      }
    case Terminated(barista) =>
      log.info("Oh well, let's find another coffeehouse...")
  }
}

Мы начинаем наблюдать за coffeeSource, также мы добавили ещё одну case-альтернативу с сообщением Terminated, мы получим это сообщение от Akka, в случае гибели актора, за которым мы следим.

Теперь, если мы отправим сообщение ClosingTime и Barista остановит себя, актор посетителя Customer будет оповещён. Убедитесь в этом — запустите программу и посмотрите в логи.

Вместо того, чтобы выразить наше недовольство в логах, мы могли бы запустить какой-нибудь процесс восстановления или что-то ещё.

Итоги

Во второй части нашего рассказа об Akka узнали о важных компонентах системы акторов. Мы узнали о тех возможностях, что предлагает нам Akka, для повышения отказоустойчивости системы.

Несмотря на то, что для нас осталось много неизвестного в Akka, нам всё же придётся закруглиться, поскольку дальнейшее изучение Akka выходит за рамки этого пособия. В следующей, заключительной, статье мы подведём итоги и я покажу вам много ссылок на полезные материалы для дальнейшего путешествия по миру Scala. Там будет кое-что интересное и для тех, кто всерьёз заинтересовался Akka.