17 июня 2010, 11:36
Темы: git, syntax, automation

Введение
Моя любимая система контроля версий имеет огромное количество инструментов. Как-то раз я участвовал в опросе, после которого выяснилось, что даже из самых популярных инструментов я использую от силы 10%.
Но иногда возникают ситуации, единственно продуктивным выходом из которых бывает изучение и использование нового для себя инструмента. О двух таких случаях я сегодня и расскажу.
Внезапные просьбы: git stash
Бывает так, что пока я работаю над нововведениями в программу, текущая стабильная её версия активно используется. При активном использовании, конечно же, могут возникнуть ошибки или пожелания что-то изменить. Бывает так, что при этом я нахожусь в середине тестирования какого-то новшества, и всё настолько сыро, что я даже не могу сделать коммит.
Итак, я нахожусь в середине правок на ветке extremely_experimental, а мне необходимо внести правки в ветку master. Вот, как это делается:
git stash save
git checkout master
После первой команды всё наши изменения, которые нельзя было закоммитить, сохранены и текущая ветка приведена в состояние до правок. После этого мы можем сменить ветку и внести наши правки. После того, как ошибки исправлены, нововведения сделаны и тесты проходят, мы можем вернуться обратно к нашим правкам.
Но скорее всего все или некоторые из сделанных изменений понадобятся нам в нашей экспериментальной ветке. После перехода на неё:
git checkout extremely_experimental
Если нам нужны все изменения, то:
git merge master
Если только некоторые, то:
git cherry-pick ...
После этого вернём наши правки:
git stash pop
Если возникли конфликты, то правим их и делаем:
git stash drop
git reset --mixed
Последнее нужно для того, чтобы вынести наши правки из индекса, т.к. при конфликте они не выходят оттуда самостоятельно.
Конечно же таких незавершённых правок может быть несколько, но это я оставлю на самостоятельное изучение пытливому читателю.
Неизвестно, когда сломалось: git bisect
Бывает так, что вдруг обнаруживается ошибка, про которую точно известно, что давным давно её не было. Так бывает в больших проектах, в непокрытых тестами областях. Бывает так, что обнаружить, в чём же дело, быстро не удаётся.
Хорошая новость в том, что это и не обязательно. Нужно просто начать процесс:
git bisect start
git bisect bad
Так мы обозначили, что текущий коммит содержит ошибку. После этого, либо мы знаем, как называется коммит, в котором ошибки ещё не было, или находим его.
git bisect good v2.3.1
или
git checkout ...
git bisect good
После этого за нас всё будет делать git. Он будет перемещать нас по истории, а мы будем проверять, есть эта ошибка или нет, и сообщать об этом:
git bisect good
или
git bisect bad
В конце концов нам сообщат, какой именно коммит всё поломал. Название инструмента подсказывает нам, что на тестирование нам всегда предоставляется коммит, который находится посередине между плохим и хорошим. Таким образом, мы просматриваем не все N коммитов в истории ошибки, а всего лишь log2N.
После того, как мы выяснили, в чём причина, убрать следы, которые оставил после себя git bisect можно так:
git bisect reset
А какими инструментами git пользуетесь вы?
Материалы для самостоятельного изучения
- Документация по git
- git stash
- git bisect
01 июня 2010, 23:40
Темы: ruby, git, automation, air, mistakes, syntax

