Основная проблема IT-отрасли, на мой непросвещенный взгляд, заключается в том, что жизнь обучает нас профессии примерно так же, как учителя́ начальной школы — арифметике. Сначала нам говорят: делить на ноль нельзя. А потом оказывается, что ещё в XVII веке один маркиз по имени Гийом Франсуа Лопиталь научился. Нам говорят: квадратный корень можно извлекать только из положительных чисел. А потом — хоба — оказывается комплексными бывают не только обеды. И так далее.
С чего начинается обучение компьютерным наукам? — С некоторого количества теории, которая скучная и непонятная, как и любая полностью оторванная от практики теория, — а потом — с примеров. Мы открываем REPL и некоторое время забавляемся с ней, как с калькулятором.
Python 3.13.3 (main, Jun 16 2025, 18:15:32) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 1 + 2
3
>>> 0.1 + 0.2
0.30000000000000004
Серьёзно? 0.30000000000000004
? Это квантовомеханическая, или гравитационная поправка? Нет, это число с плавающей точкой так работает. Мантисса — это слово звучало на лекциях. Но кто про него вспоминает спустя несколько лет, устроившись стажёром в финтеховский стартап? Так в проектах и появляются деньги, хранящиеся во флоатах, и приводящие в уныние всех бухгалтеров при подбивке баланса.
Скажете, это смешной пример, про который все знают? — Ладно, вот вам пример повзрослее. Опросите 10 синьёров бигтеха с мегаопытом в пять и более лет, на предмет: какого рода гарантии предоставляет инкапсуляция в джаве? — И они вам хором ответят: скрывает внутреннюю кухню, не позволяя получить к ней доступ напрямую. Ха-ха-ха. Проект AspectJ появился 25 лет назад. С тех пор аспектное программирование затащили даже в Spring. Кроме того, если чужой код упал внутри чего-нибудь инкапсулированного в другом потоке — объект может остаться в любом неопределенном состоянии.
Скажете, это хак, а хаки использовать не нужно? — Ладно, давайте уже перейдем к хрестоматийному примеру: типы. Как часто вы слышали аргумент, мол, типы устраняют баги, заменяют тесты и всячески прочищают карбюратор? Некоторые ошибки разработчика типы действительно помогут выявить на ранней стадии: например, если вы попробуете передать объект «продукт» в качестве параметра в функцию «updateUserInfo», тип вам помешает. Но типичная трудноотлавливаемая ошибка часто бывает связана с ситуациями типа «off-be-one», или «неверный знак в арифметической операции», или даже «забыли про заглушку». Тут типы не помогут (ну, в off-by-one и смежных — помогут зависимые типы, реализованные в Идрисе, который с каждым годом уходит всё дальше от готовности к продакшену).
И вот, наконец, мой любимый пример: транзакции в СУБД. Якобы они предоставляют гарантии консистентности. Но на всех уровнях изоляции, отличных от Serializable (которая во-первых не умолчательная, а во-вторых — приносит кучу своих проблем, которые придется разруливать отдельно) — это не так. Причем, это бы еще и ладно; но ведь работа с СУБД обычно ведется не из консоли со скоростью печати и атомарностью каждой команды — а из высоконагруженного приложения, расположенного где-то на другой машине в сети. Наличие в этом уравнении сущности «сеть» — сразу ломает все гарантии, потому что не существует способа синхронизировать состояние в базе там и в приложении здесь. Простейший пример, с которым сталкивался лично я: мы стартуем транзакцию из приложения, и тут отваливается сеть. Вызов падает по таймауту. Если в этот момент просто отловить ошибку и попытаться транзакцию повторить (что, смею предположить, делает примерно 90% всего клиентского кода в мире) — транзакция будет применена неизвестное количество раз в диапазоне [0, N+1)
— где N
— количество попыток повтора.
Гарантии — зло
К чему я это всё? — Да к тому, что ожидание гарантий от какого-то стороннего кода — исключительно пагубная привычка, к которой нас старательно приучивают всю жизнь. Нам подсовывают механизм «try
/catch
», говоря: вот если что-то там снаружи (или внутри) поломается — перехвати исключительную ситуацию и ты снова на коне в дамках. Нам говорят: «Не хочешь потерять данные — положи в базу». Не хочешь, чтобы всё сломалось — перехвати исключение. Что-то не получилось? — Попробуй снова. Добейся, чтобы твой код никогда не ломался, не падал, не приводил к сегфолту.
Проблема в том, что на самом деле нет никаких гарантий, что «положил в базу — значит не потерял», «перехватил исключение — значит, продолжил нормальное выполнение», «получилось со второй попытки — ну и отлично». Я уже выслушал кучу наставлений «синьёров» про то, что «всю Молдаванку устраивает, а его видите ли нет» — когда попытался рассказать, почему просто засунуть медленный код в жобу — очень плохая идея. Красной нитью проходит всё то же непонимание, что в IT нет и не может быть никаких гарантий. И есть всего два варианта работать в такой ситуации: ① смириться, что каждая сотая (тысячная, миллионная) операция выполнится с неожиданным результатом, или ② принять постулат об отсутствии гарантий и научиться писать код, которому гарантии не нужны.
На самом деле, перед разработчиком никогда не стоит задача добиться гарантий успеха на промежуточном этапе. Гарантии нужны только при идемпотентном терминировании условного конечного автомата жизни данных. Результат — должен быть сохранён, для потомков и аудиторов, тут спору нет, но это несложно: просто долбитесь в базу, пока она не ответит согласием, очевидная идемпотентность этого вызова позволяет. А вот промежуточные шаги — совсем не требуют никаких гарантий выполнения, нужна лишь гарантия «перехода в следующее состояние».
«Добиться, чтобы при создании черновика документа он был сохранён в базе» — неправильная постановка задачи. Нас интересует не факт сохранения в базе, а появление возможности публикации. Доступность следующего шага, а не знание того, сохранен документ, или еще нет (это не значит, что нам не нужно его сохранить, это значит только то, что проверять факт сохранения, и полагаться на него — бессмысленно и вредно). Полагаться надо на доступность возможности опубликовать, которая появится только после сохранения в базу (при обычном дизайне приложения). Поэтому прикладной код должен не добиваться гарантий сохранения, а предпринять определенные действия, если черновику была отдана команда сохраниться, а действие «публикация» — всё еще недоступно.
Асинхронные сообщения
В голой акторной модели все сообщения асинхронны. Гарантий доставки нет. «Крутись как хочешь, но завтра в полдень — похороны.»
Такое (на поверхностный и неверный взгляд) недружелюбие со стороны окружения — заставляет разработчика мыслить немного иначе. Не полагаться слепо на какие-то там гарантии, предоставляемые на бумаге чужим кодом, — но четко понимать, что и зачем он делает.
Взять вот хотя бы набивший лично мне оскомину пример с расплатой за товар. Что должно произойти в результате этого действия? — Если у вас в голове появились мысли про проверку наличия товара, денег на балансе, еще какие-то детали реализации — ответ неверный. Должно произойти следующее:
процесс покупки должен быть тотальным (завершиться при любых входных данных с ожидаемым результатом за конечное время)
пользователь должен оказаться в одном из двух возможных состояний: либо с товаром и меньшим количеством денег на балансе, либо без товара но с той же суммой, что у него была до начала этой возни
магазин должен оказаться в одном из двух возможных состояний: либо без товара и бо́льшим количеством денег на балансе, либо с товаром и с той же суммой, что у него была до начала этой возни
Всё. Вы видите где-то тут что-то про базу, транзакции, и прочие детали имплементации? — Я да: тотальность, которую я не случайно поставил во главе угла, недостижима, если полагаться только на базу и транзакции (и жобу, конечно, которая будет повторять попытку провести оплату даже в ситуации «по банку вдарила болванка»). Заметьте, кстати, любопытную деталь: не имеет никакого смысла удостоверяться, что количество денег, заплаченных покупателем, не отличается от количества денег, полученных магазином. Или что это один и тот же товар. Операционные пространства покупателя и магазина — совершенно, абсолютно не связаны по умолчанию (их можно связать пото́м, если надо): покупателю важна только транзакция деньги→товар, а магазину — только товар→деньги. Если товар аннигилировал, а деньги пришли в результате сжигания квантово-связанной со счетом магазина банкноты в Зимбабве — всё хорошо.
Я знаю, как правильно реализовать этот сценарий с базой. Простите за болд, но смешные человечки, спешащие мне сообщить, что я просто не умею программировать, — неимоверно надоели. Еще я знаю, что если опираться в этом решении на базу — решение получится либо не тотальным, либо неимоверно окостыленным. Большинство, конечно, либо просто не подумает о крайних случаях, либо забьёт на них болт на тридцать два с правой резьбой.
При этом, если решать эту задачу изначально в предположении, что никаких гарантий нет, на асинхронных сообщениях, то всё получится изящно и довольно бесхитростно.
Disclaimer
Поскольку этот текст (перестаньте уже называть эти текстики высокопарным «статья», этот термин подразумевает рецензирование, как минимум) написан на русском языке и опубликован тут, считаю своим долгом сообщить для людей среднего умишка и атрофированной способностью воспринимать печатный текст: я не отрицаю ценность базы, сам иногда пользуюсь синхронными сообщениями и полагаюсь на гарантии третьесторонних библиотек/софтин в некритичных случаях. Просто более десяти лет работы с критичными сущностями (типа транзакций на $50M) — заставили меня думать не о девяностодевятипроцентных гарантиях, а о том, как эту цифру довести до максимума. А это невозможно, если верить в Деда Мороза, Золотых Единорогов, Зубных Фей и Гарантии ПО.
Удачного конструктивного недоверия!