LE Blog

Инженер с поэтической душой

10.06.2009 firtree_right Ruby daemon, или как сделать демона на руби

Задача

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

Базовый механизм

Запустить демона можно с помощью метода Kernel.fork, передав ему блок. Метод возвращает pid процесса, который можно записать в файл и просто использовать в дальнейшем.

pid = fork do
  puts "from daemon"
  exit!(1)
end

Определение статуса

Я не нашёл удобного способа определения методами руби, запущен ли процесс. Есть возможность фильтровать вывод ps ax, ища в нём pid процесса. Но есть метод Process.waitpid, который можно использовать хитрым образом. Так же для будущих задач, упакуем наш код в класс:

require 'timeout'

class Daemon
  class << self
    def start
      @pid = fork do
        puts "from daemon"
        sleep 1
        exit!(1)
      end
    end

    def running?
      if @pid
        begin
          Timeout::timeout(0.01) do
            Process.waitpid(@pid)
            if $?.exited?
              return false
            end
          end
        rescue Timeout::Error
        end
        return true
      else
        return false
      end
    end
  end
end

Daemon.start

puts "running: #{Daemon.running?}"
sleep 1.5

puts "running: #{Daemon.running?}"

Сообщение об ошибках

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

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

require 'timeout'

class Daemon
  def initialize
    puts "from daemon: initializing"
  end

  class << self
    def start
      @rd, @wr = IO.pipe
      @pid = fork do
        @rd.close
        begin
          dmn = new
          @wr.write "ok"
          @wr.close
          sleep 1
        rescue Exception => e
          @wr.write e.to_s
          @wr.close
        ensure
          exit!(1)
        end
      end
      @wr.close
      str = @rd.read
      if str == "ok"
        puts "daemon started ok"
      else
        puts "error while initializing daemon: #{str}"
      end
      @rd.close
    end

    def running?
      if @pid
        begin
          Timeout::timeout(0.01) do
            Process.waitpid(@pid)
            if $?.exited?
              return false
            end
          end
        rescue Timeout::Error
        end
        return true
      else
        return false
      end
    end
  end
end

Daemon.start

puts "running: #{Daemon.running?}"
sleep 1.5

puts "running: #{Daemon.running?}"

Остановка

У каждого демона (для того они обычно и создаются) есть циклическая часть. Нам хотелось бы запускать и останавливать процесс тогда, когда нам нужно. Если с запуском всё понятно, то для остановки потребуется ещё одна вещь. Ведь в тот момент, когда мы создали демона, он создает копии всех переменных и дальнейшее их изменение внутри и снаружи демона становится независимым. Ещё один способ общения с демоном — сигнал:

require 'timeout'

class Daemon
  def initialize
    puts "from daemon: initializing"
    @cnt = 0
  end

  def main_loop
    @cnt += 1
    puts "from daemon: running loop ##{@cnt}"
    sleep 0.1
  end

  class << self
    def start
      @rd, @wr = IO.pipe
      @pid = fork do
        @rd.close
        running = true
        Signal.trap("TERM") do
          running = false
        end
        begin
          dmn = new
          @wr.write "ok"
          @wr.close
          while running
            dmn.main_loop
          end
        rescue Exception => e
          @wr.write e.to_s
          @wr.close
        ensure
          exit!(1)
        end
      end
      @wr.close
      str = @rd.read
      if str == "ok"
        puts "daemon started ok"
      else
        puts "error while initializing daemon: #{str}"
      end
      @rd.close
    end

    def stop
      unless @pid.nil?
        Process.kill("TERM", @pid)
        @pid = nil
      end
    end

    def running?
      if @pid
        begin
          Timeout::timeout(0.01) do
            Process.waitpid(@pid)
            if $?.exited?
              return false
            end
          end
        rescue Timeout::Error
        end
        return true
      else
        return false
      end
    end
  end
end

Daemon.start

puts "running: #{Daemon.running?}"
sleep 1
Daemon.stop
puts "running: #{Daemon.running?}"

Теперь, если наследовать от этого класса свой класс и переопределить методы initialize и main_loop, получится вполне себе демон :)

Для самостоятельного изучения

Есть, конечно, ещё недостатки. Например:

  1. Если ошибка возникает в main_loop, то канал вывода уже закрыт. А если не закрывать канал, то метод IO#read не позволит нам выйти из метода start. Что делать?
  2. Если нужно периодически общаться с демоном общирными объемами информации, что делать? (я в своей задаче использовал TCPSocket, но это, ведь, не панацея)
  3. Хорошо бы хранить pid в pid-файле на случай нашествия зомби. И, соответственно, обрабатывать возникающие зомбо-проблемы.

Но это я оставлю на самостоятельное решение пытливым читателям.