Минутка просвещения

Читать в телеге. Когда-то там были посты не только от меня.

Разновидности event-driven архитектуры

Согласно Фаулеру (видео), существует 3 типа Event-Driven архитектуры:

  1. event notification — уведомление о событии. У меня что-то поменялось, дерни меня синхронно, чтобы узнать что.
  2. event-carried state transfer — передача состояния в событиях. У меня поменялось поле, новое значение такое-то. Или мое состояние поменялось, вот текущее.
  3. event sourcing — нет никаких состояний, есть только набор событий, они являются мастер-данными. Хочешь состояние — либо сам храни, либо вычисляй. Иногда, так и быть, дадим тебе слепок, чтобы попроще было вычислять.
СсылкаКомментировать

Облегчение Font Awesome и задержка из-за CSS

Внезапно обнаружил, что Font Awesome, webdings современного интернета, является встроенным шрифтом в Ubuntu. Однако для других систем его все равно надо подключать. Раздаваемый с CDN CSS, кроме директивы @font-face, содержит еще и кучу лишних предопределенных классов, которые просто проставляют содержимое с нужным юникод-символом.

Но есть и другая неприятная особенность включения стороннего CSS в код страницы: пока он не будет загружен, то страница не будет отображаться. Можете попробовать сами сохранить и открыть в браузере такой HTML:

<head>
<link rel="stylesheet" href="https://httpbin.org/delay/11">
</head>
<body>
<h1>Loaded</h1>
</body>

У меня он открывается примерно через 10 секунд. И такое поведение я наблюдал, когда редактировал сайтик в оффлайн режиме. Поэтому я отказался от внешней зависимости и сохранил Font Awesome на сайт.

СсылкаКомментировать

Генерировать или валидировать?

Как известно, я люблю автоматизировать всякую дичь (например, 1, 2, 3, 4). Один из видов такой дичи — проверка того, что разные части приложения соответствуют друг другу и/или что тестами покрыто все, что нужно. В основном из-за недоверия к кожаным мешкам.

Приведу примеры:

  • проверка того, что все модули системы покрыты тестами и включены в основной конфиг. Причем немного извращенная, потому что сверялись три места в коде: список основных классов, список классов тестов и список классов в основном конфиге приложения.
  • проверка соответствия REST-клиентов соответствующим микросервисам. Куча рефлексии, чтобы проверить, что если вызвать все методы клиента со всеми параметрами, то будут вызваны все выставленные эндпоинты и покрыты все параметры и тело запроса.
  • покрытие тестами схем камунды, о котором уже писал.
  • проверка, что json-схема, маппинг Elasticsearch и swagger соответствуют друг другу.

Казалось бы, это странные операции. Ведь для проверки покрытия есть специальные утилиты, а такие вещи, как swagger, клиенты для REST легко генерируются. Но, как говорится, есть нюансы.

Средства покрытия не могут проверить все (например, все, что не является кодом) и их точность завязана на строчки кода. 100% покрытия редко достижимы, обычно это в районе 80-90%. Подразумевается, что в эти 10-20% попадает всякая служебная фигня, но никто не гарантирует, что это не окажется ядро системы. Кроме того, редко когда плохое покрытие блокирует релиз, да и мало кто смотрит эти отчеты. А вот если они валят тесты, то это уже повод что-то сделать (можно сделать таск на билде для завала по покрытию, но это обычно гемор).

Генерация — хорошая штука (в конце концов, код — это DSL для генерации машинного кода), но результат все равно придется как-то проверять. Можно довериться разработчикам генераторов, но никто не отменял garbage in → garbage out. Однако не на всякую дичь есть генератор, а хорошо написать свой — не очень тривиально. Сложно учесть все требования и нюансы, надо адекватно встраивать генерацию в процесс сборки, некоторые вещи все равно придется дописывать руками (какие-нибудь описания). Кроме того, страдает гибкость: в сгенерированном API-клиенте уже не затащишь два http-запроса в один метод, в swagger не детализируешь, что там в кишках json, который отдается as is из базы и т.п. Наконец, генерация — процесс с потерей информации, и если нет единого источника правды, который содержит ее полностью, то появляется дополнительная сущность, из которой генерируются остальные.

А написать проверку, что все соответствует друг-другу — довольно просто. И все сущности доступны напрямую из репозитория, ими легко поделиться (если это какой-нибудь контракт, например). Можно не заморачиваться с прокидыванием метаинформации (всякие описания полей) — ее все равно проверить может только человек. Ему правда писать придется чуть больше, но обычно это имеет позволительную цену. Лучше иметь явный тест, чем строчку в чек-листе код-ревью.

СсылкаКомментировать

Устройство компилятора Kotlin

В версии 1.4 JetBrains немного поменяли архитектуру компилятора и про это есть довольно интересный доклад (слайды). В старом компиляторе были немного срезаны углы для быстрой разработки, а в новом все по классике: фронтенд-часть с выходом промежуточного представления (по факту — AST с дополнительной информацией) и бэкенд (который отвечает за генерацию исполняемого кода).

Из интересного:

  • У языка все еще нет спецификации, покрывающей все нюансы языка. Источник правды про его работу — исходный код. Сейчас есть только черновик спецификации. Например, на момент доклада узнать, какие есть варианты смарт-кастов, можно было только из кода (хотя сейчас в спецификации довольно подробно описана куча вариантов, в том числе пример-загадка из доклада).
  • Внутри компилятора есть типы-пересечения (например, общий родитель у Int и FloatComparable<*> & Number), но они невыразимы (надеюсь, что это "пока").
  • Все if конвертируются в when, но писать лучше все равно по-человечески, ручные микрооптимизации будут только мешать компилятору.
  • В бэкенд-части компилятора есть оптимизации, но они в основном используются для того, чтобы упростить конструкции, чтобы потом JIT'у было их проще понять и оптимизировать. Однако для Native и JS есть еще свои оптимизации, их больше.

