LE Blog

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

09.12.2009 firtree_right Забота о неблокировании потоков в руби

Введение

Сегодня будет блиц-молния, которой место, скорее в твиттере. Однако, мне необходимо развеять тучи, которые я сам же и нагнал. :)

Постановка задачи

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

Решение

Если мы имеем чуть больше контроля над участками кода на си, которые выполняются долго (обычно в цикле), то можно добавить спасительный код в тело цикла:

if (!rb_thread_alone()) rb_thread_schedule();

Это позволит каждый раз при очередном витке передавать управление соседним потокам, если они есть, и сделает приложение более отзывчивым.

То есть, если мы сделаем точную копию функции из той статьи и добавим наш спасительный код внутрь for, то в выводе такой программы:

# coding: utf-8
require "block_thread.so"

t1 = Thread.new do
  10.times { |i| puts i; sleep 0.1 }
end

t2 = Thread.new do
  puts "Блокируем"
  BlockThread.cycle
  puts "Разблокируем"
end

t3 = Thread.new do
  puts "Стараемся не блокировать"
  BlockThread.cycle_with_schedule
  puts "Закончили стараться"
end
t1.join
t2.join
t3.join

Будет картина гораздо приятнее:

Блокируем
0
Стараемся не блокировать
Разблокируем
1
2
3
4
5
Закончили стараться
6
7
8
9

Заметьте, кстати, что два лишних вывода между «Блокируем» и «Разблокируем» благодаря передаче управления соседнимпотокам между cycle и puts.

Понятно, что соседние потоки тормозятся. Но теперь не на всё время, а только на исполнение одного витка цикла.

Материалы для самостоятельного изучения

Полный код статьи на github

18.11.2009 firtree_right Сборка руби-билиотеки в заданной среде

Постановка задачи

Как разработчику gphoto4ruby мне приходится сталкиваться с особыми задачами. Связано это с тем, что этот gem является оболочкой поверх ещё одной библиотеки. И как у всякой более-менее развитой сторонней библиотеки, у libgphoto2 есть версия, распространяемая через системные репозитории и порты и есть, так сказать, последний писк моды (bleeding edge).

Отсюда вытекает необходимость:

  1. Иметь разные версии библиотеки не конфликтующие между собой, установленные не одной системе,
  2. Компилировать свою руби-библиотеку под любую из версий.

Установка двух gphoto2 :)

Проделаю весь путь с самого начала. Для пущей целостности. Для начала установка из системного репозитория:

sudo apt-get install libgphoto2-2-dev gphoto2
gphoto2 --version

Теперь можно скачать нужную версию и установить её отдельно. Поскольку я в основном использую две версии, то версию из исходников нужно установить в /opt. Предположим, что исходники libgphoto2 и gphoto2 скачаны:

tar zxvf libgphoto2-x.x.x.tar.gz
cd libgphoto2-x.x.x.tar.gz
./configure --prefix=/opt
make
sudo make install
tar zxvf gphoto2-x.x.x.tar.gz
cd gphoto2-x.x.x.tar.gz
./configure --prefix=/opt --with-libgphoto2=/opt
make
sudo make install
/opt/bin/gphoto2 --version

Теперь мы имеем две библиотеки и две утилиты командной строки, поставленные раздельно и правильно залинкованные. Каждая утилита командной строки знает, где искать свою библиотеку. Надо, чтобы это же умел и gem

Компиляция джема

Если скачать исходник библиотеки, то можно проделать руками то, что делает команда gem install. Для создания Makefile используется утилита mkmf, которая входит в ruby-dev и с которой работает файл extconf.rb. В моём случае последовательность действий установщика такая:

cd ext
ruby extconf.rb
make

Теперь в папке ext мы имеем скомпилированную библиотеку (.so или .bundle в зависимости от системы). Установщик потом копирует её в папку lib, но мы пока остановимся. Мы можем посмотреть, какие другие библиотеки использует эта:

ldd gphoto4ruby.so

По выводу этой команды видно, что используется библиотека установленная из центрального репозитория. Теперь попробуем скомпилировать под версию «по последней моде». Поскольку я написал в extconf.rb

dir_config("gphoto2")

То это означает, что пользователю будет доступен целый ряд опций, позволяющих сказать компилятору, где искать libgphoto2. Попробуем:

ruby extconf.rb --with-gphoto2-dir=/opt
make
ldd gphoto4ruby.so

Но что это? Вывод показывает нам, что библиотека привязалась опять к тому, что установлено из репозиториев, а не тому, что в /opt. То есть компилятор, конечно, находит нужные ему заголовки (*.h), но ничего в них не говорит о том, где искать соответствующие им библиотеки. Об этом ему должны сказать мы:

