Для этого напишем собственный web-based мессенджер, умеющий отправлять сообщения, принимать и помечать сообщения прочитанными. Большая часть приложения будет оперировать на клиенте: фронт-энд будет устанавливать постоянное соединение с бэк-эндом, отправлять ему запросы и получать ответы; а бэк-энд займется непосредственной работой с API.
Самый примечательный момент в таком приложении - асинхронность: бэк-энд должен постоянно ждать от ВКонтакте новых сообщений, и параллельно с этим обрабатывать данные, присылаемые фронт-эндом, после чего вызывать нужный метод API (например, фронт-энд сообщает, что 2 входящих сообщения прочитаны - необходимо вызвать API-метод messages.markAsRead).
Итак, попробуем описать схему работы всего приложения.
Дальнейшие действия происходят по следующей схеме: фронт-энд сообщает о действии пользователя бэк-энду, тот вызывает нужный метод API, после чего соответствующее обновление приходит от ВКонтакте в основном цикле, передается обратно на фронт-энд и рендерится. Таким образом происходит отправка сообщений из формы, а также пометка входящих сообщений прочитанными (при заходе пользователя в соответствующую вкладку интерфейса). Например, при отправке сообщения мы не рисуем его сразу после сабмита формы, а ждем, когда оно придет как обновление из основного цикла.
Существует определенное количество ruby-врапперов для ВКонтакте API, но из известных мне библиотек только vkontakte_api позволяет работать с API асинхронно, поэтому в данном приложении будем использовать его. Для организации асинхронной работы на бэк-энде возьмем eventmachine, избежим callback spaghetti с помощью em-synchrony, а общаться с фронт-эндом будем посредством WebSocket (для чего используем em-websocket).
Файловую иерархию организуем следующим образом: в lib/
положим ruby-код, в public/
отправится единственный нужный нам html-файл index.html
, а в public/css/
, public/img/
и public/js/
соответственно стили, картинки и яваскрипт + кофескрипт.
Чтобы не усложнять себе жизнь, подключим все необходимые гемы через bundler
(не забывая выполнить bundle install
после этого):
1 2 3 4 5 6 7 8 9 |
|
Для упрощения локального запуска и деплоя приложения используем библиотеку foreman, которая позволит описать процесс запуска в Procfile
. Основной рабочий скрипт будет находиться в lib/main.rb
:
1
|
|
lib/main.rb
нужен для обработки запросов, приходящих по WebSocket; тут мы конфигурируем VkontakteApi
, создаем клиент API в глобальной переменной (т.к. для всех запросов будем использовать именно его) и делегируем основную работу классу Messenger
(передавая ему параметром объект WebSocket-а, дабы тот смог отправлять данные на фронт-энд):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
Long-polling в исполнении ВКонтакте работает следующим образом: приложение делает HTTP-запрос на определенный URL с определенными параметрами (и URL, и параметры нужно предварительно получить специальным API-методом messages.getLongPollServer), и соединение зависает на время не более указанного в параметре wait
кол-ва секунд (документация рекомендует устанавливать этот параметр на 25). Если до истечения этого интервала происходит какое-то событие (пришло новое сообщение, кто-то из друзей вышел в онлайн), немедленно приходит ответ; если же за 25 секунд ничего не происходит, то по истечении этого времени приходит пустой ответ. После получения ответа нужно отправить новый запрос, и так до бесконечности.
В описанном выше запросе на получение обновлений используется параметр key
- ключ, время действия которого ограничено 3-4 часами; когда это время проходит, запрос начинает возвращать в ответ {"failed": 2}
. В этом случае необходимо снова получить параметры запроса API-методом messages.getLongPollServer
. Также присутствует параметр ts
- что-то вроде идентификатора последней полученной порции обновлений; каждый long-polling запрос возвращает это значение, и его нужно использовать в следующем запросе, чтобы не получать обновления повторно.
Итак, основной класс. Сетевые запросы, пропатченные в em-synchrony
, нельзя выполнять в корневом файбере, поэтому приходится создавать отдельный файбер и запускать код в нем - для этого добавлен хелпер #in_fiber
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
|
Как видно, Messenger#start
запускает основной цикл получения обновлений, Messenger#stop
его останавливает (это нужно при закрытии вебсокет-соединения, иначе после обновления страницы будет уже два бесконечных цикла), Messenger#send_message
отправляет сообщение указанному пользователю, Messenger#load_previous_messages
загружает последнюю историю сообщений от и к указанному пользователю, а Messenger#mark_as_read
помечает сообщения прочитанными.
На этом бэк-энд готов, переходим к фронт-энду.
Во-первых, недолго думая, возьмем Twitter Bootstrap для создания интерфейса, дабы не тратить на это лишнее время и нервы. Во-вторых, по тем же причинам, используем CoffeeScript для программирования клиентской части.
Интерфейс будет выглядеть следующим образом: слева будет сайд-бар, в котором будет располагаться меню, содержащее весь список друзей пользователя; справа разместим основную контентную область, переключаемую с помощью меню. Т.е. по клику на друге в меню справа будет открываться блок с сообщениями от/к этому другу, а также с его переходами в онлайн/оффлайн.
Также в меню добавим метки: для друга, который сейчас онлайн, и для счетчика непрочитанных входящих сообщений от этого человека.
Верстка основной страницы выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
Теперь займемся обработкой данных, приходящих из веб-сокета, а также повесим обработчики на переключение вкладок пользователей (нужно подгружать предыдущие сообщения при первом открытии вкладки, и помечать все непрочитанные сообщения прочитанными) и сабмит формы отправки сообщения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
Список друзей и связанные с ним методы будем хранить в глобальной переменной usersList
, а работу с обновлениями из основного цикла организуем через глобальный объект feed
.
При получении списка друзей вызываем usersList.load
, который в свою очередь удаляет все элементы .loading
и рендерит интерфейс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
При загрузке списка пользователей создается массив объектов класса User
. Объекты этого класса должны уметь запрашивать историю сообщений, следить за кол-вом непрочитанных входящих сообщений от соответствующего друга, а также помечать их прочитанными (все сразу, т.к. это будет происходить при заходе пользователя на вкладку с этим другом).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
|
Все сообщения пользователя сохраняются в @messages
, это происходит в методе addMessage
- как и работа со счетчиком и меню. Классу Message
остается лишь вызвать @user.addMessage
и отрендерить сообщение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
И последнее, что остается - обработка обновлений, постоянно поступающих с сервера.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
Мессенджер готов. И вот как он выглядит:
К сожалению, ВКонтакте позволяет использовать API-методы для работы с сообщениями только десктопным и мобильным приложениям, поэтому придется получить токен доступа вручную и хранить его в конфигурации приложения. Благодаря foreman
можно положить токен в файл .env
в корне приложения, и он будет передаваться в main.rb
как переменная окружения.
Итак, чтобы получить токен, сначала нужно зарегистрировать свое приложение на ВКонтакте. На этой странице нужно выбрать тип приложения “Standalone-приложение” и задать имя. Далее видим страницу настроек приложения, здесь нам нужно только поле “ID приложения”.
Теперь можно получать токен. Запускаем любую ruby-REPL (я использую pry
, можно также взять irb
) и подключаем гем vkontakte_api
:
1 2 3 4 5 6 7 8 |
|
Дальше просто копируем полученный URL и идем по нему в браузере. Там будет страница, предлагающая подтвердить права приложения на доступ к друзьям и личным сообщениям, а также доступ в любое время (это важно, поскольку позволяет получить “вечный” токен - его больше не придется обновлять). После нажатия на кнопку “Разрешить” идет редирект на страницу с текстом “Login success” - при этом в URL страницы будет параметр access_token
, который нам и нужен.
Копируем токен и вставляем в файл .env
в следующем формате:
1
|
|
Теперь при запуске приложения foreman
положит токен в переменную окружения TOKEN
, и в lib/main.rb
он будет доступен как ENV['TOKEN']
- откуда мы его и берем при создании клиента API.
1 2 3 4 |
|
Остается лишь открыть в браузере файл public/index.html
. Сразу после загрузки страницы фронт-энд открывает вебсокет-соединение, получает список друзей, рендерит интерфейс и начинает ожидать новых сообщений. Мессенджер работает :)
Как всегда, код на гитхабе.
В получившемся приложении остается еще довольно много возможностей для доработки - показывать аттачменты к сообщениям (картинки, аудио, видео), находить и рендерить URL-ы в виде ссылок; но проект показывает, что асинхронно работать с ВКонтакте API довольно удобно.
]]>В стандартной библиотеке Ruby есть модуль OptionParser
, здорово облегчающий работу с парсингом аргументов, пришедших из командной строки - но в этой статье речь пойдет о библиотеке thor
, которую создал небезызвестный Yehuda Katz. Помимо обработки аргументов, thor
предоставляет методы для более удобного взаимодействия с пользователем во время сессии работы самой утилиты.
thor
заявлен как замена rake
, и в этой стезе, надо сказать, он пока еще не преуспел - но зато гем здорово помогает при разработке собственных CLI-утилит, а также отлично справляется с генерацией файлов на основе шаблонов (именно на основе thor
работают все генераторы в rails, начиная с версии 3.0).
Сначала я хотел написать очередную обзорную статью с небольшими примерами кода, но потом решил, что все-таки интереснее говорить о чем-то конкретном; поэтому по ходу статьи будем создавать простенькую CLI-утилитку, выводящую некоторую нужную информацию - непрочитанные письма в gmail-ящике, заголовки 5 последних новостей с ленты.ру и 5 последних коммитов в рельсы с гитхаба. А назовем ее workhorse
(рабочая лошадка).
Для начала создадим скелет нашей утилиты. Структура будет стандартной - в bin/
лежат запускаемые скрипты (в нашем случае один), в lib/
библиотечные файлы, стандартный Gemfile
и файл конфигурации config.yml
в корне.
В данном случае я позволю себе не писать спеки, так как это не является темой статьи, и к тому же при работе с внешними API спеки, написанные с применением традиционных стабов, получаются очень уж громоздкими.
Для работы с gmail-ящиком потребуются логин и пароль. Чтобы не вводить их каждый раз, сложим их в config.yml
в корне проекта, но позволим пользователю передавать их параметрами, если вдруг потребуется проверить другой ящик. Аналогично поступим и с гитхабом - в yaml-е напишем название репозитория rails/rails
, но будем ждать параметр из командной строки.
1 2 3 4 5 6 |
|
Во-первых нам нужен сам thor
для создания command-line интерфейса; отвечающий за эту задачу класс будет наследоваться от класса Thor
. Далее, для работы с GMail возьмем гем gmail
; новости с ленты.ру будем брать через их RSS, а для парсинга HTML и XML данных я обычно использую nokogiri
от tenderlove; ну а с Github API я выбрал библиотеку octokit
(она не единственная в своем роде - но поскольку я столкнулся с задачей впервые, просто взял самый популярный по watcher-ам гем):
1 2 3 4 5 6 7 8 9 |
|
Для такой простой задачи вполне хватит одного модуля, выполняющего всю работу - каждый метод будет производить какие-нибудь действия. Назовем этот класс Workhorse
и положим в lib/workhorse.rb
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
|
Каждый метод возвращает массив из хэшей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
Теперь остается только набросать command-line интерфейс для модуля, чтобы им можно было пользоваться даже без знания Ruby.
Для этого мы создаем свой класс, наследующийся от класса Thor
, и создаем таски. Таск в thor
представляет из себя обычный метод, перед которым указывается его описание с помощью метода desc
(он принимает 2 параметра - название метода строкой или символом, и строку с описанием). В конце мы вызываем метод .start
нашего класса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
|
Не забываем сделать файл исполняемым: chmod +x bin/workhorse
Смотрим, что у нас получилось. Для начала проверим страницы хелпа с информацией о наших тасках и их параметрах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Как видим, все выводится в достаточно удобочитаемом виде. Теперь попробуем запустить какой-нибудь таск:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
(здесь этого не видно, но заголовки ‘Unread emails:’ и ‘Last commits:’ подсвечиваются зеленым)
Ну и, наконец, проверим, как работает наш основной и дефолтный таск - summary
. Он запускает все остальные таски, спрашивая у пользователя перед каждым, нужно ли это делать (а чтобы умерить его разговорчивость, можно передать опцию --quiet
или ее синоним -q
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
При вызове таском других тасков все параметры из командной строки доступны всем участвующим таскам, поэтому при вызове summary
можно указывать опции для гитхаба и gmail-а:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
В статье была описана лишь часть возможностей thor
- гем также имеет множество возможностей по генерации файлов по шаблонам и копированию их, что делает этот инструмент незаменимым для рельсовых генераторов и аналогичных задач.
Например, есть метод inject_into_class
, вставляющий текст в файл сразу после определения класса (т.е. сразу после строки class MyClass
); insert_into_file
, вставляющий текст в файл после строки, найденной по переданному регулярному выражению; get
, создающий файл по указанному пути с содержимым, полученным по указанному урлу итд.
Во-первых стоит почитать документацию thor
:
Также немного информации есть в вики thor-а на github.
Ну и, конечно, не нужно бояться читать исходники :)
Разумеется, получившаяся утилита лежит на гитхабе.
В процессе написания статьи в качестве кейвордов было указано следующее:
1
|
|
Результат превзошел самые смелые ожидания:
1
|
|
Вывод: в octopress кейворды нужно задавать строкой, а не массивом.
]]>Первый раздел предназначен скорее для тех, кто с Ruby знаком слабо; практикующим рубистам следует его пропустить и переходить к описанию вызова метода.
Ruby является полностью объектно-ориентированным языком: числа, строки, регулярные выражения, массивы - это все объекты определенных классов. Класс определяет поведение объекта - он содержит все методы, доступные его объектам (инстансам).
1 2 3 4 5 6 7 8 |
|
Традиционно в ООП используется наследование классов - методы класса-родителя доступны объектам дочерних классов (а также их дочерних классов итд). Разумеется, в ruby этот механизм тоже не забыт:
1 2 3 4 5 6 7 8 9 10 11 |
|
Однако, множественное наследование не поддерживается: у каждого класса может быть только один родительский класс - за исключением BasicObject
(в 1.8 и ранее Object
), который является корнем иерархии и не имеет родителя. В случаях, когда нужно предоставить какое-то кол-во методов нескольким классам, но нежелательно пользоваться наследованием (скажем, когда нужно наследоваться от какого-то другого класса), удобно использовать механизм примесей (mixins). Это происходит следующим образом: в модуле определяются методы, потом определенный класс подключает этот модуль - и объекты класса получают все методы, определенные в модуле. При этом кол-во включаемых в класс модулей ограничено только здравым смыслом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Итак, каждый объект является инстансом определенного класса, а класс в свою очередь имеет цепочку наследования, которая восходит вверх до BasicObject
.
Объект содержит какое-то количество инстанс-переменных (например, объект класса Person
может иметь инстанс-переменную @name
) и ссылку на свой класс.
Класс содержит определенное количество инстанс-методов (так я буду называть методы, которые класс предоставляет своим объектам и объектам дочерних классов, чтобы не путать их с классовыми методами - такими как Time.now
), некоторое число подключенных модулей и ссылку на родительский класс.
Посмотрим, что происходит, когда вызывается определенный метод:
Начнем с получателя. В любом участке кода всегда есть текущий объект - его возвращает псевдопеременная self
. У текущего объекта есть два основных значения: при обращении к инстанс-переменным ruby будет искать их в текущем объекте, и при вызове метода без указания получателя он будет вызван на текущем объекте. При вызове метода с указанием получателя, соответственно, получателем будет указанный объект.
Так вот. Как только выполнение попадает внутрь вызванного метода, self
становится этим объектом-получателем и остается им до тех пор, пока не будет вызван метод с другим объектом-получателем, либо пока метод не будет полностью выполнен (а также в некоторых других случаях, которые будут описаны далее). Все это время обращения к инстанс-переменным будут работать с текущим объектом, и вызовы методов без указания получателя будут попадать также в текущий объект.
Теперь посмотрим на поиск метода в иерархии классов. У нас уже есть объект-получатель вызова, а он содержит ссылку на свой класс. Если класс содержит метод с таким названием, то этот метод используется; иначе поиск продолжается в классе-родителе, потом в его родителе итд - вплоть до BasicObject
.
Если же метод так и не был найден, будет вызван метод method_missing
с параметрами, первым из которых будет название вызванного метода, далее аргументы, переданные при вызове, и блок при его наличии. Вызов происходит по той же схеме - сначала method_missing
ищется в классе объекта, потом в его родителе итд. Стандартный BasicObject#method_missing
выбрасывает NoMethodError
, если вызов добирается до него.
Но как в эту схему вписываются модули, подключенные в классы из цепочки наследования? Их методы также должны быть видимы объекту класса, который стоит ниже в цепи (если считать, что BasicObject
стоит на самом верху).
Дело в том, что при включении класса руби создает анонимный класс, и помещает его в цепочку наследования прямо над включившим этот модуль классом. Такие анонимные классы часто называют прокси-классами. Соответственно, при поиске метода, определенного в модуле, он будет найден в анонимном прокси-классе, и все произойдет так, как если бы метод был определен в одном из настоящих классов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
В ruby есть специальная форма определения метода: def object.method_name
. Созданный таким образом метод называется синглтон-методом - он определен только для этого конкретного объекта.
1 2 3 4 5 6 7 8 |
|
Именно благодаря этой особенности в ruby работают классовые методы: каждый класс является объектом класса Class
, который предоставляет лишь необходимый минимум методов - new
, superclass
итд. Если же нам нужно определить классовый метод (который будет инстанс-методом с точки зрения класса Class
), обычно мы задаем его следующим образом:
1 2 3 4 5 |
|
Разумеется, в данном случае метод class_method
становится доступен только MyClass
, и никакому другому классу (на самом деле он также будет доступен дочерним классам MyClass
, но об этом чуть позже).
Часто для определения сразу нескольких классовых методов используется конструкция class << self
внутри определения класса; в этом случае методы, определенные в обычной форме (def method_name
), становятся классовыми методами. То же самое применимо и к обычным объектам - в общем случае конструкция выглядит как class << object
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Это все может выглядеть как магия, но на самом деле ruby руководствуется единой логикой. Посмотрим внимательнее, что тут происходит.
Как уже говорилось выше, объект - это просто группа инстанс-переменных и ссылка на класс, который в свою очередь содержит некоторое кол-во инстанс-методов. Но синглтон-методы видны только одному конкретному объекту, поэтому не могут находиться в классе объекта; в то же время метод не может находиться в объекте, так как только классы и модули могут содержать методы (предоставляя их своим объектам и объектам дочерних классов).
Решением этой загадки является синглтон-класс (также встречается термин eigenclass). Это эдакая прослойка между объектом и его номинальным классом (тем самым, который можно получить через object.class
). Все синглтон-методы объекта попадают в его синглтон-класс, и уже он является “реальным” классом объекта. В то же время, чтобы не терять возможность вызывать инстанс-методы, определенные в номинальном классе, синглтон-класс наследуется от него, поэтому поиск метода при его вызове начинается с синглтон-класса, дальше поднимается в номинальный класс, в его родителя и дальше вверх до BasicObject
.
1 2 3 4 5 6 7 8 9 |
|
Наследование синглтон-классов классов устроено несколько сложнее. Классы наследуют у своих родителей не только инстанс-методы, но и классовые методы - но, как мы знаем, они содержатся в синглтон-классе класса, который при вышеописанной схеме наследования не попадает в цепочку. Поэтому в ruby синглтон-класс класса наследуется от синглтон-класса его родителя. Звучит немного запутанно, поэтому лучше посмотреть на примере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
К сожалению (а, может, и к счастью), синглтон-методы скрыты от глаз программиста в методах, инспектирующих цепочку наследования - ни Kernel#class
, ни Module#ancestors
их не показывают. Но, как видно из последнего примера, с помощью Object#singleton_class
и Class#superclass
это можно обойти.
В каждой точке кода определены так называемые “текущий объект” и “текущий класс”.
Про текущий объект мы уже поговорили - это объект, который доступен через псевдопеременную self
; к нему адресуются все вызовы методов без указания получателя и в нем ищутся все запрошенные инстанс-переменные.
Текущий класс - это тот класс, инстанс-методом которого становится определенный в этом месте метод.
1 2 3 4 5 6 |
|
К сожалению, в отличие от текущего объекта, который можно узнать через self
, текущий класс можно отслеживать только по коду.
Посмотрим все типичные случаи, имеющие собственные правила определения self
и текущего класса.
До того, как мы входим в определение какого-либо класса, self
указывает на main
, а текущим классом является Object
. Это объясняет тот факт, что методы, определяемые на верхнем уровне, вызываются из любого участка кода, причем без получателя (такой метод будет приватным, т.е. его нельзя вызывать с явным получателем; а self
указывает на объект класса Object
либо его потомка, поэтому метод будет доступен).
1 2 3 4 5 6 7 |
|
В начале объявления класса self
и текущий класс меняются на объявляемый класс. Пожалуй, это самый очевидный пример использования текущего класса - определяемые методы становятся инстанс-методами класса, в определении которого это происходит. Классовые макросы, вроде attr_accessor
, вызываются без указания получателя и попадают в self
, т.е. применяются на объявляемый класс.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
В объявлении метода self
указывает на объект, являющийся получателем при вызове этого метода. Это, в свою очередь, самый очевидный пример смены текущего объекта.
1 2 3 4 5 6 7 8 9 |
|
Текущий класс в определении метода не меняется; внутри определения метода можно определить второй метод, и при вызове первого второй станет методом того же класса:
1 2 3 4 5 6 7 8 9 10 11 |
|
Как уже говорилось чуть выше, у каждого объекта есть свой синглтон-класс - собственный класс объекта, содержащий все его синглтон-методы и наследующий от его номинального класса. Для определения синглтон-метода достаточно формы def self.method_name
, но если необходимо поработать с синглтон-классом более плотно, на помощь приходит конструкция class << object
. Как только мы попадаем внутрь нее, self
и текущий класс меняются на синглтон-класс нашего объекта.
Когда речь идет об обычном объекте, мы можем определить для него сразу несколько синглтон-методов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
В более распространенной ситуации, когда class << self
используется в определении класса, в роли объекта выступает сам класс, и мы попадаем в синглтон-класс класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Часто возникает необходимость открыть существующий объект и изменить его “внутренности” - изменить инстанс-переменные, не имеющие attr_writer
и недоступные снаружи; добавить пару синглтон-методов разом; а если объект является классом - то и инстанс-методы (синтаксис class MyClass ... end
не подходит, если название класса определяется в рантайме, и на класс просто указывает переменная). Для таких случаев есть BasicObject#instance_eval
и Module#class_eval
. Они также меняют self
и текущий класс.
instance_eval
- это интерпретация кода в контексте определенного объекта. Внутри блока self
соответствует самому объекту, а текущий класс - синглтон-классу объекта:
1 2 3 4 5 6 7 8 9 10 |
|
Впрочем, синглтон-методы обычно определяют в конструкции class << object
.
Интересное достоинство instance_eval
- в лаконичных DSL, где self
, указывающий на нужный разработчику объект, принимает на себя все вызовы методов без указания получателя. Например, так устроен механизм задания маршрутов в роутере rails - вызывается метод ApplicationName::Application.routes.draw, который передает блок методу eval_block, а тот в свою очередь запускает instance_exec
на объекте mapper
- и все методы resources
, get
, post
, member
, collection
вызываются с этим объектом в качестве получателя:
1 2 3 4 5 6 7 8 9 10 |
|
Метод class_eval
, как и его синоним module_eval
, определен в классе Module. В блоке, который передается в class_eval
, self
и текущий класс указывают на класс/модуль, на котором вызван этот метод. Фактически, это аналог повторного открытия класса, которое дает возможность обыкновенного monkeypatching: мы можем переопределять инстанс-методы класса и работать с инстанс-переменными самого класса.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Мы уже знаем, что при вызове метода он ищется по всей цепочке наследования классов, начиная с синглтон-класса объекта-получателя и до BasicObject
. Рассмотрим все случаи наследования классов:
При объявлении нового класса:
Object
При включении модуля в класс создается прокси-класс, содержащий инстанс-методы модуля, и помещается в цепочку прямо над включающим классом
Синглтон-класс обычного объекта (не класса и не модуля) наследуется от номинального класса этого объекта (класс, который возвращает object.class
)
Синглтон-класс класса наследуется от синглтон-класса родителя исходного класса (кроме синглтон-класса BasicObject
, который наследуется от Class
)
Таким образом, методы любого объекта состоят из его синглтон-методов, инстанс-методов модулей, подключенных в его синглтон-класс, инстанс-методов его номинального класса и подключенных в него модулей, потом родителя этого класса и его подключенных модулей, и так далее до BasicObject
.
При подключении в один класс нескольких модулей порядок в цепочке наследования определяется порядком подключения: подключенный последним модуль будет стоять сразу после класса, его подключившего; далее предпоследний подключенный модуль итд; т.е., например, последний модуль может переопределить метод, заданный в предпоследнем.
Классовые методы любого класса, в свою очередь, состоят из синглтон-методов этого класса и инстанс-методов модулей, подключенных в его синглтон-класс, далее синглтон-методов класса-родителя и инстанс-методов его модулей, итд до синглтон-методов BasicObject
; потом инстанс-методов классов Class
, Module
и Object
, модуля Kernel
и наконец класса BasicObject
(так как цепочка наследования синглтон-класса BasicObject
выглядит следующим образом: #BasicObject < Class < Module < Object < Kernel < BasicObject
).
Резюмируя все вышеописанное:
Любой объект содержит группу инстанс-переменных и ссылку на класс.
Любой класс или модуль содержит группу инстанс-методов, и предоставляет их всем объектам, в чьих цепочках наследования он находится; также любой класс содержит ссылку на родительский класс.
Любой объект может иметь собственный синглтон-класс, содержащий его синглтон-методы; он наследуется от номинального класса исходного объекта.
Классы и модули являются объектами, и обладают всеми свойствами объектов (имеют свой класс и полученные от него методы, также могут иметь синглтон-класс).
Любой класс имеет свою цепочку наследования - сам класс, его родительский класс, родительский класс родительского класса итд до корня классовой иерархии - класса BasicObject
.
При подключении модуля в класс он помещается в цепочку наследования прямо над этим классом.
Синглтон-класс объекта наследуется от номинального класса объекта; за исключением синглтон-классов классов, каждый из которых наследуется от синглтон-класса родителя исходного класса.
При вызове метода любого объекта этот метод ищется во всех классах цепочки наследования класса исходного объекта, начиная с синглтон-класса и заканчивая BasicObject
.
При обращении к инстанс-переменной она ищется в текущем объекте (self
); при вызове метода без указания получателя он будет вызван на текущем объекте.
При объявлении метода в обычной форме (def method_name
) он становится инстанс-методом текущего класса;
При объявлении метода с указанием получателя (def object.method_name
) он становится синглтон-методом указанного объекта.
Указатели на текущий объект (self
) и текущий класс меняются в зависимости от контекста.
Часто есть необходимость подключиться к одному и тому же серверу одновременно в нескольких окнах/вкладках консоли. ssh можно настроить так, чтобы вместо создания нового соединения к серверу использовалось уже созданное. Для этого нужно добавить следующие строчки в ~/.ssh/config (возможно, этот файл придется создать):
1 2 |
|
После этого можно попробовать подключиться к какому-нибудь серверу, и в другой вкладке терминала открыть еще одно соединение - даже если в первый раз был запрошен пароль, во второй раз соединение откроется мгновенно без запроса пароля.
Этот метод также работает со всеми утилитами, использующими ssh-соединения: scp, rsync, git итд - достаточно открыть соединение, и утилита будет его использовать. При вводе пути к файлу на удаленном сервере в scp даже будет работать tab-completion.
Во время работы часто приходится устанавливать много следующих друг за другом соединений - подключаемся, отключаемся, опять подключаемся - и так по много раз в день. OpenSSH позволяет устанавливать постоянное соединение - после окончания сессии такое соединение будет висеть в “спящем режиме” в течение указанного в конфиге времени, и будет использовано, как только будет запрошено соединение с тем же сервером. Постоянные соединения включаются одной строчкой в конфиге (в дополнение к 2 строчкам для общего соединения):
1
|
|
Это, пожалуй, самая известная возможность ssh - чтобы не было нужно вводить пароль при каждом соединении, достаточно создать на локальной машине пару ключей - публичный и приватный - и скопировать публичный на удаленную машину, к которой нужно подключаться по ssh. При этом будет запрашиваться pass phrase для ключа - но это будет происходить только при первом соединении после каждой перезагрузки локальной машины, а не при каждом соединении.
Для начала нужно сгенерировать пару ключей. Это делается командой
1
|
|
которая выдаст инструкции для дальнейших действий по генерации ключа. Нужно ввести pass phrase, чтобы приватный ключ хранился в зашифрованном виде. Потом нужно скопировать публичный ключ на удаленную машину. Во многих системах для этого есть команда ssh-copy-id
, с ее помощью ключ копируется следующим образом:
1
|
|
Если же команда ssh-copy-id
отсутствует, можно проделать эти же действия вручную:
1) найти файл с публичным ключом - ssh-keygen
должен сообщить путь к файлу, обычно это что-то вроде ~/.ssh/id_rsa.pub
2) на каждой из удаленных машин нужно поместить содержимое этого файла в ~/.ssh/authorized_keys
3) убедиться, что ~/.ssh
и ~/.ssh/authorized_keys
доступны для записи только для вашего пользователя
Например, такая команда должна выполнить эти действия:
1
|
|
Чтобы не набирать каждый раз полное название домена и имя юзера для подключения, можно задать алиас для любого хоста. Делается это следующим образом:
1 2 |
|
Похожие доменные имена можно группировать, используя подстановку с помощью %h.
1 2 |
|
Данный пример создаст алиасы dev
, intranet
и backup
для dev.internal.example.com
, intranet.internal.example.com
и backup.internal.example.com
соответственно.
Также можно использовать обычную маску - *
для любого кол-ва символов и ?
для одного любого символа:
1 2 |
|
Если имя пользователя на локальной и удаленной машинах различаются, можно также указать его в описании хоста:
1 2 3 |
|
Теперь, даже если локально нашего юзера зовут smylers
, подключение через команду
1
|
|
будет использовать пользователя simon
.
Часто доступ к определенному серверу открыт только для другой доверенной удаленной машины, в этом случае нужно сначала заходить на доверенную машину, а уже оттуда соединяться с итоговым сервером. Это также можно автоматизировать. Сначала нужно убедиться в наличии публичного ключа на всех серверах, чтобы можно было подключиться к промежуточной машине одной командой, а оттуда второй командой - к нужному серверу; так, чтобы ни одна из команд не запрашивала пароль:
1 2 |
|
Теперь в локальном shh-конфиге указываем, что для подключения к конечному хосту нужно проксирование через ssh
на промежуточном сервере:
1 2 3 |
|
После этого для подключения к конечному серверу будет достаточно следующей команды:
1
|
|
Иногда на удаленном сервере работает какая-то служба - такая, как веб-сервер, или сервер БД - и есть необходимость настроить соединение с этой машиной так, чтобы можно было вести работу с сервисом на ней через локальный порт - как если бы сервис был запущен локально. Это бывает полезно, например, когда на удаленном сервере запущен сервер PostgreSQL, настроенный на работу только с локальными соединениями.
Это можно осуществить с помощью проброса портов. Например, для работы с PostgreSQL-сервером на удаленной машине через локальный порт 5433 нужно вписать в ~/.ssh/config
следующее:
1 2 |
|
Теперь когда мы подключаемся к хосту db
, весь трафик на локальном порту 5433 перенаправляется на порт 5432 на удаленной машине - и последняя думает, что соединение приходит с localhost
.
1 2 3 |
|
Или же, допустим, у нас есть бэк-энд веб-сервер, недоступный напрямую из сети - можно направить к нему весь трафик с локального порта:
1 2 |
|
Потом подключаемся к нему:
1
|
|
И сервер отвечает на локальном порту 8080:
1
|
|
Эта статья является переводом, оригинал находится здесь. Я перевел не всю статью, а только части, показавшиеся мне наиболее интересными.
]]>В ruby, как известно, для этого существует стандартная библиотека json
. Упаковка данных в json и распаковка их обратно происходит с помощью 2 методов:
1 2 3 4 |
|
И все бы хорошо, да вот только в один прекрасный день моя коллега на своей ubuntu-машине запустила спеки, и JSON.load
почему-то повел себя как IO.load
- решил, что параметром ему подсовывают имя файла - но файла под названием {"result":true}
рядом не нашлось, и случился Errno::ENOENT: No such file or directory
Поначалу проблема казалась абсолютно мистической и необъяснимой, и JSON.load
было решено поменять во всем проекте на ActiveSupport::JSON.decode
- он работал без проблем как на убунтах коллег, так и на моем маке.
Но однажды я забыл об этой особенности, и продеплоил код на staging-сервер. Результат не заставил себя ждать - JSON.load
снова захотел получить параметром путь к некоему файлу. Можно было бы, конечно, снова заменить его на вариант из ActiveSupport, но я решил, что раз уж код работает у меня на маке, то должен работать везде (уж такие вещи, как JSON, точно не должны быть платформозависимыми).
После долгих мытарств выяснилось, что ruby необходимо скомпилировать с поддержкой iconv
. Причем речь идет именно о ruby - сам iconv был уже установлен на сервере, но интерпретатор об этом понятия не имел.
В случае использования RVM
все это сводится к 3 командам (к сожалению, включающим в себя переустановку текущей версии ruby):
1 2 3 |
|
Первая команда устанавливает iconv, причем не куда-нибудь, а в ~/.rvm/usr
; вторая сносит ruby 1.9.2 - ну а третья ставит его обратно, компилируя с поддержкой iconv. Чуть более подробно этот процесс описан здесь.
После этого JSON.load
/ JSON.dump
замечательным образом заработали.
Есть rails-приложение, построенное по принципу RIA, в котором фронт-энд логически разделен с бэк-эндом. Фронт-энду нужно знать, что происходит на серверсайде, и поэтому раз в секунду приходит запрос на некий урл, где определенный экшен определенного контроллера производит какие-то действия и рендерит ответ в формате JSON.
И среди действий этого контроллера есть обновление времени последнего доступа. Реализовано оно одним UPDATE
-запросом примерно следующего вида:
1
|
|
На первый взгляд человека, незнакомого с дедлоками, тут нет ничего потенциально проблематичного. Но, тем не менее, в логе продакшен-сервера время от времени попадаются записи вида:
1 2 3 4 5 6 7 |
|
Присмотревшись повнимательнее, можно увидеть, что оба запроса меняют записи с одними и теми же id, в одной и той же таблице - но в разном порядке.
Первый запрос меняет записи 691
, 690
, 692
и 689
; второй в то же время обновляет 686
, 687
и 688
. Далее происходит следующее: первый запрос пытается обновить запись 686
, но на нее уже установлен ShareLock вторым запросом; а второй запрос пытается изменить запись 689
, запертую первым запросом. Потом оба запроса ожидают определенное время (которое устанавливается в настройках постгреса, и по умолчанию равно 1 секунде), один запрос отваливается (вместе со своим ShareLock), а второй продолжает выполнение до победного конца.
Так как проблема проявлялась на продакшен-сервере, и была довольно критичной, нужно было найти решение в максимально сжатые сроки.
В итоге некоторого обсуждения было решено просто отсортировать id обновляемых записей в обоих запросах. Таким образом, если два запроса одновременно будут обновлять записи, даже если список id будет совпадать, один запрос начнет выполнение раньше другого, и не будет заблокирован; а второй запрос выполнится после снятия ShareLock после завершения первого запроса.
Таким образом, изменения в коде минимальны:
1 2 3 4 |
|
З.Ы. на прошлых выходных установка/настройка нового сервера не позволила мне продолжить серию статей “Краткое введение в Ruby” - она обязательно будет продолжена.
]]>Ruby предоставляет интерактивную консоль irb, в которой можно вводить код и сразу видеть результат его выполнения (аналогично js-консоли в firebug). irb устанавливается вместе с самим Ruby, поэтому будем ставить его.
Сразу обращу внимание - есть способ пропустить этот шаг, и перейти на tryruby, где аналогичная консоль (с некоторыми разумными ограничениями) доступна прямо в браузере. Для тех, кто не боится процесса установки (а, возможно, Ruby уже есть в вашей системе), этот раздел.
Честно говоря, ни разу не пробовал заниматься подобными извращениями, но говорят, что это работает (там в комплекте и Ruby, и рельсы, и еще докучи всякого небесполезного барахла).
Начиная с 10.5 (а, возможно, и с 10.4, точно не знаю) Ruby предустановлен в системе, и достаточно просто открыть Терминал.app и набрать irb
; но там предустановлена стремительно устаревающая версия 1.8, и стоит установить последнюю самому. Самый удобный способ - это использовать RVM:
1 2 3 4 5 6 7 8 9 10 11 |
|
Пользователям zsh нужно заменить .bash_profile
на .zshrc
- хотя они и так об этом знают.
При ручной установке Ruby необходимо ставить дополнительные библиотеки вроде readline
, и подключать их при компиляции интерпретатора Ruby - RVM делает это автоматически.
Ruby можно поставить из репозиториев большинства дистрибутивов, но иногда приходится подолгу ждать обновления пакета, чтобы получить свежую версию Ruby; также всегда можно собрать из исходников, но не все любят этим заниматься (особенно пользователи дистрибутивов, в которых установка пакета сводится к команде наподобие sudo apt-get install ruby
); поэтому, опять же, я предлагаю использовать RVM, описанный в предыдущем подразделе.
irb
(Interactive Ruby Shell) - это консоль Ruby. Запускается, как несложно догадаться, командой
1
|
|
Ввод любой строки кода, которая возвращает какое-то значение, повлечет за собой вывод этого значения:
1 2 |
|
(узнав об этом, вскоре я начал использовать irb как калькулятор :) так как всегда держу открытым хотя бы одно окно консоли)
В Ruby не существует жесткого разделения на числа, строки, массивы и объекты - любая переменная является ссылкой на объект. Объект, в свою очередь, всегда относится к определенному классу. А у объектов, как мы знаем из ООП, обычно есть методы.
В Ruby обращение к методу объекта осуществляется через ‘.’ (точку).
1 2 3 4 |
|
В Ruby нет такого понятия, как тип переменной; его успешно заменяет класс объекта. Например, объект “hello” будет принадлежать классу String
, а объект 3.14 - классу Float
. Рассмотрим самые важные классы, зачастую представляемые в других языках примитивами.
Строки в Ruby можно создавать несколькими способами. Самый популярный - это использование одинарных либо двойных кавычек, во втором случае происходит интерполяция переменных:
1 2 3 4 5 6 |
|
Класс String
предоставляет множество полезных методов для работы со строками. Например, для разбиения строки на части по определенному разделителю можно использовать метод split
:
1 2 3 4 |
|
Целые числа в определенном диапазоне (как правило это −230 … 230 - 1 или −262 … 262 - 1) относятся к классу Fixnum
. Более крупные числа относятся к классу Bignum
.
Для работы с числами доступна вся стандартная арифметика (+, -, *, /, %). Также целочисленные типы предоставляют ряд итераторов:
1 2 3 4 5 6 7 8 9 |
|
В Ruby для коллекций объектов существуют объекты классов Array
(массив) и Hash
(хэш). Они отличаются друг от друга индексацией элементов: в массивах объекты индексируются последовательными целыми числами, а в хэшах ключом элемента может быть любой объект.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Для обхода элементов массива или хэша используются итераторы, например each
:
1 2 3 4 5 6 7 8 |
|
Как я уже писал чуть выше, каждый объект относится к определенному классу. Узнать этот класс можно с помощью метода class
:
1 2 |
|
Попробуем написать свой элементарный класс:
1 2 3 |
|
и создать объект этого класса:
1 2 |
|
Создание объекта происходит посредством вызова метода new того класса, к которому будет относиться создаваемый объект.
Основное предназначение класса - это определение поведения его объектов, то есть методов. Напишем какой-нибудь метод:
1 2 3 4 5 6 |
|
и вызовем его, предварительно создав объект:
1 2 3 4 |
|
Наш метод просто возвращает строку “hello!”, а irb выводит результат каждой возвращающей что-либо строки кода; поэтому при вызове метода мы видим возвращаемую строку.
Тут необходимо сделать небольшое отступление - Ruby позволяет открывать уже определенные классы, и определять новые методы, так же как и переопределять уже существующие. Это дает возможность применять технику, известную как monkeypatching - изменение методов встроенных (либо добавляемых какой-нибудь сторонней библиотекой) классов, чтобы получить метод, ведущий себя немного не так, как задумывалось автором, но не переписывать для этого слишком многое.
Переменные в Ruby различаются на несколько типов по своему названию:
Блок - это кусок кода, который может иметь какие-то параметры, и возвращает определенное значение. Во многом блоки похожи на методы, но они не используются независимо - блок можно только передать методу.
При вызове метода ему можно передавать аргументы, а также блок. Для этого предусмотрено два варианта синтаксиса:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Как видно из примера, результат применения обоих вариантов одинаковый; но синтаксис с использованием фигурных скобок принято применять в случаях, когда блок содержит всего одну строку кода, а синтаксис do ... end
- для многострочных блоков.
Код, переданный в блоке, будет выполняться в зависимости от метода определенное количество раз (итераторы), при определенных условиях (условные конструкции), и не только - блоки в Ruby находят достаточно широкое применение.
Чтобы метод выполнил код блока, служит ключевое слово yield
- при выполнении метода в этом месте будет выполнен код блока:
1 2 3 4 5 6 7 |
|
Если у блока предполагаются параметры, их нужно передавать параметрами в yield
:
1 2 3 4 5 6 7 |
|
Разумеется, программирование на Ruby не ограничивается irb - можно создавать скрипты (общепринятое расширение для исходников скриптов на Ruby - .rb) и запускать из командной строки:
1 2 3 |
|
Разумеется, в одной статье невозможно охватить все грани языка, но надеюсь, что смог описать основы в относительно понятном виде. В следующей части будут описаны модули, наследование классов, примеси (mixin-ы), контроль доступа к методам (public
/protected
/private
), гибкие возможности списков аргументов у методов и, возможно, что-нибудь еще.
В конце одного рабочего дня случилась обычная и вполне штатная ситуация: коллега запушила изменения в центральный репо, я попытался сделать то же самое, на что git меня справедливо послал куда подальше.
Я всегда стараюсь поддерживать простую историю коммитов, поэтому вместо обычного merge
решил сделать rebase
своего коммита на коммиты коллеги. И, как стало ясно из последующих событий, видимо совершил свою любимую ошибку - вместо наложения своего коммита поверх чужих, наложил чужие на свой. После этого я запушил результат в центральный репо, собрал вещи и ушел домой.
На следующее утро я пришел в офис и по привычке решил сделать pull
перед началом работы, но он почему-то выдал мне коммит далеко не первой свежести, причем с пометкой (forced update)
. Не помню, зачем, но я снова сделал pull
(или fetch
) - и каково же было мое удивление, когда второй раз он выдал правильный последний коммит, сделанный мной вчера вечером!
Последующие фетчи и пуллы работали аналогичным образом, через один - то выдается forced update
со старым коммитом, то правильный HEAD
. Попытки сделать новую локальную ветку, новую локальную копию, даже новый идентичный прежнему репозиторий на сервере - ничего не принесли.
Гугл поведал мне о том, что в git есть такое нехорошее состояние, как detached HEAD - это когда на определенный коммит, или даже целую ветку коммитов, не указывает ничего (как известно, в git ветка является указателем на последний из последовательности коммитов; в данной ситуации на последний из коммитов указывает только HEAD, и только до тех пор, пока мы не сделаем checkout
другой ветки). Такая ситуация случается, в частности, при rebase
- перемещаемые поверх других коммиты остаются в репозитории в двух местах - в новом, куда их переместил rebase
(поверх других коммитов), и в старом, где они были до rebase
; и на старое место более не указывает ни один ref (ни HEAD, ни ветки, ни теги).
Такая ситуация обычно не представляет опасности - но только если исходные коммиты до rebase
не были запушены в другой репозиторий, используемый другими разработчиками; если это случилось, пушить отrebase
-нные коммиты нельзя, во всяком случае если не хочется быстрой смерти от руки коллеги :) Если в общем репозитории появятся 2 версии одного и того же коммита, и каждый разработчик будет работать со своей версией, начнется трэшак.
Так вот, после некоторого изучения ситуации выяснилось, что в серверном репозитории HEAD
указывает на последний коммит в master
, а тот указывает на правильный последний коммит. Но при этом в .git/refs/heads
помимо всех наших веток был еще и файл HEAD
, указывающий уже на тот самый коммит, который выдавался fetch
-ем через раз с пометкой (forced update)
.
Не зная внутренностей гита, поначалу я решил, что HEAD
там и должен всегда находиться; но потом посмотрел внутренности нескольких других имеющихся репозиториев, и не нашел .git/refs/heads/HEAD
ни в одном из них.
После удаления этого файла все чудесным образом заработало. Честно говоря, полное понимание того, что случилось, так до сих пор не пришло; поэтому резервная копия того файла осталась на всякий случай лежать на сервере.
P.S. Пока гуглил, нашел достаточно качественный визуальный справочник по командам git.
]]>В первой статье я расскажу о проблеме, которая встала передо мной при работе над rails-админкой для проекта, написанного на php/PostgreSQL.
Проект был в общем-то реализован до меня, и когда я пришел в команду, у него было 3 интерфейса: для пользователей, для менеджеров, и для админов. Причем, если первые два работали в любых браузерах и были выполнены практически в одном и том же дизайне, то админка работала исключительно в IE, и вроде бы использовала какой-то ActiveX (или что-то подобное, совместимое исключительно с виндой).
Я, работая на макбуке, разумеется, не имел ни малейшей возможности пользоваться этим достижением прогресса. Точнее, поначалу я ходил на офисный виндовый сервер по VNC, и там запускал горячо любимый браузер, но это был ад, и вскоре я понял, что больше так нельзя, и сел писать свою админку.
Проект разрабатывался на отдельном development-сервере, затем выкладывался на своеобразный staging-сервер, где его прорабатывал тестировщик, и смотрели (и снова тестировали) люди из компании-заказчика проекта; и уже после окончательного утверждения все переносилось на production.
У каждой среды был свой отдельный postgres-сервер, и периодически возникала необходимость смотреть и править данные на development и staging серваках. Структура БД, разумеется, была идентичной.
Взявшись за админку, я начал с описания стандартных MVC-компонентов, запуская все это добро локально в dev-режиме, нацеленным на development-базу. Когда я окончательно убедился в том, что админка не порушит ничего важного в БД, я решился запустить ее на staging - опять же, локально, при этом я вписал настройки соединения в рельсовую среду development и поочередно раскомментировал то один набор настроек, то другой. Выглядело это следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
С подобным механизмом проект прожил пару месяцев, но постоянно приходилось мучиться с раскомментированием нужных строк и закомментированием ненужных, и разумеется последующим рестартом сервера. Вскоре мне это надоело, и я начал продумывать более простой способ переключения между соединениями.
Именно этот метод отвечает в AR за установку соединения с БД. Обычно рельсы вызывают его автоматически, передавая параметром название среды, которое представлено в config/database.yml
.
Моя задача состояла в том, чтобы AR вызывал этот метод самостоятельно, и в качестве параметра передавал название нужного соединения, которое можно было бы переключать из контроллеров. В результате получился следующий initializer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
К сожалению, для поддержки работы с выбранным соединением не придумалось ничего лучше, чем вызов choose_db
при определении модели, при этом для корректной работы ассоциаций они должны браться из одной и той же базы, поэтому choose_db
должен вызываться в обеих моделях.
1 2 3 4 |
|
При этом если в системе подключена авторизация через какую-то модель (в моем случае к модели Admin
был пристегнут Devise), то в этой модели работа должна идти всегда с одной и той же БД - т.е. в этой модели вызывать choose_db
нельзя.
Соединения пришлось разбить на две группы - для рельсовой среды development и production, в каждой среде должны были работать все соединения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
Далее нужно было просто вызывать ActiveRecord::Base.set_connection
при каждом запросе, и ActiveRecord::Base.restore_connection
после отработки запроса (последний нужен был для того, чтобы отрабатывать запрос с запрошенной базой, но не менять выбранное соединение в session[:db]
- чтобы при дальнейших запросах работать уже с соединением из сессии). Это вылилось в before_filter
и after_filter
в ApplicationController
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Дальше дело оставалось за малым - был необходим контроллер, переключающий в сессии выбранное соединение, и ссылки на единственный экшен этого контроллера с именем соединения в качестве параметра:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Ну и дальше просто подвешиваем ссылки в лэйауте, где-нибудь в области шапки:
1 2 3 4 |
|
Конечно, решение получилось немного костыльным в некоторых местах, но оно позволило переключаться между БД из интерфейса самого приложения, а также появилась возможность давать ссылку на страницу, которая будет отрендерена с использованием переданного соединения, не меняя при этом session[:db]
, например http://dev.domain.tld/users/123?db=staging
.
Возможно, когда-нибудь у меня дойдут руки вычистить весь этот код, написать спеки и оформить в виде gem-а, к тому же есть пока еще нерешенные задачи из смежных областей - например, работа одновременно с несколькими схемами в postgres (имеются в виду конечно схемы самого постгреса, а не рельсовая schema.rb
). Но на данный момент при необходимости это решение вполне можно использовать.