28.10.2009Удалённые вызовы через систему распределённых объектов в руби (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>
Параметр после вызова: <йипок зеб акортсрепус>
Если немножко почитать, и разобраться, какие объекты можно и нужно «маршализировать», а какие нельзя или не нужно, то получается вполне себе прекрасный инструмент. Который, повторюсь, входит в стандартную библиотеку и не требует никаких внешних зависимостей.
Материалы для самостоятельного изучения
25.10.2009Винегрет
Заметил удивительную особенность. За выходные мне нужно хотя бы один день максимально потупить. По возможности никуда не ходить, не программировать, можно заниматься уборкой разного рода, фильмов лучше смотреть немного (один-два). Если такого дня не получается, то к понедельнику ощущение, что не доотдохнул. Поэтому по выходным я люблю читать жж. Жалко, что в выходные мало кто пишет.
Когда писал статью про потоки в руби, заметил ещё одну удивительную особенность. Даже когда я пишу о том, с чем встречаюсь в работе и о том, что интересно прежде всего мне, хочется написать в форме образовательного материала. В расчёте на то, как бы мне хотелось это прочитать, если бы я этого не знал. И, конечно же, когда запрос на тему приходит через форму обратной связи, то писать сразу становится ещё интереснее. Захотелось даже сделать скринкаст: по ощущениям — это наиболее удобная образовательная форма в программировании. Потому что это прежде всего процесс и только потом код.
Кстати, Витла себе тоже поставил такую форму обратной связи на ito-sama (такая синяя кнопка слева). Ему теперь тоже можно что-то предлагать.
Кстати, сам проект http://reformal.ru/ с формами обратной связи — это калька с буржуйского проекта http://uservoice.com/. И ничего — все счастливы.
22.10.2009Работа с потоками (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
По-моему, весьма удобно, если вам нужно не просто ожидать окончания работы потоков, но ещё и делать что-то при этом.
Материалы для самостоятельного изучения
09.10.2009Спецыално дла джэма
В то время как мой любимый github перестал компилировать джемы в своих репозиториях, всё прогрессивное человечество разделяет задачи хранения исходников и создания джемов, перенося последнюю на gemcutter.
Удивительно, что проект существовал уже какое-то время, но вновь засиял после редизайна, и стремительно набирает популярность. Леденцовый дизайн — моя слабость: хочется лизнуть монитор.
08.10.2009Будни разработчика
Пригласили сегодня свежего специалиста по паянию. По его реакции было непонятно, то ли он офигел от свалившегося на него счастья, то ли вся работа ему уже обрыдла, не успев начаться. Ещё года два-три назад я бы до него докопался по этому поводу, а сегодня не стал. Зафрендил в жж: вдруг чего напишет. :)
Мы собрали новую установочку. Может быть, мне удастся понянчиться с ней пару дней и наделать разных ништяков в программе управления перед тем, как суровые люди заберут у меня её, чтобы мучать вдали от дома. О том, как мне приходится скрещивать скрипты на ruby с оболочкой bash, пока не программирую, я даже написал в блог.
А в остальном всё хорошо. И в Питере хорошо, и в Москве. А как у вас дела?
07.10.2009Некоторые тонкости стыковки 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"
Материалы для самостоятельного изучения
03.10.2009Питер
Сегодня в субботу (!) мне позвонили из одной компании, которая должна моей жене неприличную сумму денег и опять забыла об этом, и предложили работу или кого-то порекомендовать. Я, конечно, офигел, но вслух сказал, что занят на несколько месяцев вперёд, и никого порекомендовать не могу.
С одной стороны, когда задача стоит трудная, даже не обязательно сверх-задача-жизни, то это способно меня вырубить на неделю. Я читаю жж, дёрти-ру, в перерывах что-то гуглю про задачу, открывая кучу вкладок. У стороннего наблюдателя может сложиться впечатление, что я пинаю балду. Пока в один прекрасный момент задача вдруг не распадается на несколько вполне подъёмных частей. Это магический момент, но ему предшествует тупка.
А с другой стороны, когда забываешь про более крупную задачу, включая сверх-задачу-жизни, то вруг начинают мешать и досаждать всякие мелочи. Появляются придирки и беспокойство. Можно тратить часы на преждевременную оптимизациюTM или разговоры. Вроде бы чем-то плотно занят, но ничего не меняется.
Где баланс?
А мы тем временем поехали в Питер.
23.09.2009Получение 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"
Здесь стоит сделать два акцента:
- Для сообщения приложению, что оно запущено в консоли, мы использовали волшебный хэш ENV. К нему потом можно обратиться внутри config/environment.rb и сделать что-то по-другому.
- Для запуска консоли мы использовали 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" # куда сохранять
Материалы для самостоятельного изучения
16.09.2009Обработка исключений в руби
Введение
Сегодняшняя запись — это первая запись по заявкам читателей. :) Я напоминаю, что если у вас включен 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 нас не спасут.
Для самостоятельного изучения
13.09.2009День программиста
Поздравляю
Дорогие коллеги! Поздравляю вас с Днём программиста! Желаю прежде всего большого интереса в работе, прозрачного кода и быстрой отладки. :)
Бонус
Сегодня я получил предложение о теме следующей записи. В связи с этим была сделана небольшая, но приятная модификация.
С сегодняшнего дня в блоге появилась ворма обратной связи. Оранжевая, такая, кнопка слева. Через неё можно предлагать темы, о которых было бы интересно узнать. А так же голосовать за уже предложенные. Ура!