ruby extconf.rb --with-gphoto2-dir=/opt --with-dldflags="-Wl,-rpath,/opt/lib"
make
ldd gphoto4ruby.so

Вуаля!

Теперь, собственно, главное. Как это сделать при установке джема. Чтобы передать ключи для extconf нужно задать их после дополнительного «--»:

sudo gem i gphoto4ruby -- --with-gphoto2-dir=/opt --with-dldflags="-Wl,-rpath,/opt/lib"

Вот такой экскурс в жизнь разработчиков библиотек. Как это звучало в школьные времена: «Спэтсыално дла джэма».

Материалы для самостоятельного изучения

Руководство по расширению руби с помощью C (см. главу про extconf.rb)

11.11.2009 firtree_right Сравнения и неравенства в руби

Постановка задачи

Собрать в одном месте важные, на мой взгляд, особенности сравнений и неравенств в руби.

Основа неравенств в руби

Основным методом сравнения является <=>. Определив его, мы определяем все остальные операции, включив модуль Comparable:

class MyComp
  attr :value
  include Comparable
  def initialize(val)
    @value = val
  end

  def <=>(other)
    @value <=> other.value
  end
end

v1 = MyComp.new(1)
v2 = MyComp.new(2)

puts v1 < v2  # > true
puts v1 <= v2 # > true
puts v1 > v2  # > false
puts v1 >= v2 # > false
puts v1 == v2 # > false

Сам метод можно было бы описать как «возвращает -1, 0 или 1 в зависимости от того, меньше равен или больше объект, чей метод вызывается в сравнении с объектом переданным в качестве параметра». Но на самом деле, скорее, наоборот понятия «больше», «меньше» и «равен» определяются исходя из работы <=>.

Далее всё понятно и более ли менее очевидно для чисел, массивов и строк. Но есть и интересная особенность.

Сравнение модулей и классов

Сравнение для модулей и классов определено таким образом, что в результате мы знаем направление наследования или включение одного модуля другим:

module T1
end
module T2
  include T1
end
T3 = T1

class C1
end
class C2 < C1
end
C3 = C1

puts "T1 <=> T2: #{(T1 <=> T2).inspect}" # > 1
puts "T1 <=> T3: #{(T1 <=> T3).inspect}" # > 0
puts "C1 <=> C2: #{(C1 <=> C2).inspect}" # > 1
puts "C1 <=> C3: #{(C1 <=> C3).inspect}" # > 0
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > nil
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > nil

C3.send(:include, T1)

puts "после включения"
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > -1
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > 1

Наследник или модуль, который включает другой модуль, меньше, чем родитель или включаемый модуль. Это видно даже из синтаксиса наследования.

Равенство

Существует три метода равенства: ==, eql?, equal?. Последний из которых никогда не следует переопределять, т.к. он отвечает за идентичность. Первые же два обычно работают одинаково. Канонический пример различия из документации:

3 == 3.0   # > true
3.eql? 3.0 # > false

Что лишь свидетельствует о том, что == проводит конвертацию чисел перед сравнением. Обычно == соответствует случаю, когда <=> возвращает 0.

Сравнение case...when

Все мы знаем, что в case...when оператор сравнения — это ===. В большинстве случаев он эквивалентен равенству из предыдущего параграфа. Но если равенство симметрично

(a.==(b)) == (b.==(a))

И если это не так, то это можно считать ошибкой. То === вовсе не обязано таковым быть. Нужно помнить, что в конструкции case...when вызывается метод сравнения объекта, стоящего после when, а в качестве параметра ему передаётся объект, стоящий после case:

puts String === "строка" # > true
puts "строка" === String # > false
puts /ок/ === "строка"   # > true
puts "строка" === /ок/   # > false
puts (1..10) === 5       # > true
puts 5 === (1..10)       # > false

Материалы для самостоятельного изучения

  1. Полный код статьи на github
  2. Что нужно помнить, создавая свой объект руби

28.10.2009 firtree_right Удалённые вызовы через систему распределённых объектов в руби (dRuby)

Введение

Некоторое время назад я писал о создании подпроцессов на руби. В числе прочего один из вопросов был об общении между собой демона и родительского процесса. Об одном из методов пойдёт речь сегодня

Постановка задачи

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

Решение: DRb

Для удалённого обращения с объектами существует стандартная руби-библиотека dRuby, в которой находится модуль DRb, который мы и будем использовать. Ничего устанавливать не нужно. Согласно документации, совершенно прозрачным образом можно вызвать методы на удалённом объекте даже на другой машине. Объекты и ссылки на них передаются в формате Marshal.

