Тем не менее, при неразумном использовании субъекты могут принести еще больше боли и шаблонов, чем параллельный параллелизм или STM . Отсутствие углубленной документации по моделированию на основе акторов приводит к неправильным представлениям и мифам, которые могут быть разрушены при использовании лучших практик и руководящих принципов проектирования для построения отказоустойчивых систем в Erlang.
Защитное программирование заставляет вас прилагать огромные усилия для защиты системы от всевозможных неожиданных действий, о которых вы можете подумать, проверяя входные данные и текущее состояние, когда это возможно, и определяя способы эскалирования проблемы на более высоком уровне. Будучи программистом-защитником, вы всегда должны проявлять инициативу и искать новые потенциальные проблемы, которые нужно решить, потому что даже самая маленькая из них, приводящая к сбою в незначительном компоненте, может привести к огромному сбою системы.
Отказоустойчивые системы в их применении не склонны предсказывать все возможные причины сбоев (чтобы предотвратить сбой одного компонента), а скорее изолируют компонент от остальной части системы и перезагружают его, поддерживая согласованную работу системы. Если перезапуск компонента не помогает, проблема может быть перенесена на более высокий уровень, чтобы [возможно] перезапустить стороны, которые обмениваются сообщениями с компонентом.
Как говорит Джо Армстронг в своей книге « Программирование на Эрланге », когда один актер умирает в комнате, полной актеров, другие, вероятно, должны это заметить и начать решать проблему (очищать тела).
В Erlang и Scala такое поведение достигается путем объединения актеров. В самой основной форме, когда два актера связаны между собой, и один из них умирает, он посылает сигнал выхода другому актеру, чтобы завершить его тоже.
Связывание актеров является двунаправленной операцией, поэтому, когда вы связываете актера A с B, B находится на заднем плане, связанном с A, и смерть любого из них вызывает отправку сигнала выхода другому. В Erlang возможно создание однонаправленной связи с мониторами (когда отслеживаемый процесс умирает, сообщение «ВНИЗ» отправляется обработчику). Аналогов для мониторов в стандартной библиотеке Scala нет, однако реализовать их по запросу будет легко.
Связанный актер может создать ловушку выхода, так что сигнал выхода будет обрабатываться как обычное сообщение, не вызывая прекращение действия актера. Сигнал выхода обычно содержит ссылку на неисправного субъекта (который можно использовать для его перезапуска) и причину сбоя.
В «Программировании Erlang» несколько сценариев выхода из ссылки / ловушки представлены на простом примере:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
-module(edemo 1 ). -export([start 2 ]). start(Bool, M) -> A = spawn(fun() -> a() end), B = spawn(fun() -> b(A, Bool) end), C = spawn(fun() -> c(B, M) end), sleep( 1000 ), status(b, B), status(c, C). |
В приведенном выше коде создаются три процесса, синхронизированные со сном, чтобы дать им время для обработки пропущенных сообщений (в общем, уродливо, но работает на простом примере), а затем проверяется их состояние. Актеры определены следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
a() -> process _ flag(trap _ exit, true ), wait(a). b(A, Bool) -> process _ flag(trap _ exit, Bool), link(A), wait(b). c(B, M) -> link(B), case M of {die, Reason} -> exit(Reason); {divide, N} -> 1 N, wait(c); normal -> true end. |
Процесс A всегда прерывает выходы, процесс B связан с A и прерывает выходы в зависимости от входа функции, процесс C, связанный с B, получает сообщения и либо выполняет вычисления, либо завершается с ошибкой.
1
2
3
4
5
6
|
wait(Prog) -> receive Any -> io : format( 'Process ~p received ~p~n' ,[Prog, Any]), wait(Prog) end. |
Метод wait рекурсивно получает сообщения, распечатывающие содержимое сообщения.
При переводе на Scala с использованием стандартной библиотеки Actors пример выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
object LinkedActors { case class Die(reason : AnyRef) case class Divide(number : Int) def status(name : String, actor : Actor) = println( 'Actor %s is %s' format(name, actor.getState)) def printMessage(actorName : String) : PartialFunction[Any, Unit] = { case msg = > println( 'Actor %s received %s' format(actorName, msg))} def start(isTrapExit : Boolean, message : Any) = { val A = actor{ self.trapExit = true loop(react(printMessage( 'a' ))) } val B = actor{ self.trapExit = isTrapExit self link A loop(react(printMessage( 'b' ))) } val C = actor{ self link B loop{ react{ case Die(reason) = > exit(reason) case Divide(number) = > 1 number } } } C! message Thread.sleep( 1000 ) status( 'b' , B) status( 'c' , C) } } |
По сути, код такой же, с той разницей, что сообщения, принятые субъектом C, классифицируются с классами случаев, а поведение получателей акторов B и C представляется частичными функциями.
Давайте передадим некоторые входные данные методу start, чтобы увидеть, как поведут себя цепочечные актеры, когда некоторые из них умрут:
1
2
3
4
|
scala> start( false , Die( 'abc' )) Actor a received Exit(scala.actors.Actor$$anon$ 1 @ dc 8 f 6 d,abc) Actor b is Terminated Actor c is Terminated |
Актер C получает сообщение Die и существует с причиной «abc». Актер B, связанный с C, не перехватывает выходы, поэтому он также завершается. Пока A, подключенный к ловушкам B, завершается, когда субъект B завершает свою работу, он отправляет A сообщение с причиной сбоя (класс дел со следующей сигнатурой):
1
2
3
4
5
6
7
8
|
** An `Exit` message (an instance of this class ) is sent to an actor * with `trapExit` set to ` true ` whenever one of its linked actors * terminates. * * @ param from the actor that terminated * @ param reason the reason that caused the actor to terminate * case class Exit(from : AbstractActor, reason : AnyRef) |
В то же время, когда ожидается выход (не вызванный вычислительным исключением), связанные субъекты не затрагиваются:
1
2
3
|
scala> start( false , Die('normal)) Actor b is Suspended Actor c is Terminated |
В приведенном ниже фрагменте необработанное деление на нулевое исключение приводит к смерти C и B:
1
2
3
4
5
6
|
scala> start( false , Divide( 0 )) Actor a received Exit(scala.actors.Actor$$anon$ 1 @ 113 eb 9 c,UncaughtException (scala.actors.Actor$$anon$ 1 @ 1 a 1446 d,Some(Divide( 0 )),Some(scala.act ors.ActorProxy @ 14 f 83 d 1 ),java.lang.ArithmeticException : by zero)) Actor b is Terminated Actor c is Terminated |
Если мы заставим B перехватить выход, актер останется в живых во всех сценариях, описанных выше:
1
2
3
4
|
scala> start( true , Die( 'abc' )) Actor b received Exit(scala.actors.Actor$$anon$ 1 @ 13 e 49 a 8 ,abc) Actor b is Suspended Actor c is Terminated |
По сравнению с первым фрагментом, теперь B получает сообщение о выходе от C.
Необработанные ошибки также не распространяются на A:
1
2
3
4
5
6
|
scala> start( true , Divide( 0 )) Actor b received Exit(scala.actors.Actor$$anon$ 1 @ 119 f 779 ,UncaughtException (scala.actors.Actor$$anon$ 1 @ 119 f 779 ,Some(Divide( 0 )),Some(scala.act ors.ActorProxy @ 14 f 83 d 1 ),java.lang.ArithmeticException : by zero)) Actor b is Suspended Actor c is Terminated |
Выходное сообщение содержит ссылку на неисправного субъекта, который можно использовать для его перезапуска. Можно реализовать очень простой супервизор актера по аналогии с поведением супервизора в Erlang.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
case class ChildSpecification(worker : Worker, restartBehavior : Child RestartBehavior.Value = permanent) case class OneForOne(maxRestarts : Long = 3 , maxTime : Long = 3000 ) extends RestartStrategy case class OneForAll(maxRestarts : Long = 3 , maxTime : Long = 3000 ) extends RestartStrategy class Supervisor(supervisorId : String, strategy : RestartStrategy, childSpecs : List[ChildSpecification]) extends Worker { ... override def act = { self.trapExit = true linkWorkers loop { react{ case Exit(worker : Worker, reason) = > println( 'Worker [%s] has failed due to [%s]' format(worker.id, reason)) if (worker.restartsInPeriod(strategy.maxTime) > = strategy .maxRestarts) exit( 'Maximum restart intensity for %s is reached!' format(worker.id)) strategy match { case str : OneForOne = > restartWorker(worker) case str : OneForAll = > childSpecs.foreach{spec = > restartWorker(spec.worker)} } case Terminate(reason) = > println( 'Supervisor terminated with reason [%s]' format(reason)) exit(reason) } } } ... } |
Сам по себе Supervisor — это обычный Scala Actor, который отлавливает сообщения от других связанных с ним субъектов (рабочих, с точки зрения терминологии надзора) и перезапускает либо только одного отказавшего участника, либо всех контролируемых субъектов. Когда частота перезапуска достигает предела, заданного стратегией перезапуска, супервизор завершает работу, так что супервизор на более высокой иерархической позиции может попытаться решить проблему.
В простейшем сценарии супервизор перезапускает актера, прерванного из-за необработанного исключения:
1
2
3
4
5
6
7
|
'Actor terminated due to uncaught exception is restarted by the supervisor' in { val worker = new SimpleWorker( 'simple_worker' ) Supervisor( 'basic_supervisor' , OneForOne(), List(ChildSpecification(worker))).start worker !? ( 1000 , Divide( 0 )) (worker !? ( 1000 , Divide( 1 ))).asInstanceOf[Option[Int]] must be equalTo Some( 1 ) } |
Вывод спецификации:
1
2
3
4
5
6
|
Starting worker simple _ worker Worker [simple _ worker] has failed due to [UncaughtException(com.vasilrem.linked. SupervisorSpec$SimpleWorker @ fd 54 ec,Some(Divide( 0 )),Some(scal a.actors.Channel @ 16167 ab),java.lang.ArithmeticException : by zero)] Restarting worker [simple _ worker]... [info] + Actor terminated due to uncaught exception is restarted by the supervisor |
В более сложном сценарии, когда супервизоры связаны в дереве, высокоуровневый супервизор перезапускает низкоуровневый супервизор, когда он умирает, что вызывает перезапуск связанных с ним рабочих:
01
02
03
04
05
06
07
08
09
10
11
|
'High-level supervisor restarts low-level supervisor and the wrokers linked to it' in{ val worker = new SimpleWorker( 'simple_worker' ) val lowLevel = Supervisor( 'lowlevel_supervisor' , OneForOne(), List(ChildSpecification(worker))) val highLevel = Supervisor( 'lowlevel_supervisor' , OneForOne(), List(ChildSpecification(lowLevel))).start worker.getState must not be equalTo(State.Terminated) lowLevel ! Terminate( 'Kill lowlevel' ) Thread.sleep( 1000 ) worker.getState must not be equalTo(State.Terminated) } |
Результат теста следующий:
1
2
3
4
5
6
7
|
Starting worker lowlevel _ supervisor Starting worker simple _ worker Supervisor terminated with reason [Kill lowlevel] Worker [lowlevel _ supervisor] has failed due to [Kill lowlevel] Restarting worker [lowlevel _ supervisor]... Starting worker simple _ worker [info] + High-level supervisor restart low-level supervisor |
Вы можете найти больше спецификаций и кода руководителя здесь .
Приятного кодирования и не забудьте поделиться!
Ссылка: отказоустойчивые примитивы в Scala: ссылки и ловушки от нашего партнера JCG Василя Ременюка в блоге Василя Ременюка .