Введение
Основной целью этого блога является сбор в одном удобном месте необходимых мне по работе знаний и фишек. Однако, именно потому что это активно используемые в работе решения, со временем появляется более продуктивный или более правильный способ сделать то, о чём написано почти в каждой статье.
Иногда я просто ошибаюсь. Трудно представить что-то более полезное для опыта, нежели набивание шишек. Будет хорошо, если проведение работ над ошибками станет доброй традицией. Итак, в этом году.
git hooks
Недостатков скрипта для удаления пробелов в концах строк нашёл два:
- Скрипт без нужды дёргает ни в чём не повинные файлы, потому что \s соответствует и символу конца строки, который там всегда есть.
- Скрипт не содержит решения для выбора всех текстовых файлов проекта.
Вот хороший скрипт:
#!/usr/bin/env ruby
`git grep -I --name-only -e ""`.split("\n").each do |p|
lines = File.readlines(p).map(&:chomp)
if lines.inject(false) { |memo, l| l.gsub!(/\s+$/, "") || memo }
File.open(p, "w") do |f|
f.puts lines.join("\n")
end
puts "Removed trailing spaced from '#{p}'"
system "git add #{p}"
end
end
Так же по совету Дмитрия в комментариях добавил скрипт для проверки счастливого коммита.
Работа с версией в (ai)rake
Совершенно очевидная ошибка в примере про работу с версиями air-приложения в rake. Когда увеличивается более старшая часть версии, то все младшие должны обнуляться:
namespace :version do
[:major, :minor, :patch].each_with_index do |subv, index|
desc "Bump #{subv} in version"
task :"bump_#{subv}" do
unless `git status` =~ /nothing to commit/
raise "There are uncommitted changes. Failed to proceed."
end
appxml = YAML.load_file('airake.yml')["appxml_path"]
str = File.read(appxml)
msg = nil
new_version = nil
if str.gsub! /<version>(.*)<\/version>/ do |matched|
old_version = $1
major, minor, patch = old_version.split(".").map(&:to_i)
eval("#{subv} += 1")
new_version = [major, minor, patch].fill(0, index+1).join(".")
msg = "Version bump #{old_version} => #{new_version}"
puts msg
"<version>#{new_version}</version>"
end.nil?
raise "Cannot detect current version.\nMake sure appxml file contains <version>X.X.X</version> tag."
else
File.open(appxml, "w") do |f|
f.write str
end
puts `git commit -am "#{msg}"`
puts `git tag v#{new_version}`
end
end
end
end
Теперь rake version:bump_minor делает из 0.1.6 не 0.2.6, а 0.2.0, как и должно быть.
Мимоходом
Тем временем я сменил тарифный план у своего провайдера на (ve). И незаметно перенёс сайт. Посмотрим, как работает на собственном опыте. Работа по ssh, как была, так и осталась основным способом администрирования, а необходимость лазить в plesk пропала, потому что его теперь нет :)
07 апреля 2010, 12:59
Темы: ruby, ruby1.9, syntax, security

Введение
Совершенно не по работе заинтересовался переменной $SAFE и её ролью в жизни современного разработчика. Оказалось, что всё нужно проверять самому.
Нежная безопасность
Для тестирования возможностей на разных уровнях безопасности собрал небольшую программку. Она просит ввести имя файла, делая строковую переменную небезопасной, и пытается что-то с этим всем сделать.
print "child: "
child = gets.chomp
puts "child tainted: #{child.tainted?}"
(0..4).to_a.each do |i|
puts "SAFE: #{i}"
$a = "safe"
th = Thread.new do
$SAFE = i
child_copy = child.dup
Thread.current[:out] = ""
begin
load child_copy
Thread.current[:out] += "1. Child loaded\n"
rescue SecurityError => e
Thread.current[:out] += "1. Security error: #{e.to_s}\n"
begin
child_copy.untaint
load child_copy
Thread.current[:out] += "2. Child untainted and loaded\n"
rescue SecurityError => e
Thread.current[:out] += "2. Security error: #{e.to_s}\n"
begin
Thread.current[:out] += "3. Read from file '#{child_copy}': '#{File.read(child_copy)}'\n"
rescue SecurityError => e
Thread.current[:out] += "3. Security error: #{e.to_s}\n"
begin
Thread.current[:out] += "4. Read from untainted file: '#{File.read("child.rb")}'\n"
rescue SecurityError => e
Thread.current[:out] += "4. Security error: #{e.to_s}\n"
end
end
end
end
begin
$a = "modified"
Thread.current[:out] += "5. Global variable modified: $a = '#{$a}'\n"
rescue SecurityError => e
Thread.current[:out] += "5. Security error: #{e.to_s}\n"
end
begin
Dir.mkdir "test"
Thread.current[:out] += "6. Created directory 'test': #{File.exist?("test")}\n"
Dir.rmdir "test"
rescue SecurityError => e
Thread.current[:out] += "6. Security error: #{e.to_s}\n"
end
begin
Thread.current[:out] += "7. Dir glob: #{Dir.glob(File.join("..", "*")).inspect}\n"
rescue SecurityError => e
Thread.current[:out] += "7. Security error: #{e.to_s}\n"
end
begin
Thread.current[:out] += "8. System ls output: '#{`ls`.chomp}'"
rescue SecurityError => e
Thread.current[:out] += "8. Security error: #{e.to_s}\n"
end
end
th.join
puts "Global variable: $a = '#{$a}'"
puts th[:out] if th[:out]
end
Конструкция со Thread.current[:out] используется потому, что для $SAFE >= 4 нельзя ничего писать ни в какие устройства вывода.
Вроде бы всё логично. Первый уровень годится для умеренного карантина внешних данных. При желании их можно и расколдовать. Второй уровень запрещает изменения в файловой системе. Третий уровень похож на осаду с постоянным подозрением на шпионаж. Все созданные объекты считаются небезопасными. А четвёртый уровень это самое близкое к песочнице (sandbox) в руби, что что есть.
Кстати, когда ещё github работал как репозиторий библиотек, спецификация gemspec выполнялась там под $SAFE = 3. Для разработчиков это выливалось в то, что нужно было перечислять все файлы своей библиотеки вручную вместо использования какого-нибудь листинга.
Суровый гайдлайн
Конечно же, только использование $SAFE не убережёт от действительно настойчивой атаки или блокирующего кода. Например:
Thread.new do
$SAFE = 2
class String
def ==(other_string)
true
end
end
end.join
puts "string modified: #{'a' == 'b'}"
И это на втором уровне! А на третьем открыть класс тоже можно, но вызов перегруженного оператора будет вызывать SecurityError.
На сегодняшний момент эту концепцию безопасности можно считать сырой. Актуальное поведение руби 1.8 слегка отклоняется от описаний, что я нашёл. Поведение в 1.9 изменилось, но подробно нигде не описано (я не нашёл).
Это не значит, что этой переменной нет применения в жизни прогрессивного человечества. Адекватное текущему состоянию применение это гайдлайн при разработке. Руководство для программистов, которое само следит за своим исполнением. Жестковато, но зато действенно. :)
Материалы для самостоятельного изучения
- Код примеров в статье на github
- Старая, но самая подробная документация по $SAFE
- Просто дополнительно: шпаргалка по руби
24 марта 2010, 19:07
Темы: git, syntax