Ну, довольно теории! Перейдём к практике. Для эмуляции параллельных процессов (возможно на разных машинах (!)) мы будем использовать два окна терминала. В одном запустим server.rb:

# coding: utf-8
$KCODE = "utf-8" if RUBY_VERSION < "1.9.0"
require "drb/drb"

class RemoteObject
  def remote_method_with_param(param)
    puts "вызван метод на сервере с параметром #{param.inspect}"
    case param.class.to_s
    when "String"
      puts "параметр типа строка"
      param.reverse!
    when "Array"
      puts "параметр типа массив"
      param.shift
    else
      puts "параметр оставшегося типа"
      param.do_smth
    end
  end
end

$SAFE = 1 # Запретить eval() и eval-оподобные вызовы

DRb.start_service("druby://localhost:45678", RemoteObject.new)
DRb.thread.join

Здесь мы используем банальный Thread#join, чтобы при необходимости просто прервать выполнение. Но те, кто читал предыдущую статью, знают, что в это время можно делать что угодно и следить за потоком dRuby отдельно.

В другом терминале запустим клиентский код client.rb:

# coding: utf-8
$KCODE = "utf-8" if RUBY_VERSION < "1.9.0"
require "drb/drb"

class MyString
  def initialize(str)
    @string = str
  end

  def do_smth
    @string.reverse!
  end

  def inspect
    "<#{@string}>"
  end
end

rem_o = DRbObject.new_with_uri("druby://localhost:45678")

["строка", ["котик", "пёсик", "слоник"], MyString.new("суперстрока")].each do |obj|
  puts "Вызов метода вернул: #{rem_o.remote_method_with_param(obj).inspect}"
  puts "Параметр после вызова: #{obj.inspect}"
end

Вывод в терминалы будет следующий (я использую вывод для версии руби 1.9.1, потому что он нормально переворачивает кириллическую строку без колдовства) для сервера:

вызван метод на сервере с параметром "строка"
параметр типа строка
вызван метод на сервере с параметром ["котик", "пёсик", "слоник"]
параметр типа массив
вызван метод на сервере с параметром #<DRb::DRbUnknown:0x00000001248910 @name="MyString", @buf="\x04\bo:\rMyString\x06:\f@stringI\"\e\xD1\x81\xD1\x83\xD0\xBF\xD0\xB5\xD1\x80\xD1\x81\xD1\x82\xD1\x80\xD0\xBE\xD0\xBA\xD0\xB0\x06:\rencoding\"\nUTF-8">
параметр оставшегося типа

Клиент же упадёт с ошибкой:

Вызов метода вернул: "акортс"
Параметр после вызова: "строка"
Вызов метода вернул: "котик"
Параметр после вызова: ["котик", "пёсик", "слоник"]
(druby://localhost:45678) server.rb:17:in `remote_method_with_param': undefined method `do_smth' for #<DRb::DRbUnknown:0x00000001248910> (NoMethodError)
     .....

Что, безусловно, прекрасно. Прекрасно, что упал не сервер. :) Понятно, что он не знает ничего про этот объект и не знает, как с ним обращаться.

Как видно из вывода, объекты передаются в виде копий. Нашим же третьим, самодельным объектом, мы можем исследовать две возможности: таки передавать копию объекта или передавать лишь ссылку на него, чтобы вызовы выполнялись на клиентской копии. Для первой возможности достаточно вынести определение класса в общедоступное для клиента и сервера место -- common.rb:

# coding: utf-8
$KCODE = "utf-8" if RUBY_VERSION < "1.9.0"
require "drb/drb"

REM_URI = "druby://localhost:45678"

class MyStringCopied
  def initialize(str)
    @string = str
  end

  def do_smth
    @string.reverse!
    self
  end

  def inspect
    "<<#{@string}>>"
  end
end

class MyStringSingle
  include DRb::DRbUndumped # это ключ :)
  def initialize(str)
    @string = str
  end

  def do_smth
    @string.reverse!
    self
  end

  def inspect
    "<#{@string}>"
  end
end

Добавим require "common.rb" в серверный код, а клиентский преобразится до такого:

# coding: utf-8
require "common"

rem_o = DRbObject.new_with_uri(REM_URI)

DRb.start_service # Это нужно для объекта, который не копируется при передаче

["строка",
  ["котик", "пёсик", "слоник"],
  MyStringCopied.new("суперстрока"),
  MyStringSingle.new("суперстрока без копий")].each do |obj|
  puts "Вызов метода вернул: #{rem_o.remote_method_with_param(obj).inspect}"
  puts "Параметр после вызова: #{obj.inspect}"
