Читать в телеге. Когда-то там были посты не только от меня.
Пирамида код-ревью
В продолжение темы про код-ревью — шпаргалка про то, чему стоит больше уделять внимание.
Отмечу, что она ничего не говорит про порядок проверки. И, разумеется, работает только при налаженном процессе: если кто-то хреначит коммиты в основную ветку без CI (потому что “знает, как надо”), а документации нет (даже в формате OpenAPI), то говорить тут о качестве ревью рано. Кроме того, тут еще не покрыт вопрос качества требований и их осмысления/анализа.
Миграция очередей в RabbitMQ
Как известно, в RabbitMQ не все настройки очереди можно изменить после ее создания. Но что делать, если все-таки понадобилось их поменять — например, если были изменены настройки TTL или добавлена dlq?
Одно из решений — делать все на стороне клиента или на уровне сообщений, чтобы самой очереди вообще не нужны были настройки. Но это скорее велосипед, который чреват багами (если этот велосипед пишете вы).
Другое “простое” решение — временно остановить работу с очередью, потом куда-то временно переместить данные и пересоздать ее. Увы, тут страдает доступность приложения и можно потерять данные.
Более продвинутые варианты вертятся вокруг создания новой версии очереди — в новом vhost, exchange или просто с новым именем. Зависит от потребителей и схемы релиза, но общая идея одинаковая — создать новую очередь, переключить все на нее, переместить все сообщения из старой (с помощью Shovel, например) и удалить ее.
В моем текущем проекте используется версионирование exchange и это отражается еще в имени очереди. Если надо поменять настройки, то перед релизом увеличивается версия настроек, создается новый exchange и очереди. После процедуры релиза запускается утилита, которая создает shovel’ы для переноса данных из очередей “неправильных” версий, ждет их завершения и удаляет старое. Что не удалилось — можно посмотреть ответственному за релиз.
Дополнительный плюс такой схемы — новые инстансы производят и потребляют сообщения только в своем контуре, который не как не связан со старыми инстансами.
Keep alive для ssh
Подключаешься такой по ssh
к серваку, отвлекся на пару минут, возвращаешься в консольку с ssh
, а она уже на ввод не реагирует. Бесит жутко.
Чтобы такого не было, можно настроить keep alive на сервере (но кто ж вам даст) или у себя в ~/.ssh/config
:
Host *
ServerAliveInterval 200
Почему это не включено по умолчанию — загадка :(
Dead letter queue для очередей RabbitMQ
Простейший вариант обработки сообщений из очереди — слать ack
после успешной обработки и nack
с requeue=true
при временной неудаче.
Однако такой подход работает только для временных/разовых ошибок, при серьезной проблеме потребитель будет пытаться обработать сообщение до посинения с маленьким интервалом между попытками.
Чтобы этого избежать, можно создать для таких сообщений специальную очередь — dead letter queue (dlq) и для серьезных ошибок слать nack
с requeue=false
.
А чтобы сделать несколько попыток, для каждого сообщения в dlq можно назначить TTL, и установить в качестве dlq для dlq оригинальную очередь.
Тогда при истечении TTL сообщение из DLQ попадает обратно в оригинальную очередь с заголовком, содержащим число “смертей”.
Потребитель может проверять это число и, если оно превысило предел, не обрабатывать сообщение совсем, но послать ack
.
Таким образом, TTL для dlq служит интервалом между попытками.
Если в сообщениях есть ценная информация и выкидывать их совсем нежелательно, то при превышении числа попыток потребитель может их складывать в третью очередь (parking lot) для последующего ручного исправления.
С точки зрения настроек RabbitMQ нужно добавить к параметрам оригинальной очереди
x-dead-letter-exchange = ""
x-dead-letter-routing-key = "dlq_queue_name"
а к параметрам dlq
x-dead-letter-exchange = ""
x-dead-letter-routing-key = "original_queue_name"
x-message-ttl = 100500 # milliseconds
В обоих случаях используется стандартный обменник, чтобы перенаправлять сообщения напрямую. TTL можно еще устанавливать для каждого сообщения индивидуально (если хочется иметь экспоненциально увеличивающуюся задержку), но это придется делать уже на стороне потребителя.
Разумеется, возможны и другие сценарии (с общей dlq для всего, например), но описанный — один из самых простых.
Выбор коммита для добавления изменений в git
Одна из устоявшихся практик добавления изменений — rebase + fast-forward. Часто со схлопыванием всех коммитов в один жирный. История конечно получается красивая и это просто, но сваливать в одну кучу бизнес-изменения и рефакторинг — так себе затея, атомарные коммиты все-таки не зря придумали.
Но тогда после очередного rebase на основную ветку может возникнуть проблема, когда ваши красивые коммиты перестают компилироваться (например, кто-то тоже порефакторил от души).
И после этого делать коммит fix after rebase
или лепить все исправления в последний коммит через --amend
как-то уныло.
В этом случае поможет --fixup
:
git add someChangedFiles
git commit --fixup=OLD_COMMIT_HASH
git rebase --interactive --autosquash OLD_COMMIT_HASH^
который позволяет добавлять изменения в существующий коммит, даже если он не последний.
Порядок просмотра файлов при код-ревью
Довольно интересное исследование показало:
- Большинство разработчиков ревьюят изменения тупо по порядку следования файлов (и обычно это алфавитный).
- Тесты комментируют реже, их качество обычно меньше волнует разрабов.
- Если тесты ревьюить после основного кода (как это обычно происходит, потому что
t
— в конце алфавита), то в них реже находят баги (для основного кода существенной разницы нет). - Больше всего комментариев оставляют к первым файлам в списке, и баги в них обнаруживают с большей вероятностью.
В общем, сначала ревьюер смотрит внимательно, потом расслабляется, а тесты смотрит уже наискосок. Вроде довольно очевидно, но теперь на это есть пруфы. И это на фоне того, что лишь сравнительно недавно GitHub сделал функцию просмотра дерева измененных файлов (я еще в январе на это жаловался).
Добавлю, что еще в далекие года ведущие эксперты™ писали в своем супер-мануале про порядок просмотра при ревью: тикет, тесты, код, тесты. Потому что на ревью важнее выявлять фундаментальные проблемы: неправильные требования, сделано не то, работает не как надо и т.п. И уже во вторую очередь идут ошибки в реализации. А для всякого форматирования и прочей мелочевки должен быть линтер, а не человек.
Ковариантность и контравариантность
У меня всегда были трудности с запоминанием умных терминов, хотя суть вариантности довольно проста. Если иерархия типов сохраняется в том же порядке для производных типов (которые используют искомый тип как параметр), то это (ко)вариатность. Если идет в обратном порядке — (контра)вариантность. Если нет иерархии — (ин)вариантность.
Если термины из теории категорий — “сложна”, то можно мыслить в терминах потребитель/производитель, в Kotlin и C# параметры так пишут — in
и out
вместо всяких плюс-минусов, и это гораздо читаемее.
Вариации сна для компьютера
- Бездействие (aka S1, idle) — CPU остановлен, система потребляет чуть меньше энергии: почти все устройства переключены в режим низкого энергопотребления или выключаются (ЖД, подсветка экрана).
- Сон (aka S2) — как S1, только CPU отключен от питания (разница с S1 минимальна, на некоторых материнских платах его даже не реализовывают).
- Ждущий режим (aka S3, sleep, suspend) — почти все устройства, кроме RAM отключены, состояние хранится в памяти. RAM обновляется медленнее. Жрет питание/батарею, но чуть-чуть.
- Гибернация (aka S4, hibernate) — состояние сохраняется на диск (например, в swap), питание полностью отключается. Однако не все оборудование корректно может восстановить свое состояние, поэтому этот режим обычно отключен по умолчанию.
- Мягкое выключение (aka S5, soft off) — все выключено, но некоторые порты/контроллеры включены (чтобы включить комп по LAN или по нажатию кнопки на клавиатуре, например).
- Гибридная гибернация (aka hybrid suspend) — состояние сохраняется на диск, как в гибернации, а потом комп переводится в ждущий режим. Позволяет быстрее выходить из сна, но не терять состояние, если питание пропадет. Есть еще вариация, когда сначала выполняется suspend, а потом, по таймеру или по триггеру (например, низкий заряд батареи) кратенько просыпается и уходит в гибернацию.
- Modern Standby (aka S0 low power idle, S0ix) и PowerNap — маркетинговые названия для S0 (полностью работающий комп), просто часть функций системы не работают и часть периферии отключена. Но при этом выполняются фоновые задачи и остается подключение к сети (“как у смартфона”). Питание потребляет много, и ноутбук в таком режиме в сумку класть не стоит — может перегреться.
Псевдонимы для типов и value-классы
Почти бесплатно повысить читаемость кода можно за счет псевдонимов типов (type aliases). Например, можно заменить в бизнес-логике безликий UUID
на UserId
. Есть это почти во всех языках: typedef
или даже #define
в С, using
в C++ и C#, type alias
в Kotlin, type
в Scala/Haskell и даже в питоне, но не в Java. Можно еще использовать локально: импортировать тип с другим именем. Но у псевдонимов есть банальный недостаток: ничто не помешает в UserId
запихать другой UUID
, например ObjectId
.
Чтобы компилятор проверял подобные несоответствия, можно использовать value-типы (они же Single case discriminated union). Обычно это класс-обертка над целевым типом, который имеет единственное поле и при возможности заменяется после компиляции на целевой тип. Например, в Kotlin это будет
@JvmInline
value class UserId(val value: UUID)
В некоторых языках еще можно добавить логику (например, проверку в init
блоке или добавить методы-геттеры). Если value-типов нет, можно достичь примерно того же, используя data-классы с одним полем (а где-то может и вообще разницы не будет).
Очевидная проблема оберток в том, что к содержимому всегда придется обращаться через поле или паттерн-матчинг, а не напрямую (и какой-нибудь делегацией это, увы, не решается). А из-за потенциального оборачивания страдают библиотеки, использующие рефлексию, например, сериализация в JSON. В Scala это решили с помощью opaque-типов: рядом с объявлением класса он работает прозрачно, как type alias, а снаружи — непрозрачно, и доступа к целевому типу нет. При этом там есть и “обычные” value-классы, которые почти такие же, как в Kotlin.
А еще и в Scala и в Kotlin ждут Project Valhalla, чтобы можно было создавать пользовательские типы, доступ к которым осуществляется по значению, а не по ссылке. В комбинации с inline это позволит решить часть проблем с оборачиванием value-типов.
Help me, step-container, I'm stuck!
Иногда контейнер не может подняться, а в логах полезной информации нет. Надо бы провалиться в консоль контейнера, чтобы проверить все изнутри, но из-за постоянных рестартов это не получится сделать. В этом случае можно сохранить текущее состояние контейнера:
docker commit %container_id% %some_name%
и стартовать его с командной строкой:
docker run -it %some_name% /bin/sh
А потом уже дебажить, что там внутри не так.