Введение
Для работы в проекте с открытыми исходниками весьма удобна распределённая система контроля версий. Я использую git. Понятно, что есть процесс с использованием патча, высылаемого по почте, но этот процесс не является эксклюзивным для распределённой системы контроля версий. Поэтому я опишу процесс с так называемым pull request.
Постановка задачи
Пишет мне некто Tallak Tveide, сообщая, что он сделал копию моего проекта у себя на github и внёс несколько правок, которые ему были необходимы, и от которых другие ребята, пользующиеся этой библиотекой только выиграют. Ветка, в которой находятся нужные мне правки, называется eos_40D_bugs. Это довольно кстати, что нашёлся человек с Кэноном, потому что я испытываю всё на Никонах :)
Каковы же мои действия?
Решение
Заходим в наш локальный рабочий репозиторий и добавляем новый источник правок:
git remote add tallakt git://github.com/tallakt/gphoto4ruby
Теперь рассмотрим правки:
git fetch tallakt eos_40D_bugs:develop
Эта команда заберёт из репозитория tallakt с ветки eos_40D_bugs исправления и создаст локальную версию в локальной ветке develop. Чтобы увидеть исправления:
git diff develop
Что выдаст нам исправления относительно текущей ветки.
git checkout develop
Чтобы работать с правками и тестировать то, что получилось.
Если я пока не готов сливать исправления с основной веткой master, но хочу ещё поработать с этим из разных мест, то мне нужно создать ветку develop в моём центральном репозитории на github, который относительно локальной копии у меня обычно называется origin.
git push origin develop
Это создаст ветку develop на удалённом репозитории, с которой я потом смогу работать из другого локального репозитория, выполнив:
git pull origin develop
После того, как я доволен изменениями и хочу сделать официальный релиз:
git merge master
git branch -d develop
git push origin master
git push origin :develop
Первая команда, предполагая, что текущая ветка develop, сливает её в master. Вторая команда удаляет локальную ветку develop. Третья команда отправляет изменения в ветку master на центральном репозитории. Четвёртая команда удаляет ветку develop на центральном репозитории.
Материалы для самостоятельного изучения
- Несколько шпаргалок от github
- Дельная документация по git
Послесловие
Как вы заметили, в этом году мои статьи сопровождаются прекраснейшими тематическими картинками авторства Ирины Троицкой моей прекрасной супруги. Её перу также принадлежит дизайн сайта и логотипа.
09 декабря 2009, 16:43
Темы: ruby, ruby1.9, syntax, extension
Введение
Сегодня будет блиц-молния, которой место, скорее в твиттере. Однако, мне необходимо развеять тучи, которые я сам же и нагнал. :)
Постановка задачи
Не так давно я писал о том, что при создании руби-оболочки вокруг библиотеки на си, легко получить код, который блокирует все потоки приложения. Можно ли с этим что-то сделать?
Решение
Если мы имеем чуть больше контроля над участками кода на си, которые выполняются долго (обычно в цикле), то можно добавить спасительный код в тело цикла:
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 ноября 2009, 23:16
Темы: ruby, extension, syntax, bash
Постановка задачи
Как разработчику gphoto4ruby мне приходится сталкиваться с особыми задачами. Связано это с тем, что этот gem является оболочкой поверх ещё одной библиотеки. И как у всякой более-менее развитой сторонней библиотеки, у libgphoto2 есть версия, распространяемая через системные репозитории и порты и есть, так сказать, последний писк моды (bleeding edge).
Отсюда вытекает необходимость:- Иметь разные версии библиотеки не конфликтующие между собой, установленные не одной системе,
- Компилировать свою руби-библиотеку под любую из версий.
Установка двух 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 ноября 2009, 20:46
Темы: ruby, ruby1.9, syntax, regexp
Постановка задачи
Собрать в одном месте важные, на мой взгляд, особенности сравнений и неравенств в руби.
Основа неравенств в руби
Основным методом сравнения является <=>. Определив его, мы определяем все остальные операции, включив модуль 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
Материалы для самостоятельного изучения
- Полный код статьи на github
- Что нужно помнить, создавая свой объект руби
22 октября 2009, 00:48
Темы: ruby, extension, syntax, daemon, ruby1.9
Введение
Сначала я расскажу, почему на сегодняшний день я не очень много работаю с подпроцессами на базе 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
По-моему, весьма удобно, если вам нужно не просто ожидать окончания работы потоков, но ещё и делать что-то при этом.
Материалы для самостоятельного изучения
- Полный код статьи на github
- Документация ThreadsWait
- Толковая статья о многопотоковости и процессах в руби
16 сентября 2009, 01:26
Темы: ruby, syntax
Введение
Сегодняшняя запись это первая запись по заявкам читателей. :) Я напоминаю, что если у вас включен 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: Неведомая ошибка>
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Ошибка!
------------------------
Что мы отсюда почерпнули:- Блок else (необязательный) выполняется в случае удачно выполненного begin и возвращает значение вместо него.
- Выполняется первый из блоков rescue, который соответствует ошибке
- Метод raise без параметров выбрасывает текущее исключение: то, которое находится в глобальной переменной $!
- Чтобы создать свой класс исключений, наследовать нужно от 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 нас не спасут.
Для самостоятельного изучения
- Вопросы-задачи-загадки, на которые полезно ответить не пиша программы
- Иерархия классов исключений в руби
- Более подробно про азы, в которые я не стал здесь спускаться
27 августа 2009, 17:07
Темы: ruby, syntax
Введение
Главная причина, по которой я испытываю нежные чувства к руби это гибкость синтаксиса. Сегодня я хочу рассказать об особенностях передачи блоков и массивов в качестве параметров. Базовые вещи можно прочитать по ссылкам в последнем параграфе.
Массивы
О том, как принять неограниченное число аргументов, знают все:
$KCODE = "utf-8"
def my_args(*args)
puts args.inspect
end
my_args(1, 2, "собачка", "котик") # => [1, 2, "собачка", "котик"]
Но что, если сами объекты, которые необходимо передать в качестве аргументов, уже находятся в массиве по какой-то причине?
arr = ["собачка", "котик", "ёжик", "медвежонок"]
selection = arr.select{ |a| a.chars.to_a.length > 5 }
my_args(selection) # => [["собачка", "медвежонок"]]
Внезапно массив (args) стал двумерным, что неудивительно, т.к. метод интерпретировал переданный ему массив как первый аргумент типа Array. В данном случае, чтобы передать массив как список аргументов, нужно использовать ту же звезду:
my_args(*selection) # => ["собачка", "медвежонок"]
Ура!
Кстати, звезду можно использовать и при получении значения от функции. Например, в таком случае:
def get_values
["красный", "зелёный", "синий"]
end
r, g, b = get_values
puts r.inspect # => "красный"
puts g.inspect # => "зелёный"
puts b.inspect # => "синий"
r, gb = get_values
puts r.inspect # => "красный"
puts gb.inspect # => "зелёный"
r, *gb = get_values
puts r.inspect # => "красный"
puts gb.inspect # => ["зелёный", "синий"]
По-моему, здорово!
Блоки
Бывают случаи, когда блок нужно сохранить на будущее, чтобы потом его использовать. Тогда можно указать его в списке параметров метода, что, конечно же, не делает его обязательным само по себе.
class Button
def on_click(&block)
if block_given?
@stored_block = block
else
puts "Блока нет, но жизнь продолжается"
end
end
def click(*args)
@stored_block.call(*args)
end
end
my_btn = Button.new
my_btn.on_click # => Блока нет, но жизнь продолжается
my_btn.on_click do |*args|
puts "Произошёл клик!"
end
my_btn.click # => Произошёл клик!
Теперь предположим, опять же, что по каким-то причинам блок у нас уже есть. Он определён до вызова метода.
handler = lambda{ |*args| puts "Вот аргументы: #{args.inspect}" }
my_btn.on_click(handler) # => wrong number of arguments (1 for 0) (ArgumentError)
То есть наш блок превратился в аргумент типа Proc. А метод совсем этого не ожидал. Можно модифицировать сам метод, чтобы он выдерживал подобные условия, но не для этого я всё это пишу :) Если мы чему-нибудь научились в предыдущей главке, то решение по аналогии приходит само:
my_btn.on_click(&handler)
my_btn.click("усы", "хвост") # => Вот аргументы: ["усы", "хвост"]
Надеюсь, что было полезно.
Материалы для самостоятельного изучения
- Аргументы в руби
- Ещё про аргументы в руби