Василий Половнёв

Ведущий разработчик.

Специализация — Рельсы, фронтенд, автоматизация тестирования и разработки.

Связаться: vasily@polovnyov.ru или телеграм.

Блог Курс о тестах в Руби и Рельсах

Ищу сообразительного стажёра

Ищу сообразительного стажёра, который вместе со мной будет делать крутые бэкендерские штуки и со временем станет крепким медлом.

Я ищу стажёра, который:

  • отличает GET от POST, SELECT от UPDATE, SSH от zsh;
  • отличает git rebase от git merge, пулреквест от реверта;
  • знаком с Рельсами на уровне «написал блог за 15 минут, а потом допиливал его по выходным»;
  • слышал про RabbitMQ, RSpec, модульные и интеграционные тесты;
  • не боится ни легаси, ни ПХП, ни фронтенда, ни асинхронных систем.

За полтора месяца научу стажёра:

  • проверять свою работу модульными и интеграционными тестами;
  • писать тесты быстрее, чем код;
  • ревьюить, парно программировать и не унывать;
  • быстро работать с Гитхабом, Рельсами и Семафором;
  • находить и исправлять «запахи» в коде: от Long Method до Shotgun Surgery.

Для этого стажёр будет:

  • работать 15-20 часов в неделю, часть времени — в паре со мной;
  • тратить 2-3 часа в неделю на теорию и домашние задания;
  • переписывать код снова и снова после ревью.

После стажировки мы либо продолжим работать вместе, либо стажер уйдёт с планом роста и списком рекомендуемой литературы.

Если написанное выше про вас, напишите мне письмо с рассказом о себе: vasily@polovnyov.ru

3д-печать и ФФФ

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

У меня фотополимерный 3д-принтер. Он печатает с помощью фотополимерной смолы. Смола по умолчанию жидкая, но если посветить на нее особой длиной волны, затвердевает. Принтер печатает модельку слой за слоем, «запекая» их на платформе. Я наливаю смолу в ванночку с прозрачным дном, принтер опускает в нее платформу и засвечивает через дно текущий слой. Слой затвердевает и прилипает к платформе. Принтер поднимает платформу, отрывая запекшийся слой, и повторяет процесс для следующих слоев.

У такого способа печати бывают проблемы. Скажем, если слой слишком большой, принтер не сможет его оторвать и он останется на дне ванночки, похерив все последующие слои. Если слою не к чему прилепиться, если нет подпорок, он отвалится, станет плавать в ванночке и портить последующие слои.

При этом цена ошибок высока. Скажем, котик печатается 12 часов. Если вовремя не заметить проблему, легко похерить смолу и 12 часов печати. Еще и принтер придется «сбрасывать»: очищать ванночку, убирать с нее проблемный слой, фильтровать остатки смолы и все чистить.

Чтобы вовремя замечать проблемы и не терять время, я использую «Гвозди в гусенице»:

  1. Ставлю котика на печать
  2. Прошу Сири напомнить мне проверить котика через час
  3. Когда час прошел, должно быть готово около сантиметра модельки. Ставлю принтер на паузу, поднимаю платформу и изучаю результат. Если все в порядке, продолжаю печать. Если нет, останавливаю печать, переделываю модель и ставлю на печать новую ревизию
  4. Прошу Сири напомнить мне проверить котика через 5 часов
  5. Когда прошло еще 5 часов, должна быть готова половина модельки. Ставлю принтер на паузу, поднимаю платформу и изучаю результат. Если все в порядке, продолжаю печать. Если нет, останавливаю печать, переделываю модель и ставлю на печать новую ревизию

90% проблем ловятся на первом гвозде. Если первые слои не смогли закрепиться, то и печатать дальше нет смысла, поэтому первый гвоздь — как можно ближе к началу. Оставшиеся проблемы ловятся на втором гвозде, половине модельки.

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

Гвозди работают везде. Поручили младшему разработчику задачу на 5 дней, договоритесь о гвоздях. Через день проверьте направление мысли и модель решения задачи, через 3 дня — готовность половины работы. Если что-то пойдет не так, вовремя заметите и скорректируете.

Баготерапия

Мне тяжело неделями пилить новые фичи. Фичи связаны с неопределенностью — мы не совсем понимаем, куда идем, что нас ждет впереди, что мы забыли или не учли. Короче, очень много энергии уходит на ДУМАНИЯ. Если пилить новые фичи 6 недель подряд, можно здорово выгореть.