end

Как видно, мы сразу позаботились и о второй возможности, создав для неё ещё один класс. Секрет заключается во включении модуля DRb::DRbUndumped и старте ещё одного серверного процесса на клиенте (для вызовов методов объектов клиента удалённо) Клиентский вывод теперь выглядит так:

Вызов метода вернул: "акортс"
Параметр после вызова: "строка"
Вызов метода вернул: "котик"
Параметр после вызова: ["котик", "пёсик", "слоник"]
Вызов метода вернул: <<акортсрепус>>
Параметр после вызова: <<суперстрока>>
Вызов метода вернул: #<DRb::DRbObject:0x000000012588c8 @uri="druby://127.0.1.1:43998", @ref=9631244>
Параметр после вызова: <йипок зеб акортсрепус>

Если немножко почитать, и разобраться, какие объекты можно и нужно «маршализировать», а какие нельзя или не нужно, то получается вполне себе прекрасный инструмент. Который, повторюсь, входит в стандартную библиотеку и не требует никаких внешних зависимостей.

Материалы для самостоятельного изучения

  1. Полный код статьи на github
  2. Документация по DRb (rdoc)
  3. Документация по Marshal

22.10.2009 firtree_right Работа с потоками (Thread) в руби

Введение

Сначала я расскажу, почему на сегодняшний день я не очень много работаю с подпроцессами на базе Thread, предпочитая им Kernel.fork. А потом покажу простой способ следить за потоками при работе приложения.

На текущий момент, основная проблема потоков -- это «ненастоящее» распределение ресурсов. Все потоки руби на самом деле находятся в одном системном потоке, который по очереди передаёт им управление. Это влечёт за собой полтора следствия.

Зависание

Когда имеешь дело с внешним оборудованием, сторонними библиотеками и серийными портами, зависание потока может случиться на самом низком уровне. Это можно симулировать небольшой программой на си -- block_thread.c:

#include <ruby.h>

VALUE rb_mBlockThread;

/*
 * call-seq:
 *   BlockThread::cycle(interval=5)
 *
 * Блокирует текущий поток на <code>interval</code> секунд.
 *
 */

VALUE bt_cycle(int argc, VALUE *argv, VALUE self) {
  int i, max;
  max = 5;

  if (argc == 1) {
    max = FIX2INT(argv[0]);
  } else if (argc > 1) {
    rb_raise(rb_eArgError, "Неправильное количество аргументов (%d вместо 0 или 1)");
  }

  for (i=0; i<max; i++) {
    sleep(1);
  }
  return Qnil;
}

void Init_block_thread() {
  /*
   * Модуль содержит методы для демонстрации работы потока
   */
  rb_mBlockThread = rb_define_module("BlockThread");
  rb_define_module_function(rb_mBlockThread, "cycle", bt_cycle, -1);
}

Если вы никогда не расширяли руби с помощью си, поясню, что в этой программе мы создаём модуль BlockTread, в котором создаём метода класса cycle, который указанное число раз (по умолчанию 5) в цикле ждёт одну секунду. Напишем extconf.rb:

require "mkmf"
create_makefile("block_thread")

И программу на руби, в которой будут два потока, один из которых мы заблокируем на низком уровне block_threads.rb:

# coding: utf-8
require "block_thread.so"

t1 = Thread.new do
  10.times { |i| puts i; sleep 0.1 }
end

t2 = Thread.new do
  puts "Блокируем"
  BlockThread.cycle
  puts "Разблокируем"
end

t1.join
t2.join

Скомпилируем и запустим:

ruby extconf.rb
make
ruby block_threads.rb

И что же мы видим? Мы видим, как все потоки, включая основной, блокируются на пять секунд (или любое число секунд, которое мы укажем) И даже ctrl + c не в силах нам помочь. Помогает только ctrl + z и потом killall ...

В случае же с Kernel.fork, процессы действительно равномерно делят между собой ресурсы, и один подпроцесс не способен заблокировать всё.

Синхронизация

Я говорил про полторы проблемы. Об одной уже рассказал, а вторая известна давно -- попробуйте выполнить следующий код:

# coding: utf-8
$cnt = 0

t1 = Thread.new do
  100000.times { $cnt += 1 }
end

t2 = Thread.new do
  100000.times { $cnt += 1 }
end

t1.join
t2.join

puts "Without sync: #{$cnt}"

Если вы не используете руби 1.9, то вы получите неожиданный и каждый раз разный результат. Всё дело в том, что переключение между потоками происходит между элементарными операциями, а += состоит из трёх элементарных операций: достать значение, прибавить к нему число, записать значение. Чтобы этого не произошло, нужно либо использовать синхронизацию с помощью Mutex, либо руби 1.9. Ссылка на полный код для этой статьи в конце, т.к. я спешу перейти к более интересной части. :)

