Hub и Git

January 15, 2015

Привет! Легкая посленовогодняя разминка для лица:

:expressionless: :open_mouth: :expressionless: :flushed: :expressionless: :stuck_out_tongue_closed_eyes: :expressionless: :yum: Yo! Теперь хорошо, можно работать дальше.

25 декабря прошлого года я описал свой процесс отправки пул реквестов и буквально на следующий день наткнулся на утилиту hub - makes git better with GitHub. С ее помощью (и кстати и без ее помощи) можно сделать процесс контрибьюторства совсем приятным.

Более правильный процесс

1 - Клонируем оригинальный репозитарий (лучше через протокол git:// - Гитхаб не разрешает пушить через него и поэтому вы случайно не отправите код, даже если у вас есть права на запись в репозитарий awesome/awesome)

git clone git://github.com/awesome/awesome.git

2 - Подцепляем наш форк через дополнительный ремоут (вот здесь пригодиться hub)

hub fork

3 - Пушим ветки в ориджин username

git checkout -b new-feature
...
git push avakhov new-feature -u

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

На работе мы отправляем пул реквесты через ветки, без форков

В это случае полезны три алиаса, которыми я пользуюсь регулярно:

# ~/.gitconfig
[user]
  name = Alexey Vakhov
  email = vakhov@gmail.com
[alias]
  o = !hub browse # раньше здесь был замысловатый код,
                  # пока я не знал про hub
  pushup = !git push origin `git symbolic-ref HEAD --short` -u
  cleanup = !git checkout master && git pull --rebase && \
    git remote prune origin && git branch --merged | \
    grep -v "\\*" | xargs -n 1 git branch -d

git o - открыть страницу репозитария.

git pushup - отправить текущую, свежесозданную ветку в origin.

git cleanup - почистить локальный репозитарий от разных старых, уже смерженных в мастер веток.

Year += 1

December 29, 2014
year += 1
puts "Happy new #{year} year!"

Дорогие читатели, поздравляю с Новым Годом! Желаю вам в новом году рейлс 4.2 и руби 2.2! Сезон блоггинга-2014 объявляется закрытым, до новых встреч в следующем году!

:evergreen_tree: :wine_glass: :poultry_leg: :sake: :cocktail: :clap: :sleeping:

Пул реквесты в опен сорс

December 25, 2014

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

  1. Fork it ( https://github.com/[my-github-username]/awesome-gem/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

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

Процесс создания пул реквестов

1. Форкаем репозитарий и клонируем

git clone git@github.com:avakhov/awesome-gem.git

2. Подцепляем оригинальный remote (я его называю papa)

git remote add papa git@github.com:awesome/awesome-gem.git

3. Берем самую свежую версию

git fetch papa
get checkout papa/master

4. Создаем ветку (можно начинать имя с чисел по порядку)

git checkout -b 2-mega-fix
git push origin 2-mega-fix -u

5. Комитим

git commit -m"Msg"
git push

6. Посылаем пул реквест через Github! :beer: :beer: :beer:

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

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

Добротный мультиплексор

December 22, 2014

Думаю большинство знает и использует замечательную "свободную консольную утилиту-мультиплексор", как дружелюбно описывает GNU Screen википедия , но я все же скажу пару слов.

Если вам нужно выполнить большой и важный скрипт на несколько часов, запустите этот скрипт на сервере в сессии GNU Screen, который наверняка уже установлен.

  • screen - новый сеанс
  • screen -rd - присоединиться к старой сессии
  • Ctrl-a c - создать новое окно (уже находясь внутри screen)
  • Ctrl-a n - перейти на следующее окно
  • Ctrl-a d - отсоединиться от сессии (оставив ее работать)

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

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

Удаляем записи из базы данных

December 18, 2014

Если база маленькая её можно и даже, думаю, нужно, считать той штукой, которая выполняет Active Record запросы. Когда записей уже миллионы, начинаются разные интересные задачи.

Как то раз я расщеплял базу на две и нужно было удалить примерно 10 миллионов детей, которые остались без родителей (имена таблиц - выдуманные).

Иван Евтухович (я всегда звоню ему с техническими вопросами, Иван знает все, особенно про PostgreSQL) сказал, что можно конечно удалить в лоб, в одной транзакции, но она будет работать долго и молча. Лучше положить id во временную таблицу и удалять записи небольшими порциями, не теряя лица.

Играем с тестовой базой

Заполняем базу крайне синтетическими данными на 20M записей :

# ...
10_000.times do
  parent = Parent.create!
  values = ["(#{parent.id})"]*1000
  sql = "INSERT INTO children (parent_id) VALUES #{values.join(", ")};"
  ActiveRecord::Base.connection.execute(sql)
  puts "Parent ##{parent.id}"
end
10_000.times do |i|
  values = ["(0)"]*1000
  sql = "INSERT INTO children (parent_id) VALUES #{values.join(", ")};"
  ActiveRecord::Base.connection.execute(sql)
  puts "Chunk ##{i}"
end

Удаляем одним запросом:

DELETE FROM children
WHERE id IN (
  SELECT children.id
  FROM children LEFT JOIN parents ON children.parent_id = parents.id
  WHERE parents.id IS NULL
);
-- => DELETE 10000000
-- => Time: 7538596.724 ms (~ 2 hours)

Удаляем порциями по 1000 записей :

# ...
unless ActiveRecord::Base.connection.table_exists?("deleting_ids")
  m1 = Benchmark.measure {
    ActiveRecord::Base.connection.execute <<-SQL
      BEGIN;

      CREATE TABLE deleting_ids(id integer);
      CREATE INDEX ON deleting_ids(id);

      INSERT INTO deleting_ids
      SELECT children.id
      FROM children LEFT JOIN parents ON children.parent_id = parents.id
      WHERE parents.id IS NULL;

      COMMIT;
    SQL
  }
end

m2 = Benchmark.measure {
  index = 0
  while DeletingId.first
    ids = DeletingId.limit(1000).pluck(:id)
    Child.delete_all(id: ids)
    DeletingId.delete_all(id: ids)
    puts "Chunk ##{index += 1} processed"
  end
}
# => (112.227890)   (~ 2 mins)
# => (1052.880441)  (~ 17 mins)

Выводы

В одной транзакции 10М записей удалялось 2 часа, по частям - 20 минут (эти времена некорректно сравнивать, так как многое зависит от настроек базы, данных, нагрузки и так далее).

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

Сравниваем таймстемпы

December 15, 2014

Как то раз я нашел ошибку в сервисе Batsd, написанным Noah Lorang из компании тогда еще 37signals. И очень обрадовался, найти у ошибку у таких парней - очень почетно.

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

        File.open(filename, 'r') do |file| 
          while (line = file.gets)
            ts, value = line.split
            if ts >= start_ts && ts <= end_ts    # <--- ЗДЕСЬ!
              datapoints << {timestamp: ts.to_i, value: value.to_f}
            end
          end
          file.close
        end

Вот оно! - подумал я, но оказалось было рано праздновать победу.

irb(main):004:0> [Time.now, Time.now.to_i]
=> [2014-12-15 13:10:19 +0300, 1418638219]

irb(main):002:0> Time.at(1_000_000_000)
=> 2001-09-09 05:46:40 +0400

irb(main):003:0> Time.at(9_999_999_999)
=> 2286-11-20 20:46:39 +0300

Сейчас таймстемп 10-ти значный и даты, начиная c конца 2001 года, можно безболезненно сравнивать как строки.

Это конечно хак, но хак забавный.

React.js + jQuery UI Sortable

December 11, 2014

В React.js прекрасно все, кроме поддержки драгов из коробки, в частности сортируемого списка. В интернете в основном предлагают вручную обрабатывать события мышки. Однако сделать корректную сортировку длинного списка с учетом скроллов и браузеров довольно сложно, поэтому я прикрутил jQuery UI Sortable, не нарушая реактивности системы.

Воспользуемся генератором вкусных рецептов из старого выпуска и добавим возможность менять шаги в рецепте с помощью перетаскивания.

С React.js даже самый минимальный код все равно довольно большой, поэтому дам ссылки на результат и немного поясню изменения:

За таскание отвечает класс Sortable, в который передается функция, рисующая шаг рецепта. Sortable отрисовывает список и навешивает jQuery UI Sortable на элементы. В колбеке stop мы вызываем sortable("cancel") чтобы отменить результат переноса (это очень важно, так как если мы будем вручную менять DOM, то React.js будет ругаться). Далее мы рапортуем главному приложение о изменении состояния, и он уже сам обновляет список через реактивность.

Я постарался изобразить процесс на диаграмме (думаю совершил 33 ошибки в UML, но мне хотелось опробовать сервис draw.io):

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

Пару руби-мелочей

December 8, 2014

Накопилось пару мелочей, о которых хочу сегодня написать.

Array#to_h

Ровно сто тысяч раз я писал примерно так:

ages = Hash[ users.map { |user|
  [user.name, user.age]
}]

Боги увидели мои страдания и послали человека с именем Marc-André Lafortune, который добавил в транк метод #to_h около года назад. Изменения появились в руби 2.1, я все время забываю, что этим уже можно и нужно пользоваться:

ages = users.map { |user|
  [user.name, user.age]
}.to_h

Интерполяция без {}

Где-то в исходниках я увидел как выводять пид процесса

puts "PID: #$$"

"Что же это такое?" - подумал я. Я знаю только только вариант puts "PID: #{$$}". Оказываетcя в руби глобальные переменные можно интерполировать без кавычек:

$name = "Alexey"
puts "I'm #{$name}"   # => I'm Alexey
puts "I'm #$name"     # => I'm Alexey

:fire: :fire:

Require Local File

December 4, 2014

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

Допустим у нас есть утилитка, которая выросла в размерах и мы разнесли ее по разным файлам.

# some.rb
module Some
  # ...
end

# main.rb
require 'some'                                # 1. никогда
require './some'                              # 2. иногда
require File.expand_path('../some', __FILE__) # 3. всегда

1-й вариант не будет работать совсем, так как руби не будет искать файл локально, во 2-м варианте можно запускать скрипт только из директории, где расположен main.rb, 3-й вариант работает всегда.

В исходниках я встречал конструкцию $: << '.', но не использовал из-за смешного вида. Но даже смешному коду нужно дать шанс. Код ниже будет запускать только из текущей директории:

$: << '.'
require 'some' 

expand_path будет работать всегда, правда в живой природе не такого не видел:

$: << File.expand_path('..', __FILE__)
require 'some' 

Почему же хочется запускать скрипты из другой директории? Рассказываю. Например во многих репозитариях у нас есть папка custom с разными одноразовыми скриптами, которые выкинуть жалко. Из Рейлс мы запускаем их через rails runner ./custom/some-code.rb, чтобы была вся привычная магия и можно было бы вообще не думать. Но если репозитарий не рейлсовый, то удобно набрать в корневой папке проекта команду ruby ./custom/some-tricky-file.rb.

Кейс конечно редкий, но реальный. Поэтому, пожалуй, продолжу и дальше использовать require File.expand_path('../some', __FILE__).

Как получить ошибку 500 на продакшене

December 1, 2014

Когда к новому проекту подключаешь трекер ошибок, Airbrake или Honeybadger (кстати говорят, что можно еще Skylight или Opbeat, но я не пробовал), хочется проверить, что все настроено правильно.

Можно дождаться когда кто-нибудь внесет в продакшен дефект, но я обычно добавляю какой-нибудь секретный раут, вызывающий 500:

# config/routes.rb
get 'some_500_secret_url', to: proc { raise '500' }

На самом деле к рауте привязано полноценное rack-приложение, которое по канонам должно выглядеть хотя бы так:

get 'some_500_secret_url', to: proc { |env|
  [200, {'Content-Type' => 'text/html'}, ['Hello 500']]
}

Но для тестирование работы эксепшенов достаточно просто бросить рантайм-ошибку.

Теперь можно вызывать 500 на продакшене столько раз, сколько необходимо.

Update: Алексей показал как вызвать 500 на любом урле Рейлс-сайта, для этого достаточно добавить ?%28t%B3odei%29 в конец. Я проверил, получается ArgumentError: invalid byte sequence in UTF-8, очень забавно, на вызов Ктулху похоже :-)