С другой стороны, мне легко исправлять баги. Баги тоже связаны с неопределенностью, но другого вида — мы не понимаем их причину. Зато мы знаем, как работает сейчас и как должно работать. Бац, бац и багфикс готов. Еще полчаса и он уже в продакшене. Красота!

Поэтому я предпочитаю чередовать фичи и баги. Когда чувствую, что не могу больше пилить новое, переключаюсь на баги и отдыхаю. Это работает на уровне итераций и ежедневной работы. В первом случае после 6 недель работы над новыми фичами, мы планируем разгрузочную неделю на рефакторинг и багфиксы. Во втором случае я каждый день беру баг-два, чтобы передохнуть. Периодизация!

Конечно, это не касается гейзенбагов, пожирающих твою душу. Их нужно планировать, отслеживать гипотезы и планомерно исследовать день за днем. Их лучше запихивать «продуктовым налогом» в ближайшие итерации.

Дефолтные стили браузеров

Если сделать текстовый файлик, написать в него немного текста и тегов и открыть в браузере, то текст все равно будет стилизован. Индексы работают, задан какой-то кегль и интерлиньяж.

Так происходит потому, что у браузеров есть user agent stylesheets — цсс-стили, встроенные в сам браузер. То есть это не какое-то дефолтное поведение отдельных текстовых нод, а буквально цсс-файл, который браузер подключает в первую очередь ко всем страницам. И конечно, именно с этих стилей начинается каскад: стили браузера → стили страницы → стили пользователя.

Такое поведение — часть спецификации CSS2:

User agent: Conforming user agents must apply a default style sheet (or behave as if they did). A user agent’s default style sheet should present the elements of the document language in ways that satisfy general presentation expectations for the document language (e.g., for visual browsers, the EM element in HTML is presented using an italic font). See A sample style sheet for HTML for a recommended default style sheet for HTML documents.

И конечно, эти стили можно посмотреть: опен-сорс, все дела. Например, Хром:
https://chromium.googlesource.com/chromium/blink/+/master/Source/core/css/html.css

Сафари:
https://trac.webkit.org/browser/trunk/Source/WebCore/css/html.css

Фаерфокс:
https://searchfox.org/mozilla-central/source/layout/style/res/html.css

Эти стили стоит глянуть, чтобы офигеть (1000 строк!), лучше понять ЦСС (head { display: none }) и подсмотреть прикольные трюки.

Просто не делай херни

Бывает, в сложных и проблемных ситуациях хочется просто забить и ультануть: внезапно уволиться с работы и стать плотником, бросить семью и уйти в ЧВК, написать клиенту, что умерла бабушка, и пропасть, пушнуть в мастер с форсом. В таких случаях ты получаешь мгновенное облегчение, а с другой стороны, просто сваливаешь, вывалив ушат говна на тех, кто на тебя надеялся. Совсем не круто.

В последний год я научился справляться с такими желаниями с помощью мантры «Просто не делай херни и…». Жопа на работе — просто не делай херни и подумай, как разгрести. Жопа в семье — просто не делай херни и подумай, как восстановить отношения. Не успеваешь с проектом — просто не делай херни и поговори с клиентом. Хочешь форспушнуть в мастер — просто не делай херни и разрезолви конфликт. Короче, просто не делай херни.

Знающие люди подсказывают, что это называется «Профессиональное самоубийство».

Как конкретно разгребать ситуации, кажущиеся неподъемными и ужасными — тема отдельного поста. Если коротко, то нужны две вещи: уверенность в том, что проблемы решаются делом, и «паззлы».

Датчик есть, но его как бы нет

В ноябре я попал в жесткое ДТП и серьезно поехал кукухой по безопасности. Исключением не стал и дом: я накупил огнетушителей, детекторов дыма и датчиков протечки. Один из датчиков бросил под ванну, убедился, что он работает, и забыл.

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

Так и в программировании. Мониторим регулярность бэкапов, а когда приходится что-то достать из бэкапа, оказывается, что дампы битые и не восстанавливаются. Проверяем очередь платежей, отправляем алерты в Sentry, а они не доходят до девопсов: интеграция с PagerDuty отвалилась. Мониторим доступность сайта, а форма, через которую клиенты записываются на пробные занятия, сломалась из-за ошибки в скрипте на странице: сайт работает, форма есть, денег нет.