Про устройство компиляторов есть еще доклад от небезызвестного Брагилевского (но там про Haskell уже). Я слушал его вживую на конференции, и он был неплох.

СсылкаКомментировать

Лимиты Docker Hub

У Docker Hub есть лимиты на выкачивание образов. Значение по умолчанию — 100 загрузок за 6 часов. Маловероятно, конечно, что будет столько будет закачек, но если нет авторизации и выделенного IP, то исчерпать лимит возможно.

Решения два: либо заплатить бабок, либо качать через кэширующий прокси. Во втором случае нужно будет поменять конфиги и проверять helm-чарты, потому что в них могут быть указаны свои репозитории образов. Например, в bitnami/postgresql нужно заменить docker.io на свою прокси аж 3 раза.

СсылкаКомментировать

Таймеры на сервисных тасках в Camunda

В Camunda таймеры не работают на сервисных тасках. Т.е. такой таймер

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

Что делать?

Если таск внешний, то таймер будет работать, потому что внешние таски находятся в состоянии ожидания (транзакция закрывается). Однако это будет означать разделение камунды и бизнес-логики на два сервиса и переход на Rest API вместо внутреннего.

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

Третий вариант — таймауты можно обрабатывать внутри логики делегата и кидать ошибку/эскалацию самостоятельно.

Наконец, можно отрефакторить схему так, чтобы таймер не был на сервисной таске. Однако банально выделить его в параллельную ветку с каким-нибудь условием не получится: параллельные ветки на схеме в движке выполняются на самом деле последовательно.

СсылкаКомментировать

OffsetDateTime и Hibernate

При работе с датами Hibernate стоит быть аккуратным: он все типы конвертирует в java.sql.Timestamp, который по смыслу идентичен java.time.Instant. Поэтому информация о временной зоне будет потеряна: при чтении из базы в какой-нибудь OffsetDateTime будет подставлена системная зона. Так что проще сразу маппить на Instant во избежание недоразумений. Матерые Java-чемпионы так и говорят, что OffsetDateTime и ZonedDateTime для JPA не очень полезны. И вообще, можно стрельнуть себе в ногу с ними.

Можно еще выбрать в самой базе тип "timestamp with time zone", если он поддерживается (потому что в SQL стандарте его нет), но, во-первых, Hibernate пофигу на это отличие, а во-вторых, в этом типе на самом деле не хранится информация о самой зоне, см., например, документацию Postgresql:

All timezone-aware dates and times are stored internally in UTC. They are converted to local time in the zone specified by the TimeZone configuration parameter before being displayed to the client.
СсылкаКомментировать

DSL для роутинга

В Spring 5.2 можно писать в функциональном стиле не только для WebFlux, но и для обычного MVC. Т.е. вместо анноташек над контроллерами можно писать что-то вроде

fun routes() = router {
    accept(ALL).nest {
        GET("/hello/{name}") {
            val name = it.pathVariable("name")
            ok().body("Hello, $name!")
        }
    }
}

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

Подобный подход очень напоминает Ktor:

    routing {
        get("/hello/{name}") {
            val name = call.parameters["name"]
            call.respondText("Hello $name!")
        }
    }
СсылкаКомментировать

Модели памяти языков программирования

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

В хардварной части в основном рассматриваются разные ситуации с гонками и последовательной согласованностью (Sequential consistency). Раскрывается мысль, что даже на уровне железа есть приколы с многопоточностью. Получается, что очень похожие проблем с параллелизмом случаются в разных контекстах и на разных уровнях абстракций: в процессоре, языке программирования, узлах распределенной БД, микросервисах. И методы решения похожи: куча очередей (как в x86), куча синхронизирующихся копий состояния (как в ARM). Разумеется, x86 и ARM дают разные гарантии консистентности. Под конец статьи рассказывается о модели синхронизации DRF (data-race-free). Вкратце суть такая: кроме чтения и записи, для памяти есть еще операция синхронизации. Сихнронизация служит "барьером": операции чтения и записи можно перемешивать как угодно, но нельзя перемещать через барьер. И в 1990 доказали, что если писать программу без гонок (нормально синхронизировать чтение-запись между потоками), то тогда на железо, соответствующее DRF, будет выполнять с последовательной согласованностью, т.е. как будто она вся выполнялась в одном потоке. И это очень классный результат, потому что позволил абстрагироваться от кучи проблем, написанных в начале статьи, и дать хоть какой-то простор для оптимизаций компилятору.

В программной части больше интересного. Например, Java, оказывается, первый язык, в котором была попытка прописать поведение многопоточных программ на уровне спецификации (1996). С первого раза не получилось, но в 2004 в Java 1.5 добавили фич и она стала поддерживать модель DRF, причем в этом активно участвовала одна из авторов оригинальной статьи. Отношение happens-before как реализация DRF — важная часть модели памяти Java. Однако создатели в 2010 году признали, что у этой реализации есть баги, хоть она и является хорошим компромиссом между сложностью и надежностью.

В C++11 использовали модель Java как основу (sic!), пытались сделать проще, но в итоге только усложнили и она стала менее полезной для программистов. Ну и undefined behaviour добавили, куда ж без него, и стремные атомики, которые особо ничего не гарантируют. Однако в Си, Rust и Swift использовали модель C++11 (последние два — потому что LLVM и интеграция). JavaScript пошел своим путем: его модель памяти совместима с C++11, но ближе к Java (место для вашей шутки про название языка).

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

СсылкаКомментировать