Слежение за потоками с помощью менеджера ThreadsWait

Совершенно недавно открыл для себя интересный способ следить за статусом пакетов в блокирующей и неблокирующей манере:

# coding: utf-8
require "thwait"

t1 = Thread.new do
  10.times { |i| puts "поток 1 тик #{i}"; sleep 0.5 }
end

t2 = Thread.new do
  10.times { |i| puts "поток 2 тик #{i}"; sleep 0.7 }
end

tw = ThreadsWait.new t1, t2

t3 = Thread.new do
  10.times { |i| puts "поток 3 тик #{i}"; sleep 0.3 }
end

run = true
tw.join_nowait t3

while run do
  begin
    # Неблокирующее ожидание
    puts "Закончил работу #{tw.next_wait(true).inspect }"
    run = false
  rescue ThreadsWait::ErrNoFinishedThread
    puts "Ожидаем окончания работы одного из потоков"
    sleep 0.5
  end
end

# Блокирующее ожидание
tw.all_waits do |t|
  puts "Закончил работу #{t.inspect}"
end

По-моему, весьма удобно, если вам нужно не просто ожидать окончания работы потоков, но ещё и делать что-то при этом.

Материалы для самостоятельного изучения

  1. Полный код статьи на github
  2. Документация ThreadsWait
  3. Толковая статья о многопотоковости и процессах в руби

07.10.2009 firtree_right Некоторые тонкости стыковки ruby и bash

Введение

Последнее время больше занимаюсь работой системного администратора нежели программиста. Прошлую неделю даже пропустил написание статьи. Это, конечно, не означает, что совсем нечего рассказать.

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

Условное выполнение

В bash кроме разделителей команд «&» и «;», существует ещё и условное выполнение списка команд с помощью «&&» и «||». Их работа зависит от кода, с которым произошёл выход. exit0:

#!/usr/bin/env ruby
puts "Выход с кодом 0"
exit 0

exit1:

#!/usr/bin/env ruby
puts "Выход с кодом 1"
exit 1

Теперь если запустить скрипты в следующем сочетании:

./exit0 && ./exit0 && ./exit1 && ./exit0

То вывод будет следующий:

Выход с кодом 0
Выход с кодом 0
Выход с кодом 1

А если запустить скрипты в следующем сочетании:

./exit1 || ./exit1 || ./exit0 || ./exit1

То вывод будет следующий:

Выход с кодом 1
Выход с кодом 1
Выход с кодом 0

То есть, «&&» выполняет следующую команду, если предыдущая вышла с кодом 0, а «||» выполняет следующую команду, если предыдущая вернула ненулевой код. Условно говоря, первый список выполняется пока всё срабатывает, а второй -- пока не срабатывает.

Перенаправление вывода с помощью pipeline

Тут всё просто. Если в bash команды разделены с помощью «|», то вывод первой команды будет перенаправлен на вход второй.

Простая демонстрация из трёх файлов. show:

#!/usr/bin/env ruby
$KCODE = "utf8"
$stdout.puts "stdin содержит: #{$stdin.read.inspect}"

out:

#!/usr/bin/env ruby
$stdout.puts "Привет из stdout!"

err:

#!/usr/bin/env ruby
$stderr.puts "[*stderr*] Привет из stderr!"
$stdout.puts "Привет из stdout!"

Думаю, не составит труда выяснить, что выводят запущенные отдельно out и err, но вот, что из этого можно сделать с помощью pipeline:

./out | ./show

выводит

stdin содержит: "Привет из stdout!\n"

Когда же есть обращение к другому каналу вывода, то

./err | ./show

выводит

[*stderr*] Привет из stderr!
stdin содержит: "Привет из stdout!\n"

Вывод stderr можно перенаправить в stdout:

./err 2>&1 | ./show

выводит

stdin содержит: "[*stderr*] Привет из stderr!\nПривет из stdout!\n"

Материалы для самостоятельного изучения

Документация по bash

23.09.2009 firtree_right Получение irb-консоли в среде приложения

Задача

Допустим, есть приложение, написанное на руби. У него есть основной процесс и есть тесты, которые так или иначе загружают объекты приложения.

Хочется получить доступ в среду приложения в виде irb-консоли, чтобы вручную взаимодействовать с объектами и изменять данные. По типу script/console в rails.

Решение

Как большинство подобных решений, необходимость возникает, когда замечаешь себя за повторением одного и того же набора действий множество раз:

irb
require ...
require ...
...