Что с этим можно поделать? Во-первых, убедиться, что датчики проверяют то, что нужно. Во-вторых, убедиться, что датчиков хватает. В-третьих, ввести регулярные «проверки связи» и sanity checks.

Большая маленькая ложь

Самая большая ложь в разработке — фраза «сделаем когда-нибудь». Конечно, никто ничего никогда не сделает. Это прекрасная двойная ложь: врем себе и остальным, будто задача важна, и врем себе, что обязательно до нее доберемся.

Если у задачи нет дедлайна, считайте, что и задачи нет. Поэтому любое «сделаем когда-нибудь» нужно либо фиксировать дедлайном, либо отпускать с миром: значит, не очень-то оно нам и нужно.

P. S. Даже если добавить «после пуска», «после майских» или «после отпусков», ситуация не изменится: никто ничего не сделает, все забудут.

Карма тестировщика

Бывают люди с кармой тестировщика: что бы они не использовали, находят самые невероятные баги, заусенцы и нестыковки. Взялся пинговать ya.ru, сломал ping ¯\_(ツ)\_/¯. Самые суровые из них одарены еще и бесконечной энергией: 5 багрепортов за 10 минут для них не проблема.

Конечно, когда я был начинающим программистом, я всегда их недолюбливал: ну емана, неужели тяжело НОРМАЛЬНО пинговать? Зачем это исправлять, почему нельзя последовательно нажать A, B и C, но с минимальной задержкой в 300 мс?

Со временем до меня дошло, что такие люди — дар, а не проблема. Как программисты мы склонны использовать продукты так, как это делали бы другие программы: в строгом соответствии с инструкциями и командами. Ребята с кармой тестировщика помогают делать отличные продукты, которые учитывают человеческие слабости, а не бьют за них по рукам. Короче, если среди ваших коллег есть ребята с кармой тестировщика, обнимайте их и почаще слушайте.

irb: echo on assignment

Недавно заметил, что при запуске рельсовой консоли пропало «эхо» при присвоении. Раньше было так:

> subscription = Subscription.find(123)
=>
#<Subscription:0x00007fe17d6b6e38
 id: 123,
 product_id: "xxx",
 status: "active">

А стало так (некруто):

> subscription = Subscription.find(123)
=>
#<Subscription:0x00007fb3f4d06588
...

Полез разбираться и узнал пару интересных вещей. Во-первых, с марта railties бандлит irb.

Это нужно для того, чтобы ребята со старым Руби все равно получили новый irb вместо «системного» старья.

Во-вторых, новый irb по умолчанию обрезает «эхо» при присвоении:

В-третьих, чтобы вернуть «эхо», достаточно подкрутить конфигурацию irb в ~/.irbrc:

IRB.conf[:ECHO_ON_ASSIGNMENT] = true

Там же можно вырубить всратое автодополнение:

IRB.conf[:USE_AUTOCOMPLETE] = false

Как тестировать код, завязанный на рандом

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

class Banner
  attr_reader :probability

  def initialize(probability:)
    @probability = probability
  end

  def visible?
    probability > Kernel.rand
  end
end

Чтобы проверить visible?, можно провести достаточно большое количество испытаний и оценить получившийся процент появлений банера:

banner = described_class.new(probability: 0.7)

# Проводим 1000 испытаний, результаты — в массив
trials = Array.new(1000) { banner.visible? }
# Сколько раз visible? вернул true
successful_trials = trials.select { |trial| trial }
# Какой процент
successful_trials_percentage = successful_trials.length.to_f / trials.length.to_f

# Проверяем получившийся процент
expect(successful_trials_percentage).to be_within(0.05).of(0.7)

Этот вариант кажется логичным и математичным. Но с ним есть проблемы: «мигания» и производительность. Тест будет иногда падать в зависимости от количества испытаний: чем меньше испытаний, тем чаще. Чтобы сделать тест стабильным, придётся увеличить количество испытаний и завернуть тест в rspec-retry.

С другой стороны, можно избавиться от рандома. Например, застабив Kernel#rand:

allow(Kernel).to receive(:rand).and_return(0.25)

banner = described_class.new(probability: 0.7)

expect(banner).to be_visible

Или зафиксировав сид:

# Осторожно, магия!
# rand с таким сидом первым числом возвращает 0.37
srand(42)

banner = described_class.new(probability: 0.7)

expect(banner).to be_visible

Ещё по теме:

Лайфхак: процедура завершения работы

