Читать в телеге. Когда-то там были посты не только от меня.
Связи в проекте и Github Action
Очевидно, что связи между артефактами в проекте очень полезны. Код привязан к коммиту, коммит — к тикету и код-ревью/пулл-реквесту, тикет к ТЗ/эпику/истории и/или документации. Причем хорошо, когда эти связи еще и двусторонние: например, чтобы по коду можно посмотреть тикет и зачем он был написан, а по тикету — написанный код. Иначе при смене процесса придется искать эти связи вручную или не напрямую. Кажется, это одна из “продающих” фишек Gitlab или Space.
Мне не хватало связи пулл-реквеста GitHub с тикетом, существующие варианты action’ов показались унылыми, поэтому я написал свой, старался сделать его максимально универсальным.
К сожалению, для написания action’ов есть только два варианта: nodejs или docker-образ. Я по глупости выбрал первый вариант, потому что большинство action’ов были написаны на нем, да и библиотека для работы с GitHub была только для JS. В очередной раз вляпался в экосистему nodejs: обновление npm через него самого сделало его непригодным для использования. Куча пулл-реквестов от Dependabot — это мрак, группировки обновлений нет годами.
TypeScript вроде мощный и классный, можно выражения в шаблонах считать, но нет каких-то банальных вещей вроде безопасного enum или filterNotNull. Но мне понравились линтеры и форматтеры — благодаря им исправилась половина моих ошибок.
Сама библиотека для работы с GitHub сильно разочаровала. Хочешь получить текст коммита в пулл-реквесте? Запрашивай через API. Хочешь имя ветки? Используй окружение или костыли. Конфигурация в yaml, но параметры только строчные. Наконец, добила возможность гонок: все работы из action’ов запускаются параллельно, а контекст запуска статичный, поэтому пришлось менять получение тела из контекста на запрос к API.
Что говорит наука о разработке?
Многие “лучшие практики” разработки основаны на мнениях и личном опыте, а не на исследованиях. А исследований про процесс разработки очень мало и к ним есть много вопросов относительно качества. Мне понравился доклад на эту тему.
В качестве примера приводится исследование про полные и сокращенные имена сущностей: оно показало, что при отладке нет разницы, написано ли employer_number
или emp_num
. Но при изучении самого процесса отладки разработчиками выяснилось, что в двух группах просто использовали разные подходы для отладки, и оба сработали примерно одинаково.
В другом исследовании выяснили, что в говнокоде больше вероятность наличия багов, но при этом его переписывание не коррелирует со снижением числа багов. Еще было исследование исходников кучи проектов на GitHub с результатом, что программы на ФЯП имели меньше багов, чем программы на императивных языках, а на языках с автоматическим управлением памятью — меньше, чем на языках с ручным управлением и т.п. (что вроде очевидно). Однако последующее исследование показало ошибку в методике, а с ее учетом выяснилось, что ни у одного языка нет преимуществ относительно другого. Похожие проблемы были при исследовании TDD, систем типов, парного программирования.
Но есть и вещи, которые более-менее подтверждаются наукой. Эмпирически доказано, что устройство организации сильно влияет на устройство кода. Многие самые дорогие баги — это проблемы архитектуры или требований. Практика, которая работает — код-ревью, она позволяет ловить 60-80% багов. А еще одни из самых существенных факторов для качественной разработки (да и любой работы в целом) — это отсутствие стресса и хороший сон. Бессонная ночь = -50% к производительности. Кранчи делают итоговый продукт хуже по всем метрикам.
Ссылки на статьи из доклада можно найти тут.
Насколько уникален UUID?
Напомню, что UUID — это идентификатор вида ac2c6d9a-68d0-4802-948b-da8cc594ac80
, который содержит 16 байт или 128 бит. При этом в 4 версии из этих 128 бит только 122 случайны (и генерируются обычно хорошим генератором случайных чисел), остальные 6 — это 4 для номера версии и 2 зарезервированных бита.
Чтобы оценить вероятность совпадения, можно воспользоваться формулой для “парадокса” дней рождения, а именно ее приближением:
p(n, b) = 1 - e^(-n(n-1)/2^(b+1)))
где n
— количество сгенерированных элементов, а b
— количество случайных бит (2^b
— количество вариантов).
Дальше надо прикинуть, сколько будет UUID в системе. Пусть нагрузка на систему — 1000 RPS, и при каждом запросе генерируется новый UUID. Тогда за 1000 лет будет сгенерировано всего 31,5 триллиона идентификаторов, а вероятность, что будет пара одинаковых — всего 9*10^-11
, чем можно спокойно пренебречь.
Круглые скобки в Scala
Иногда слышу в качестве одной из претензий к Scala “почему для получения элемента из ассоциативного массива используют круглые скобки?”. В одном из докладов услышал интересную гипотезу на эту тему.
Что такое “map”? Вообще это карта, и имеет происхождение от латинского “mappa mundi” — “лист мира”. Если заглянуть в словарь поглубже, то там будет упомянуто еще математическое значение — синоним “mapping”, отображения [из одного множества в другое], что большинстве случаев можно считать синонимом функции.
Ассоциативный массив — это взаимооднозначное соответствие ключей и значений. Т.е. это практически частичная функция, которая записана без всяких обобщений, а тупо набором соответствий. Если ассоциативный массив называется как функция, работает как функция и крякает как функция, то это, вероятно, и есть функция. Поэтому и скобочки как у функции. Можно, конечно, возразить, что можно менять, но это неверно — по умолчанию все иммутабельно.
Увы, каких-нибудь веских подтверждений я не нашел, так что половину доводов я тут сам придумал:) Если посмотреть на сравнение ассоциативных массивов в разных языках, то решение действительно довольно маргинальное.
Action для коммита в другой репозиторий
У меня на сайте сейчас два способа подключения чего-то стороннего: через git read-tree и через git submodule. Теперь появился еще один: через github action, который пушит коммит в другой репозиторий. Этот вариант оказался самым удобным, жаль, что я на него наткнулся недавно и случайно.
Будьте проще
Неплохой, хотя и немного спорный доклад про то, что надо делать вещи простыми, но это нелегко. Простота почти синонимична с изолированностью, модульностью. У простых вещей масса преимуществ — ими проще оперировать, их проще понимать, поддерживать, комбинировать (привет, UNIX-way).
В свою очередь сложные вещи проблемны из-за того, что они “переплетают” между собой несколько сущностей. Например, переменные переплетают между собой значение и время, а циклы — действие и способ его исполнения. Причем можно построить эквивалентную программу с использованием более простых конструкций (но не обязательно менее мощных).
Есть более полная версия, там чуть подробнее и есть юморок.
Запросы к jsonb
На SQL можно написать порой ужасные вещи, например:
SELECT id, jsonb_path_query_array(value, '$[*].entityType')
FROM demo WHERE value @? '$[*] ? (@.entityId == 1 &&
(!exists (@.childrenIds) || exists (@.childrenIds ? (@[*] == 4))))';
Выглядит как совсем другой язык, потому что идет обращение к jsonb. Одно дело хранить неструктурированные данные в jsonb, но гонять по ним запросы — это уже звоночек. Для какой-нибудь отладки это еще куда ни шло, хотя там быстрее будет сделать большую часть операций вручную, чем написать полностью корректный запрос. Но в основном коде это делать — это признак того, что кто-то не смог нормально смоделировать бизнес сущности или использует данные по какому-то извращенному сценарию.
А в запросе выбираются элементы массива, в которых есть объект с entityId
и либо пустым childrenIds
, либо содержащим 4
. В ответе выдаются только значения полей entityType
.
Полный пример
CREATE TABLE demo(
id int not NULL,
value jsonb Not NULL
);
INSERT into demo VALUES
(1, '[{"entityId": 1, "entityType": "type1", "childrenIds": []}, {"entityId": 10, "entityType": "type5", "childrenIds": [30, 20]}]'::jsonb),
(2, '[{"entityId": 1, "entityType": "type2"}, {"entityId": 15, "entityType": "type1", "childrenIds": [4, 20]}]'::jsonb),
(3, '[{"entityId": 1, "entityType": "type3", "childrenIds": [5, 4]}, {"entityId": 25, "entityType": "type4", "childrenIds": [30, 4]}]'::jsonb),
(4, '[{"entityId": 35, "entityType": "type4", "childrenIds": [35, 4]}]'::jsonb);
SELECT id, jsonb_path_query_array(value, '$[*].entityType')
FROM demo WHERE VALUE @? '$[*] ? (@.entityId == 1 && (!exists (@.childrenIds) || exists (@.childrenIds ? (@[*] == 4))))';
Поиграться можно тут. В ответе будет 2 и 3 запись.
Local-first приложения
Мартин Клеппман (тот самый, который автор книги с кабанчиком) продвигает подход Local-first. Основная идея довольно простая: все действия должны по максимуму производиться локально на машине пользователя, а облако нужно использовать для синхронизации изменений. Когда программисты работают над кодом, им не нужно, чтобы постоянно было подключение к git-серверу, а если он взорвется, то можно будет поднять свой, и данные полностью под контролем. Так почему в каких-нибудь гугл-документах все не так?
Кроме того, с точки зрения архитектуры классический путь данных в приложении сейчас — это HTML-DOM → JS-фреймворк → REST → модель → сущность ORM → запись на диске, и большая часть кода — это конвертеры одного слоя в другой. В Local-First подходе предлагается оставить HTML-DOM → JS-фреймворк, оттуда писать в локальное хранилище, а всю синхронизацию вынести в фоновый режим. Сеть — ненадежный способ передачи данных, и в классических приложениях надо решать как технические проблемы, так и проблемы с UX. Для синхронизации данных можно использовать не привязанный ни к конкретной компании, ни к конкретным моделям данных фреймворк, основанный на CRDT (conflict-free replicated data type). Группа Клеппмана разрабатывает свой фреймворк для этого, Automerge.
Идея классная, технически интересная, и как пользователю мне она нравится, но увы, непонятно, кто готов будет за это платить. Тут, как минимум, две проблемы: единство протокола и бесконтрольность компании. С единым протоколом/форматом данных можно привести в качестве канонического примера миллиард мессенджеров, при этом плюс-минус единый стандарт — это XMPP (пользуетесь jabber’ом?), а еще и рабочий при этом — это email (которому уже больше 35 лет). А когда контроль над данными переходит от сервиса к пользователю, то автоматом всплывают проблемы с моделью продажи (это уже не SaaS, в котором можно сделать что угодно), тайной алгоритмов, стоимостью поддержки старых версий, аналитикой, обновлениями и т.п.. Кажется, что подобный подход к ПО пока могут позволить себе только энтузиасты от Open Source и единичные производители.
JMX в Kubernetes
JMX (Java Management Extensions) — технология, с помощью которой можно подключится к java-процессу через сокет и посмотреть использование ресурсов, снять дамп памяти или даже поменять какие-нибудь значения в памяти через управляемые ресурсы (MBeans). Есть два плюс-минус стандартных инструмента для этого: jconsole
и jvisualvm
.
Но сейчас нельзя просто так взять и подключиться к чему-то по сокету — все в кубере лежит. А поды еще и со сгенерированными айдишниками. Сначала надо получить id пода от желаемого сервиса:
POD=`kubectl -n $NAMESPACE get pods -l app=$APPNAME -o name`
флаг -l
фильтрует по метке пода, -o
— оставляет в выводе только имя.
Потом получаем случайный порт (можно заморочиться и проверить, что он свободен):
PORT=`echo $(( $RANDOM % (65550 - 15000) + 15000))`
# или
PORT=`shuf -i 15000-65550 -n 1`
Перенаправляем порт ($JMX_PORT
— номер порта, через который JMX выставлен локально в поде):
kubectl -n $NAMESPACE port-forward $POD $PORT:$JMX_PORT &
процесс запускается в фоне, и перенаправление будет работать только пока он запущен.
Наконец, запускаем jconsole
или jvisualvm
… и получаем облом из-за того, что удаленный порт-то не настоящий: нужно больше одного порта для подключения через RMI. Это можно вылечить работой через JMXMP-протокол, но нужно будет добавить его jar и в приложение, и в клиент. А после этого запустить так:
jvisualvm -cp:a jmxremote_optional.jar --openjmx "service:jmx:jmxmp://localhost:$PORT"
Или можно просто добавить в настройки приложения -Djava.rmi.server.hostname=127.0.0.1
и подключаться так:
jvisualvm --openjmx "localhost:$PORT"
PROFIT! Все это, разумеется, можно накидать в баш-скрипт, чтобы не запоминать, а указывать только namespace и имя сервиса.
Null в SQL
Сколько результатов вернет этот запрос?
SELECT * FROM ABS(null) as r WHERE (r >= 0) OR NOT (r >= 0)
Ответ
Правильный ответ — ни одного, потому что условие вернет ложь. А связано это с тем, что null
обрабатывается по особому, почти как NaN: почти любое выражение с ним возвращает null
: ABS(null)
— это null
, null >=0
и NOT null
— тоже. Можно сказать, что SQL реализует тернарную логику, где любое выражение может быть правдой, ложью или неизвестным. Поэтому стоит очень аккуратно писать запросы для nullable колонок, а еще лучше избегать их, если это возможно.
UPD:
Стоит дополнить, что вместо
WHERE (r IS NULL OR r != 'value')
можно использовать
WHERE r IS DISTINCT FROM 'value';