Раз уж мы захотели как в рельсах, то следует предположить, что окружение нашего приложения загружается одним файлом. Например, config/environment.rb. Это будет первым упрощением многократно повторённого процесса.

Теперь сам файл script/console:

#!/usr/bin/env ruby

APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), ".."))

libs = " -r irb/completion"
libs << " -r pp" # специально для консоли автоподставнока и pretty print
libs << %( -r "#{APP_ROOT}/config/environment.rb")

ENV["APP_ENV"] = "console" # пример того, как сообщить приложению, что оно в консоли

exec "irb #{libs} --simple=prompt"

Здесь стоит сделать два акцента:

  1. Для сообщения приложению, что оно запущено в консоли, мы использовали волшебный хэш ENV. К нему потом можно обратиться внутри config/environment.rb и сделать что-то по-другому.
  2. Для запуска консоли мы использовали Kernel.exec, который не просто выполняет системную команду, но и передаёт ей управление, заменяя текущий процесс.

Теперь, если сделать наш файл запускаемым, будет как в сказке:

chmod +x script/console

Я хочу, чтоб эта песня, эта песня не кончааалась

Если есть приятные библиотеки, которые хочется подключать при каждом запуске irb. А так же, если хочется сохранять историю команд консоли при выходе. То следует воспользоваться мощью ~/.irbrc. Создайте этот файл («~» -- это $HOME, на всякий случай) и напишите в него:

require 'pp' # pretty print
require 'irb/completion' # автоподстановка
require 'irb/ext/save-history' # сохранение истории

ARGV.concat [ "--readline", # не пробовал без readline
              "--prompt-mode", "simple" ] # --simple-prompt

IRB.conf[:SAVE_HISTORY] = 25 # сколько сохранять

IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.irb-history" # куда сохранять

Материалы для самостоятельного изучения

  1. Подробная документация irb;
  2. Занятный способ получить irb-консоль прямо в запущенном работающем приложении.

16.09.2009 firtree_right Обработка исключений в руби

Введение

Сегодняшняя запись — это первая запись по заявкам читателей. :) Я напоминаю, что если у вас включен JavaScript, и если вы читаете эту запись на сайте, а не в rss-читалке, то вдоль левой границы окна будет оранжевая кнопка. Можно написать своё предложение либо нажав на неё, либо непосредственно на странице проекта. За предложения можно так же голосовать.

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

Базовые блоки

Я не буду слишком подробно останавливаться на каждом составляющем, а просто приведу пример, сделав некоторые пояснения:

class RetryException < StandardError
  attr :can_retry
  def initialize(rtr = true)
    @can_retry = rtr
  end
end

def exception_handling
  begin # Если бы не было else, то возвращалось бы последнее значение begin
    yield if block_given? 
  rescue ZeroDivisionError # Если програму запускает не Чак Норрис
    "На ноль делить нельзя"
  rescue RetryException => re # Возврат перезапускает блок begin
    puts "Не получилось, но мы попробуем снова"
    if re.can_retry
      retry
    else
      "Теперь точно не получилось"
    end
  rescue # Здесь ловится всё остальное
    puts "Случилось непредвиденное: #{$!.inspect}"
    raise
  else # Если всё прошло без ошибок
    "Всё прошло без ошибок"
  ensure # Этот блок выполняется в любом случае
    puts "Процесс окончен, но эта часть ничего не возвращает"
    "В любом случае" # Этой строки мы нигде не увидим
  end
end

blocks = [] # Массив с блоками

blocks.push(lambda {})
blocks.push(lambda { 1/0 })
blocks.push(lambda do
  @retry ||= 3 # Пробуем ещё раз не более трёх раз
  @retry -= 1
  raise RetryException.new(@retry > 0), "Временная ошибка"
end)
blocks.push(lambda { raise "Неведомая ошибка" })

blocks.each do |block|
  puts "Возвратилось: #{exception_handling(&block) rescue "Ошибка!"}"
  puts "------------------------"
end

Результат выполнения программы выглядит следующим образом:

Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Всё прошло без ошибок
------------------------
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: На ноль делить нельзя
------------------------
Не получилось, но мы попробуем снова
Не получилось, но мы попробуем снова
Не получилось, но мы попробуем снова
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Теперь точно не получилось
------------------------
Случилось непредвиденное: #<RuntimeError: Неведомая ошибка>
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Ошибка!
------------------------

Что мы отсюда почерпнули:

  1. Блок else (необязательный) выполняется в случае удачно выполненного begin и возвращает значение вместо него.
  2. Выполняется первый из блоков rescue, который соответствует ошибке
  3. Метод raise без параметров выбрасывает текущее исключение: то, которое находится в глобальной переменной $!
  4. Чтобы создать свой класс исключений, наследовать нужно от StandardError или от его потомков, иначе не сработает rescue без указания класса (попробуйте сами проверить)