Украл лайфхак, чтобы снизить тревогу после работы. Ну вы знаете: рабочий день давно закончился, пора бы уснуть, а ты лежишь в кровати до 2-3 ночи и стрессуешь, бесконечно возвращаясь к работе и задачам. А вот надо было Мише вот так ответить. Блин, на что переходить с Яндекс.Почты? Или лучше заплатить им? Но мы не ведем переговоры с террористами! А когда надо счётчики на воду поверить? Как в билинге учесть, что подарков может быть несколько? А если ещё и одновременно? Зачем я вообще пошёл в программисты?

Чтобы избавиться от этого и нормально спать, я использую процедуру завершения работы:

1. Разбираю инбоксы — почту, телегу и inbox.txt — и распихиваю всё по задачам.

2. Смотрю список дел на сегодня. Всё, что не успел, переношу на завтра или выкидываю.

3. Смотрю список дел на завтра и остаток недели.

4. Разбираю незакрытые вопросы в голове: вытаскиваю в задачи или заметку с идеями «на подумать».

5. Говорю себе: «Теперь питание компьютера можно отключить».

Важный момент: фразу можно использовать любую, главное, доверять ей. Если сказал, значит, всё: рабочий день закончен, всё аккуратно учтено, рассортировано и записано, можно отдыхать.

Мне помогло, попробуйте и вы.

RSpec: to be_within().of()

Как вы прекрасно знаете, в комплуктерах 0,1 + 0,2 ≠ 0,3. Аналогичные проблемы есть и в любых других вычислениях, связанных с дробями (считай, с числами с плавающей запятой). Поэтому в тестах никогда нельзя напрямую сравнивать дробные результаты вычислений.

Используйте be_within(delta).of(expected) — матчер, проверяющий, что полученное число находится в окрестности радиусом delta от нужного:

expect(score).to be_within(0.0001).of(2.19)

Аналогичная история и со временем: в Руби Time имеет точность до наносекунд, а БД — до микросекунд или секунд. В таких случаях приходится сравнивать приведением к юникстайму:

expect(deadline.to_i).to eq Time.new(2022, 7, 7).to_i

Или тем же матчером:

expect(deadline).to be_within(1.second).of(Time.new(2022, 7, 7))

И конечно, если работаете с деньгами, берите BigDecimal.

SPA на Реакте без Вебпака

В конце сентября у меня резко ухудшилось зрение в правом глазу: даже верхнюю строчку таблицы Сивцева видел с трудом. Врачи установили ложную близорукость, назначили лекарства, ограничение зрительных нагрузок (ха!) и гимнастику для глаз «по Аветисову». Конечно, ни одно из приложений с гимнастикой для глаз мне не понравилось, и я написал свое. Чтобы было интереснее, добавил ограничений: Реакт, одна страница и никакого Вебпака.

Оказалось, это ограничение решается довольно легко. Все можно сделать на клиенте:

1. Берем react и react-dom с CDN:

<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

2. Берем babel с CDN:

<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

3. Добавляем рутовый элемент, в который отрендерим приложение:

<div id="root"></div>

4. Добавляем <script> в конец body, который babel скомпилирует в рантайме:

<script type="text/babel">

4. Пишем приложение внутри этого скрипта:

const App = (<h1>Ola!</h1>)

ReactDOM.render(<App />, document.getElementById('root'))

5. Вы великолепны!

RSpec: skip и pending

Ситуация: пришли дизайнеры, говорят, СРОЧНО меняем механизм рекомендаций к статье. Вы меняете код, запускаете тесты, получаете кучу ошибок: рекомендации теперь другие, результаты не соответствуют действительности. Дизайнеры и маркетологи стоят у вас над душой и просят выкатить все НЕМЕДЛЕННО. Что делать с «битыми» тестами?

В RSpec есть пара тегов для «пропуска» битых тестов: skip и pending. Добавляем тег к context, describe или it и указываем причину:

it "returns posts with tags commonly used together", skip: "Broken in #45"

it "returns posts with tags commonly used together", pending: "Blocked by #46"

skip и pending отличаются по смыслу: skip отмечает тесты, которые пропускаем по какой-то причине; pending отмечает тесты, которые ждут какого-то события, чтобы заработать. Соответственно, и ведут себя по-разному: skip пропускается, а pending выполняется. Если тесты в pending пройдут, RSpec свалится с ошибкой:

1) returns posts with tags commonly used together FIXED
     Expected pending 'Blocked by #46' to fail. No error was raised.

