Блог 7even

про ruby, rails, sinatra, git и всё на свете

CLI-утилита на основе Thor

Для многих не секрет, что Ruby используется не только для web-разработки - этот язык также отлично пригодится для написания простеньких скриптов, упрощающих повседневные рутинные задачи. При этом самый естественный способ взаимодействия со скриптами - через командную строку, он же CLI (Command-Line Interface).

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

thor заявлен как замена rake, и в этой стезе, надо сказать, он пока еще не преуспел - но зато гем здорово помогает при разработке собственных CLI-утилит, а также отлично справляется с генерацией файлов на основе шаблонов (именно на основе thor работают все генераторы в rails, начиная с версии 3.0).

Задача

Сначала я хотел написать очередную обзорную статью с небольшими примерами кода, но потом решил, что все-таки интереснее говорить о чем-то конкретном; поэтому по ходу статьи будем создавать простенькую CLI-утилитку, выводящую некоторую нужную информацию - непрочитанные письма в gmail-ящике, заголовки 5 последних новостей с ленты.ру и 5 последних коммитов в рельсы с гитхаба. А назовем ее workhorse (рабочая лошадка).

Объектная модель Ruby

Данный опус является моей попыткой собрать воедино все принципы, согласно которым работает объектная модель в языке Ruby. Цель статьи в том, чтобы читатель увидел логику там, где раньше видел “магию”.

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

Основы

Ruby является полностью объектно-ориентированным языком: числа, строки, регулярные выражения, массивы - это все объекты определенных классов. Класс определяет поведение объекта - он содержит все методы, доступные его объектам (инстансам).

1
2
3
4
5
6
7
8
class MyClass
  def my_method
    # some code
  end
end

obj = MyClass.new
obj.my_method

Традиционно в ООП используется наследование классов - методы класса-родителя доступны объектам дочерних классов (а также их дочерних классов итд). Разумеется, в ruby этот механизм тоже не забыт:

1
2
3
4
5
6
7
8
9
10
11
class Parent
  def some_method
    'Parent#some_method'
  end
end

class Child < Parent
end

obj = Child.new
obj.some_method # => "Parent#some_method"

Тонкая настройка SSH

Любой веб-разработчик пользуется ssh чуть ли не ежедневно - чтобы размещать код на удаленном сервере, конфигурировать этот сервер, производить какие-то операции с файлами итд. Но не все знают о возможностях тонкой настройки клиента OpenSSH - о них и пойдет речь в этой статье.

Общее соединение

Часто есть необходимость подключиться к одному и тому же серверу одновременно в нескольких окнах/вкладках консоли. ssh можно настроить так, чтобы вместо создания нового соединения к серверу использовалось уже созданное. Для этого нужно добавить следующие строчки в ~/.ssh/config (возможно, этот файл придется создать):

1
2
ControlMaster auto
ControlPath /tmp/ssh_mux_%h_%p_%r

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

Этот метод также работает со всеми утилитами, использующими ssh-соединения: scp, rsync, git итд - достаточно открыть соединение, и утилита будет его использовать. При вводе пути к файлу на удаленном сервере в scp даже будет работать tab-completion.

JSON.load и компания

В работе над текущим проектом мне часто приходится иметь дело с форматом json, причем не только выдавать json ответом с сервера, но еще и читать json-параметры запросов, и даже наборы параметров, сериализованные в json и уложенные в поле в таблице БД.

В ruby, как известно, для этого существует стандартная библиотека json. Упаковка данных в json и распаковка их обратно происходит с помощью 2 методов:

1
2
3
4
JSON.dump(a: 1, b: 2)
# => '{"a":1,"b":2}'
JSON.load('{"a":1,"b":2}')
# => {"a"=>1, "b"=>2}

И все бы хорошо, да вот только в один прекрасный день моя коллега на своей ubuntu-машине запустила спеки, и JSON.load почему-то повел себя как IO.load - решил, что параметром ему подсовывают имя файла - но файла под названием ‘{“result”:true}’ рядом не нашлось, и случился Errno::ENOENT: No such file or directory

Сказ о Deadlock-ах

Каждый разработчик, работавший над нагруженным проектом, сталкивался с дедлоками - это ситуация, которая возникает в БД, когда две транзакции блокируют друг друга, и в результате одна из них сбрасывается (во всяком случае такое поведение реализовано в PostgreSQL). Недавно пришло время и мне столкнуться с такой ситуацией.

Бэкграунд

Есть rails-приложение, построенное по принципу RIA, в котором фронт-энд логически разделен с бэк-эндом. Фронт-энду нужно знать, что происходит на серверсайде, и поэтому раз в секунду приходит запрос на некий урл, где определенный экшен определенного контроллера производит какие-то действия и рендерит ответ в формате JSON.

И среди действий этого контроллера есть обновление времени последнего доступа. Реализовано оно одним UPDATE-запросом примерно следующего вида:

1
UPDATE items SET access_time = NOW() WHERE id IN (34256, 34978, 34147)

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

1
2
3
4
5
6
7
postgres[22432]: [30-1] ERROR:  deadlock detected
postgres[22432]: [30-2] DETAIL:  Process 22432 waits for ShareLock on transaction 189302415; blocked by process 22443.
postgres[22432]: [30-3]  Process 22443 waits for ShareLock on transaction 189302416; blocked by process 22432.
postgres[22432]: [30-4]  Process 22432: UPDATE "items" SET "access_time" = '2011-07-08 08:49:03.429301' WHERE "items"."id" IN (691, 690, 692, 689, 686, 688, 687)
postgres[22432]: [30-5]  Process 22443: UPDATE "items" SET "access_time" = '2011-07-08 08:49:03.414084' WHERE "items"."id" IN (686, 687, 688, 689, 691, 690)
postgres[22432]: [30-6] HINT:  See server log for query details.
postgres[22432]: [30-7] STATEMENT:  UPDATE "items" SET "access_time" = '2011-07-08 08:49:03.429301' WHERE "items"."id" IN (691, 690, 692, 689, 686, 688, 687)