Обратите внимание, что когда мы знаем, что вызов может бросить в нас ошибкой, то ловим её ещё раз. Запись

a = some_call rescue "some value"

эквивалентна

a = begin
  some_call
rescue
  "some value"
end

Особенности работы с потоками

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

Во-первых, без специальных указаний, никто даже не узнает о том, что в каком-то из потоков произошла ошибка:

begin
  Thread.new do
    sleep 1
    puts  "Первый поток"
  end

  Thread.new do
    sleep 3
    puts "Последний поток"
  end

  Thread.new do
    sleep 2
    raise "Ошибка внутри потока"
  end
rescue
  puts "Спасены!"
end

puts "Поехали!"

sleep 7

puts "Как-то слишком тихо"

Производит вывод:

Поехали!
Первый поток
Последний поток
Как-то слишком тихо

Первый способ засечь исключения -- это вызвать join на потоке. Тогда исключение будет переброшено в сам join. Но этот метод блокирует программу, пока поток не завершится. Что конечно же не очень удобно. Наше «поехали» перемещается ближе к концу:

begin
  Thread.new do
    sleep 1
    puts  "Первый поток"
  end

  Thread.new do
    sleep 3
    puts "Последний поток"
  end

  Thread.new do
    sleep 2
    raise "Ошибка внутри потока"
  end.join
rescue Exception => e
  puts "Спасены!"
  puts e.backtrace.join("\n")
end

puts "Поехали!"

sleep 7

Производит вывод:

Первый поток
Спасены!
thr.rb:16
thr.rb:14:in `join'
thr.rb:14
Поехали!
Последний поток

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

Thread.abort_on_exception = true

begin
  Thread.new do
    sleep 1
    puts  "Первый поток"
  end

  Thread.new do
    sleep 3
    puts "Последний поток"
  end

  Thread.new do
    sleep 2
    raise "Ошибка внутри потока"
  end
rescue Exception => e
  puts "Спасены!"
end

puts "Поехали!"

sleep 7

Тогда наша программа оборвётся в момент ошибки и выведет:

Поехали!
Первый поток
thr.rb:16: Ошибка внутри потока (RuntimeError)
        from thr.rb:14:in `initialize'
        from thr.rb:14:in `new'
        from thr.rb:14

И никакие rescue нас не спасут.

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

  1. Вопросы-задачи-загадки, на которые полезно ответить не пиша программы
  2. Иерархия классов исключений в руби
  3. Более подробно про азы, в которые я не стал здесь спускаться

10.09.2009 firtree_right Создание init-скриптов с помощью руби

Предисловие

Сегодня у меня кроме всего прочего появился повод похвастаться! :) Когда вы смотрите широко обсуждаемые Яндекс-Панорамы, знайте, что их производством в составе моей любимой компании neq4 занимался и я.

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

Так же я, безусловно, планирую раскрывать некоторые секреты. Кстати, практически все записи в этом блоге основаны на работе над этим проектом. Например, программа, которая управляет камерами во время движения, а так же получает данные от приборов, была так же написана мной. И теперь вы знаете, что написана она на руби. :) За исключением библиотеки для работы с камерами, о которой я уже писал.

Это и подводит нас вплотную к теме сегодняшней записи.

Введение

Безусловно, руби — лучший из языков. Для меня выбор в его пользу обусловлен прежде всего высокой скоростью разработки. Поэтому при возникновении какой-то задачи, особенно если её нужно сделать быстро, я пробую решить её на руби. В прошлый раз я рассказал об автоподстановке. А сегодня — создание init-скриптов

Скрипты эти располагаются в папке /etc/init.d и запускаются при старте в определённых условиях. Они должны принимать параметры start, stop, restart и status. Как правило, они служат для запуска и остановки служб.

Решение

Написать программу, которая принимает текстовый ключ на входе, не составляет труда (/etc/init.d/my_script):

#!/usr/bin/env ruby

APP_NAME = "my_script"

case ARGV.first
  when "status"
    if...
      ...
      exit 0
    elsif ...
      ...
      exit 1
    else
      exit ...
    end
  when "start"
    ...
  when "stop"
    ...
  when "restart"
    ...
  else
    puts "Usage: #{APP_NAME} {start|stop|restart|status}"
end

Круто, но до настоящего init-скрипта не дотягивает. Когда система проходит свой цикл жизни от запуска до перезагрузки или выключения, она проходит через шесть стадий. Нужно указать, в каких стадиях наш скрипт следует запустить с параметром start, а когда — с параметром stop. Для этого используется спецификация LSB (Linux Standard Base). Вот так:

#!/usr/bin/env ruby
# Start/stop my daemon
#
### BEGIN INIT INFO
# Provides:          my_script
# Required-Start:    $all
# Required-Stop:     $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Initializes my daemon at boot time
# Description:       In a Galaxy far, far away...
### END INIT INFO

APP_NAME = "my_script"

case ARGV.first
.....

Теперь лежащий в /etc/init.d скрипт нужно подрегулировать и зарегистрировать:

sudo chown root:root /etc/init.d/my_script
sudo chmod 755 /etc/init.d/my_script
sudo update-rc.d my_script defaults

Таким образом, вовсе необязательно изучать bash-скрипт. Вот, кстати, оболочка-альтернатива всяким башам, использующая синтаксис руби. Для таких, как я. :)

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

Спецификация LSB в части запуска и остановки скриптов

02.09.2009 firtree_right Автоподстановка задач rake в терминале с помощью ruby

Введение

Периодически натыкаюсь на хвалебные отзывы о zsh и о совершенной необходимости перехода с bash на него всему прогрессивному человечеству. В качестве демонстрации удобства демонстрируют лёгкость создания скриптов автоподстановки, например для задач rake. Неужели из-за этого следует сменить оболочку терминала?

Задача

Найти решение для автоподстановки задач rake с помощью bash несложно. Для этого используется complete. Огромное число примеров можно посмотреть у себя же в системе в _/etc/bashcompletion.

При ближайшем рассмотрении выяснилось, что complete может использовать не только функцию, но и команду. То есть отдельный запускаемый скрипт, который может быть написан и на руби в том числе.

Можно ли из этого извлечь пользу?

Решение

Итак, чтобы подключить наш скрипт, нужно добавить в файл ~/.bashrc такую строчку.

complete -C ~/.bash/autocomplete/rake_complete -o default rake

Для начала положим по адресу _~/.bash/autocomplete/rakecomplete элементарную реализацию автоподстановки, которая будет запускать rake и фильтровать результат.

Следует знать два момента:

  1. Введенная строка, после которой была нажата табуляция, находится в ENV["COMP_LINE"].
  2. В задачах могут попасться пространства имён. Поэтому из окончательного результата нужно убрать часть введенной строки, содержащей двоеточие.

Элементарная реализация выглядит следующим образом:

#!/usr/bin/env ruby

class RakeComplete
  def initialize(cmd)
    @command = cmd[ /\s(.+)$/, 1 ] || ""
  end

  def search
    `rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }.select{ |cmd| cmd[0, @command.length] == @command }.map{ |cmd| cmd.gsub(cmd_before_column, "") }
  end
