Типовая "боль" любого разработчика в Энтерпрайзе - "я устал писать 'ресты'". Вместо слова "ресты" можете подставить свое. То есть, суть в том, что очень часто разраб садится на тухлый легаси-проект и сидит там только ради зарплаты и "надежности". Особенно хреново дело обстоит в больших организациях, которые стараются сократить "изобретение велоспипедов" программистов и большую часть времени там кодер даже не кодит, а занимается различной чухней: переписки со смежниками, инструкции ПСИ, отчеты ИФТ, терки с "безами", настройка конфигов и ямлов и прочее. Так и хочется спросить - "а где код-то?".
На Хабре даже как-то была статья, где аналитик писал, что работая в каком-нить ВТБ ты становишься спецом по работе в ВТБ и чем больше ты тратитьшь на это время, тем сложнее тебе будет выйти наружу и тем сильнее падает твой ценник на рынке.
В октябре прошлого года я принял четкое решение "валить". Но быстро убедившись, что буржуйский интерпрайз - это, как правило, "галера за 3 копейки", где денег будет не хватить даже на хлеб, а РФ ну прям очень большие бабки платят те же "большие друзья" - Тенек, Альфа, ВТБ и прочее. Однако слухи и второй волне мобилизации и незакрытые некоторые фин-вопросы заставили меня пока просто рассмотреть переход в другую команду.
В общем, у нас здесь есть несколько "любимых игрушек" рук-ва (например, подразделение робототехники), которые показывают руководству страны при официальных визитах, и я уже писал, что в итоге я перешел в одно из них. Но сегодня я хочу рассказать про одну из тасочек, которую я пилил 2,5 месяца и от которой слегка поседел. Тасочка несколько дней назад уехала на ИФТ и даже откатала позитивные кейсы, так что я думаю можно слегка выдохнуть уже.
И так, задача.
Есть некое распредленное файловое хранилище. Там все как у вас на жестком диске - папки, документы. Документы не простые, а подписанные всякими разными ЭЦП. Если документ подписан, он содержит еще некий набор бюрократии к этой подписи - протоколы подписи и все такое. Будем далее называть это просто протоколами для простоты. У каждого документа и каждой папки есть свой уникальный айдишник (UUID в хитром формате), а так же есть имя и другие свойства.
ПО состоит из модулей. Вообще их более 50-ти штук. Наша задача - переписать метод создания архива. То есть пользователь присылает сообщение с указанием айдишников документов, надо все положить в ZIP и отдать айдишник созданного архива пользаку. Как с точки зрения модулей и внешних систем все работает:
1) приходит запрос в Кафку
2) вычитываем
3) валидируем
4) достаем из хранилища документы
5) идем в сторонний внешний сервис подписей и тащим оттуда протоколы
6) все пакупаем в архив и отдаем айдишник пользователю
Итого: у нас задействовано 4 компонента:
1) Принимает запрос асинхронный сервис
2) Для вытаскивания документов и записи архива в хранилище идем в адаптер хранилища через реактивный асинхронный файловый адаптер (по сути Рест-сервис на Java RX). Этот Реактивный сервис реактивно пуляет запросы в обычный синхронный файловый адаптер
3) Адаптер - это обычный рест-сервис, который взаимодействует с поставляемым разработчиком (IBM) хранилища драйвером
4) REST-Сервис подписей, который взаимодействует с внешней системой (в рамках банка это называется АС), отвечающей за все, что связано с подписями в организации. Крайне нагруженная и тормозная штуковина, которая может в некоторых случаях отвечать по 30 секунд, у которой есть свои лимиты на кол-во запросов и все такое. А еще она может иногда вообще не отвечать.
Сервис приехал на НТ и свалился на 10 отправленных запросах одновременно. И его вернули в разработку и дали мне.
Знатокам
Перед тем как рассказывать о решении задачи, нужно написать послание знатокам. Есть такая категория людей, которая вообще не терпима к чужим сложностям, что у кого-то может что-то не получаться. 90% таких людей откровенные задроты. Я могу смело в глаза сказать, что такие есть среди моих лучших друзей и я им часто говорю - ребята, исправляйте это в себе. Это косяк! В чем заключается задротство? Такие люди как правило долбились долго и уныло в одну и ту же тему почти всю жизнь. Начали, к примеру, увлекаться IT лет в 7, в 9-том классе побеждали уже на Олимпиадах по информатике школьных. Но им это давалось не то, что легко. У нас в спортивной тусовке был парниша, который сейчас прям звездит, ездит по всяким там турнирам международным и все такое. Я говорил с его самым первым преподавателем и он рассказал мне: "Блин, Паша первые 10 месяцев вообще в музыку ходить не мог. У него все шаги были мимо. Мы его тупо не могли научить в музыку шагать почти год!". И вот так же и эти задроты. У них есть результат, но это следствие долгого и унылого "взлето-падения". И вот сейчас они читают статьи типа вот этой моей и такие - "да хули там сложного-то? Ты слишком усложняешь!".
Про таких людей есть отличная песня у группу Пневмослон. Называется "Серега". Там есть такие слова:
"Да пошел ты нААААААААхуй, ебаный Серега,
Со своею правдой и со СВОИМ, БЛЯТЬ, опытом...."
Вот на этом и закончим.
Бэкгранд
Теперь о бэкграунде. Итак, я в IT не так давно. С 2016 года. 99% времени я "пилил ресты" на мертвых проектах на Джаве. Либо вообще никому не нужных проектах, либо не стрельнувших, либо с нагрузкой 800 человек, как в своей предыдущей команде. Это обычные плоские "ресты" из серии "сходить в БД или другой сервис, достать простенький списочек позиций на 100, отфильтровать, отдать".
- Котлин, я трогал только мизинцем правой ноги на проекте у Юры Веретельникова и так, чтобы не обляпаться и не завоняло.
- Примерно то же самое с Java RX.
- "Тесты мы не пишем, потому, что для нашей компании это слишком дорого" (С) Снова Юра. И я реально думал, да на хуй они нужны вообще.
- Какая-то работа с IO? Вы чего, ебанулись совсем, я же не C++ программист, зачем?
- Кафка? Это который "Превращение" написал? аФтор такой?
Опен Шифт? Да слышал, это когда нужен дорогой девопсер и можно сходить его подоставать. Что? SMF-ы и HELM-чарты пишет разработчик? Вы пизданулись совсем? Infrastructure as Code? Нееее, не слышал.
И вот мне дают задачу с проектом, блять, на Котлине, где почти все реактивное, херова туча тестов на BDDMockito, все это работает через Кафку, которую я в глаза не видел, хренова туча IO, с которым я никогда не работал и все это надо "вчера".
Плюсы:
- от эджайла только название
- в отличие от предыдущей команды нет никакого эджайл-говна типа бесконечных никому ненужных встреч, митингов, планирований, грумингов, демо, ретро, капатиси, обратных связей и прочего.
- никаких дебильных ограничений по проекту, когда тебе шаг вправо / шаг влево - расстрел. Никакой платформы, платформенных депенденси и конфигов. Полная свобода.
- никаких ограничений от безов в плане "ой, в ИФТшную Кафку только сопроводам можно"
Что решили. Предположили, что bottle neck метода был в том месте, где собственно происходит архивирование. В листенере вычитывалось сообщение, открывалось соединение с адаптером и пока оно было открыто, доставались документы, запрашивались протоколы (которых ждать можно было очень долго), писался архив, соединение рвалось. Если очередь нагрузить одновременно 10-ю сообщениями, открывалось 10 коннектов, которые висели очень долго, пул коннекшнов переполнялся и все это обламывалось на пол-пути.
Решили, что когда в первый листенер приходит сообщение, оно в стриме должно дробиться и по каждому документу отправляться сообщение в следующую очередь. Перед дроблением сообщение валидируется, например на то, что эти документы вообще есть.
Каждое такое сообщение (каждый документ) от первого листенера вычитывает следующий листенер, который выполняет основную обработку.
Так же мы решили, убрать Pipes. Но метод адаптера, создающий документ в хранилище на вход потребляет InputStream: возникла проблема, как без пайпов скормить OutPutStream для ZIP-а Инпутстриму. Напомню, я с IO не работал вообще никогда. У меня паника!
В итоге налось решение вида:
То есть ZIP "срет" протоколами в output stream, потом оно конвертится в ByteArrayInputStream. И только после этого в createTempZipForSignatures открывается коннекшен с хранилищем и туда заливается временный ZIP. Почему ZIP? Протоколов может быть несколько и проще их собрать в zip во временную папку и передать айдишник ZIP-а дальше. Так проще потом найти и вытащить. Но тут встают проблемы:
- надо чтобы пользователь мог задавать имя временной папки в конфигах
- надо проверить на старте есть она или нет
- если нет - создать
- нужен шедуллер, который раз в сутки будет вычищать оттуда все
- нужно прокинуть в эксепшн-хендлеры всех листенеров отдельный код, который будет удалять временные файлы и удалять их в конце, если операция закончилась удачей
Тут вскрылось 2 проблемы:
1) В коде стоял какая-то хитровыебанная самописная аннотация @LogStage, которая там что-то логгировала и как только мы в PostConstruct добавили создание папки, то метод адаптера, создающий папку, на котором и стояла данная аннотация начал валиться с EmptyStackException, ибо он там пытался залоггировать весь стек-трейс. Я поступил просто - убрал нахер аннотацию и все, а проблемой занимался уже автор данной поделки. Это как раз пример того, почему Юра Веретельников говорил, что вся эта магия с пользовательскими аннотациям это зло.
2) Вторая проблема вскрылась в тестах. Мокито-тесты не могут мокать еще не инициализированные бины в пост-конструкте. Помогла вот такая штука:
Следующая проблема - Крон. Там свой язык, причем разные кроны еще слегка отличаются друг друга. Пришлось подолбаться, чтобы правильно указать время, а потом еще все это протестировать.
Да, совсем рассказать про серьезную проблему, которая вскрылась до этого. У Кафки есть интересная особенность. Если зависнуть в дебаге на листенере, через какое-то время оффсет отъезжает назад. Я не знал про эту штуку и постоянно получал дубликаты сообщений, которые я уже отправил. От этого в какой-то момент руки опустились совсем.
Когда все это порешали, встала задача, как собрать все разбитые по топикам для второго листенера документы в один? Пришлось лихорадочно изучать Kafka Streams, который из себя представляет целый мир. Условно, это штука, которая позволяет из топика весь поток сообщений представить в виде таблицы, первичный ключ для которой - ключ сообщения. Таким образом закинув первичное сообщение и соединив его с пришедшими во вторую очередь документами можно определить по ключу - одного поля ли это ягоды и все ли дошли. Тут же определяем, если хоть одно сообщение имеет статус fail - фейлим весь пакет. Если нет - кидаем в третью очередь, которую читает комбайнер - листенер, соединяющий архивы в один. Листенер третьей очереди получает пришедший на вход список временных ZIP-ов и список еще не вытащенных доков. Напомню: доки - это доки, во времеменных ZIP-ах у нас протоколы подписей. Дальше принимаем все это в нашем реактивном сервисе, вытаскиваем по пришедшим айдишникам сами доки и кладём в финальный архив... да, вспомнил, что часть методов в проекте возвращает Flux<DataBuffer>, с которым я никогда не работал. Как это все конвертить в InputStream не понятно на начальном этапе. Достаем контент каждого документа, пуляем stream() в цикле по полученному списку временных архивов, достаем ZipEntry, подкладываем в новый финальный ZIP. Алиллуя!
Забегая вперед скажу: проект долетает до QA, проходит позитивный сценарий и тут всплывает бага. Оказывается, если пользователь присылает 10 одинаковых айдишников, мы должны положить 10 одинаковых документов в архив. Как быть - я же тащу из временных архивов ZipEntry. Проблема первая - как вытащить байты?
В итоге решил вот таким хитрым способом:
Но как быть с переименованием?
Обратите внимание на строчку: val newProtocolName = prepareName(protocolName, names)
В итоге то, что я написал, ТимЛид откаментил: "ты тут какой-то целый framework написал, ну да ладно" и прожал approve:
Так же напомню, что есть еще и четвертый топик и четвертый листенер, в который улетает сообщение после сборки финального архива. Туда прилетает все тот же список айдишников временных ZIPов. Задача - подчистить эти архивы за собой.
И дальше начинается метание туда-сюда на ревью:
- сначала мы переезжаем на обычный InputStream.
- потом шеф пишет, что копировать в память не гуд и мы начинаем переезжать на какое-то копирование через буффер.
- потом возвращаемся назад на трубы, которые были в проекте до меня
В итоге я переписываю все на обычные трубы вот так:
И сама реализация:
И внутри самого processZip имеем:
И помимо этого еще более 100 камментов общего характера по рефакторингу и всему такому. Итого проект едет на НТ. Пуляем 500 запросов в очередь - проходит. Алиллуя! Код поехал на ИФТ, а я пишу эту статью. Пока это самый сложный, высокоагруженный, интересный и полезный с точки зрения прокачки скиллов проект в моей жизни.