Короче, если на собеседовании вас спросят, чем отличается skip от pending, смело отвечайте: pending может завалить тесты, а skip — нет.

SSRF

Недавно я узнал об интересном типе атак на веб-приложения: SSRF — server-side request forgery. Идея простая: если ваше приложение принимает ссылки от пользователей, что-то по ним скачивает и отображает, то злоумышленник может скормить ссылку, например, на приватный ресурс в локалке, и получить доступ к данным. Например, в AWS можно использовать http://169.254.169.254/latest/meta-data/, чтобы получить креденшиалы.

Соответственно, уязвимы все места, в которых приложение что-то загружает по ссылке. Если вы пользуетесь готовыми библиотеками, то, скорее всего, в них уже встроена защита от SSRF. Например, Carrierwave использует для этого ssrf_filter.

Чтобы погрузиться в потенциальные варианты атак с помощью SSRF, взгляните на исходники ssrf_filter: https://github.com/arkadiyt/ssrf_filter/blob/main/lib/ssrf_filter/ssrf_filter.rb

Тут всё: локальные айпишники, ipv6, левые URI, бесконечные редиректы, CRLF инъекции, ДНС и публичные адреса.

Range в ActiveRecord

Полюбил открытые и закрытые интервалы в запросах в ActiveRecord. Было:

Charge.where("created_at >= ?", 5.minutes.ago)

Subscription.where("valid_until <= ?", Time.now)

User.where("created_at >= ? and created_at <= ?", 2.weeks.ago, 1.week.ago)

Стало:

Charge.where(created_at: 5.minutes.ago..)

Subscription.where(valid_until: ..Time.now)

User.where(created_at: (2.weeks.ago..1.week.ago))

Не надо везде лепить восклицательные знаки!

Вижу такое:

def finish!
  update(status: :finished)
end

В Руби принято добавлять восклицательный знак к «небезопасным» методам, которые модифицируют оригинальный объект, а не возращают новый:

email = "foo@bar.com"

email.gsub(/foo/, "baz") # вернет новую строку, email останется тем же
email.gsub!(/foo/, "baz") # поменяет email и вернет его

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

user.save # при ошибках вернет false
user.save! # при ошибках выкинет ActiveRecord::RecordInvalid

Оттого, что мы добавили восклицательный знак к finish, лучше АПИ не стало. Наоборот, мы запутали сами себя: безопасного finish не существует, а finish! вряд ли выкинет исключение. Соответственно, метод лучше переименовать тупо в finish.

Планирование преемственности

Читаю сейчас «Элегантный пазл» Уила Ларсона — книгу о системном подходе в управлении и развитии инженерной части организации. Наткнулся на интересное упражнение, succession planning, планирование преемественности.

Идея простая: чтобы перейти на следующую позицию, нужно прикинуть, как команда будет работать без тебя, чего для этого не хватает и закрыть «дыры». Алгоритм простой:

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

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

3. Взять в работу и распланировать на год весь список «прямо сейчас» и одну-две рискованных штуки.

4. Повторить упражнение через год.

yakuake в iTerm

Когда я пользовался Линуксом, обожал yakuake — консоль, которая как в Квейке по хоткею выезжает сверху поверх всего остального. На Маке такого не хватало: иногда хочется быстро что-то ввести в терминал, посмотреть результат и забыть, а приходится запускать или искать терминал, переключаться.

Оказывается, iTerm из коробки умеет выезжать сверху. Ребята спрятали эту фичу здесь: Preferences — Keys — Hotkey — Create a Dedicated Hotkey Window…

Мониторинг ключевых метрик приложения на Рельсах в Графане

Иногда у приложения есть какие-то ключевые метрики, которые могут триггерить алерты, ахтунги и жопы. Например, в условном биллинге это может быть процент неудачных списаний за последний час. Если он высокий, скажем, более 75%, пора заводить инцидент и вызванивать инженеров. Самый толковый способ мониторить такое в приложениях на Рельсах — экспорт ключевой метрики в Графану через Прометеус и Ябеду.

Вот как-то так:

require "yabeda"

Yabeda.configure do
  # Где мы
  group :billing

  # Что мы экспортируем
  gauge :failed_charges_percentage, tags: [], comment: "The percent of failed charges in last hour"

  # Как собираем данные
  collect do
    failed_charges_percentage.set({}, ChargeMetrics.failed_charges_percentage)
  end
end