private
  def cmd_before_column
    @command[ /.+\:/ ] || ""
  end
end

puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0

Вроде бы всё понятно: из результата вывода rake -T , отбросив паразитную первую строчку, вытаскиваем только названия задач, подбираем те, начало который совпадает с введенным названием задачи, выводим массив, предварительно удалив у элементов введённую до двоеточия часть. Но уж больно медленно работает. Конечно же, встает вопрос о кешировании.

Кеширование

Приведу сразу результат:

#!/usr/bin/env ruby

class RakeComplete
  CACHE_NAME = ".rake_complete~"

  def initialize(cmd)
    @command = cmd[ /\s(.+)$/, 1 ] || ""
  end

  def search
    exit 0 if rake_file.nil?
    selected_tasks.map do |cmd|
      cmd.gsub(cmd_before_column, "")
    end
  end

private

  def cmd_before_column
    @command[ /.+\:/ ] || ""
  end

  def rake_file
    ["Rakefile", "Rakefile.rb", "rakefile", "rakefile.rb"].detect do |name|
      File.file?(File.join(Dir.pwd, name))
    end
  end

  def cache_file
    File.join(Dir.pwd, CACHE_NAME)
  end

  def generate_tasks
    tasks = `rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }
    File.open(cache_file, "w") do |f|
      tasks.each do |task|
        f.puts task
      end
    end
    tasks
  end

  def cached_tasks
    File.read(cache_file).split("\n")
  end

  def cache_valid?
    File.exist?(cache_file) && (File.mtime(cache_file) >= File.mtime(rake_file))
  end

  def tasks
    cache_valid? ? cached_tasks : generate_tasks
  end

  def selected_tasks
    tasks.select do |cmd|
      cmd[0, @command.length] == @command
    end
  end
end

puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0

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

Думаю, теперь не составит труда написать подобное для задач capistrano.