Состояние актора — единая совокупность особенностей работы актора, которые следует восстановить при перезапуске актора, чтобы он продолжил свою работу так, словно ничего не произошло.
Работа акторов может длиться миллисекундами, минутами, днями, годами. Волей-неволей о состоянии актора приходится задумываться, когда встаёт вопрос о том, что произойдёт с ним после останова (в т.ч. аварийного) и последующего нового запуска. Иногда перезапуск актора даже теоретически ни к чему плохому привести не может. Это, скорее всего, какие-то ресурсные акторы, раздающие, например, файлы или данные с датчиков. Но иногда последствия перезапуска могут быть очень неприятными. Например, когда актор владеет каким-то важным токеном, пропажу которого нелегко восполнить.
В основном состояние актора сводится к тому, что важного хранится в оперативной памяти. Однако всегда надо рассматривать проблему в комплексе, обращая внимание на мелочи. Рассмотрим пример — актор, созданный на основе gen_statem. Там от вызова к вызову передаётся специальный терм со сквозными данными, которые используются в разных колбеках:
state_null({call,From}, Mess, Data) ->
...
state_12(cast, check, Data) ->
Data — это и есть тот самый терм. В простом варианте это может быть, например, счётчик всех происшедших событий. Колбек может изменить этот терм, и в следующий вызов будет передан уже обновлённый терм. Этот терм зарождается в функции init, передаётся от одного состояния машины состояний к другому (в нашем примере state_null, state_12 и т.д.) и заканчивает своё существование в колбеке terminate, где его тоже можно должным образом обработать.
Является ли этот терм состоянием актора? На первый взгляд может показаться, что да, ведь именно в нём сосредоточена вся важная сквозная информация. Но стоит задуматься: если мы при перезапуске восстановим этот терм, будет ли полностью восстановлена работа актора? Ответ: нет.
Машина состояний всегда пребывает в одном из своих состояний (в нашем примере это state_null, state_12 и т.д.), и это состояние машины никак не отражено в терме Data. Поэтому если работу актора мы начинаем так:
init(_) ->
{ok, state_null, #{}}.
То первым состоянием всегда будет state_null. И это полбеды, что с этого всегда будет начинать работу актор. Главное — актор не знает, на чём он остановился в прошлый раз. И это в любом случае уже не будет полноценное восстановление работы. Вывод: для полноценного восстановления работы актора надо записать на диск, а потом прочитать и текущее состояние машины состояний тоже.
Далее. Мы поняли, что надо сохранить терм Data, надо сохранить состояние машины состояний. Этого достаточно для сохранения состояния актора? Ответ: опять нет.
Есть ещё таймауты. При переходе из состояния в состояние актор на основе gen_statem может программировать сам себя: “Через такое-то время я получу такое-то сообщение”. Если мы не сохраним, а потом не восстановим текущий таймаут, один или несколько, может быть нарушена логика работы актора и всего приложения.
Такой пример. Допустим, по какому-то внешнему событию запускается таймер. Скажем, надо через пять минут послать уведомление пользователю. Проходит две минуты, мы перезапускаем Систему, таймер пропадает, пользователь не получает очень важное для него уведомление.
Помимо таймаутов (которых, кстати, в gen_statem три разных типа) есть ещё отложенные события: когда текущее состояние машины состояний не желает обрабатывать полученное сообщение, оно может вызвать процедуру перехода postpone, чтобы новое состояние получило снова это же сообщение. Если мы, однако, на этом месте прервём работу актора, это сообщение так и останется необработанным.
К сожалению, насколько я знаю, в Erlang/OTP не предусмотрены штатные инструменты для сохранения состояния актора, построенного на основе gen_statem или иных gen-поведений. Более того, как-то прямо узнать текущее состояние таймаута, одного или нескольких, невозможно. Придётся как-то выкручиваться.
Когда все данные, нужные для сохранения состояния актора, собраны, мы их сохраняем на диске. Можно все эти данные собрать в один терм, сделать из него специальный бинарник с помощью term_to_binary и затем сохранить этот бинарник с помощью file:write_file в файл. Делать это правильно в колбеке terminate. Восстанавливать — в колбеке init. Делаем наоборот: сначала с помощью file:read_file получаем бинарник, а затем его превращаем в терм с помощью binary_to_term.
Из других акторов (в т.ч. оболочки) узнать состояние данного актора можно с помощью sys:get_state(Name). Для актора, построенного на основе gen_statem, эта функция выдаст кортеж, в котором первый элемент будет состояние машины состояний, а второй — сквозной терм Data. Опять же, как мы уже выяснили, это будет информативное, но не полное состояние актора.
Copyright © 2025 Алексей Карманов