Так уж получилось, что появилась необходимость досрочно останавливать уже запущенный сайдкик-воркер. И, как уже всем причастным известно, запущенную задачу невозможно остановить штатными средствами — этого просто не предусмотрено архитектурой. И когда сайдкик-задача начала уже выполняться, то ее уже ничто не остановит. Конечно же, в интернетах тут же нашлось решение с убиением руби-процесса и с отменой перезапуска оного, но это решение по очевидным причинам не может устраивать ни разработчиков приложения, ни разработчиков сайдкика.
В итоге решение появилось откуда не ждали и оказалось крайне простым и очевидным. В общем и целом идею можно сформулировать очень коротко: сайдкик-процесс должен сам себя убивать, а мы лишь должны сказать ему когда это сделать. Весь код, что раньше просто запускался в сайдкике, будем запускать в отдельном трэде внутри процесса сайдкика. И, параллельно ему запустим трэд, который будет следить за указаниями извне чтобы сайдкик процесс убил себя сам. Получив указания убить себя, следящий тред убивает соседа и убивается сам.
В общем и целом такой подход не ограничивается только лишь при использовании сайдкика и долгоработающая фигня может быть где угодно. Поэтому давайте абстрагируемся от сайдкика и попробуем обобщить наш подход.
Во-первых нужно решить каким образом мы будем сообщать треду-убийце новость, что нужно умереть. И в случае с руби лучше всего воспользоваться внешним механизмом передачи сообщений и редис в данном случае подойдет идеально. И лучше встроенный в редис механизм передачи сообщений не использовать по нескольким причинам главная из которых — отсутсвие отложенного чтения. Если по каким-либо причинам трэд-киллер пропустит сообщение, то наш процесс убийства может вообще не состоятся. Допустим, мы попросим сайдкик-процесс умереть очень быстро — сразу после того, как его запустим. Есть шанс, что трэд-киллер еще вообще не будет существовать.
Итак, процесс проверки сообщения достаточно простой и прямолинейный:
until $redis.del("sidekiq:killer:#{self.identificator}") == 1 do
sleep 0.1
end
main_thread.kill
А метод identificator
будет отвечать за уникальную составляющую ключа в редисе.
Основной же трэд с кодовым именем "жертва" после своей естественной смерти должен оставить убедится, что киллер не будет его ждать вечно:
begin
self.perform_without_thread(*args)
rescue
$redis.set("sidekiq:killer:#{self.identificator}", 1)
end
Опять же, как настоящий киллер, наш трэд-убийца должен быть уверен, что жертва мертва:
until !!main_thread.status == false do
sleep 0.1
end
Ну, и в конце концов наш общий сайдкик-процесс дожидается окончания работы всех двух процессов:
[watcher_thread, main_thread].each(&:join)
А теперь давайте соединим вышеописанный код вместе, чтобы получить целостный модуль для прямого добавления в существующие долгоиграющие процессы:
module SidekiqKiller
def perform_with_thread(*args)
main_thread = Thread.new do
begin
self.perform_without_thread(*args)
rescue
$redis.set("sidekiq:killer:#{self.identificator}", 1)
end
end
watcher_thread = Thread.new do
until $redis.del("sidekiq:killer:#{self.identificator}") == 1 do
sleep 0.1
end
main_thread.kill
until !!main_thread.status == false do
sleep 0.1
end
end
[watcher_thread, main_thread].each(&:join)
end
alias_method_chain :perform, :thread
end
В нашей системе механизм приказного самоубийства воркеров использовался для принудительной остановки воркера отдельно запущенного экземпляра стейджинг-сервера. Механизм, конечно, безотказный, но слишком дорогой по памяти. В итоге эту часть системы переписали на чистом руби, оптимизировав и избавившись от сайдкика и сопутствующих библиотек.
Какой из этого можно сделать вывод? Да никакого, кроме того, что все мы смертны. Еще, наверное то, что, не стоит так делать, если нет уж очень острой необходимости.