Сайдкик-самоубийца

в 21:11, , рубрики: ruby, ruby on rails, rubyonrails, sidekiq, метки:

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

image

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

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

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

Итак, процесс проверки сообщения достаточно простой и прямолинейный:

  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

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

Какой из этого можно сделать вывод? Да никакого, кроме того, что все мы смертны. Еще, наверное то, что, не стоит так делать, если нет уж очень острой необходимости.

image

Автор: aratak

Источник

Поделиться новостью

* - обязательные к заполнению поля