Ifunny
Решение для конкурса FunCode Java/Kotlin Challenge
Install / Use
/learn @ruslanys/IfunnyREADME
iFunny
Сайт: https://ifunny.ruslanys.me
Задание: https://habr.com/ru/company/funcorp/blog/481814/
Преамбула
Во-первых, хочу поблагодарить организаторов за конкурс. Нужно сказать, что мне вообще всегда нравилось скрапить сайты. Я даже гонял обфусцированный код на JS, который удалось выдрать с фронта ВК, под Nashorn, чтобы декодировать путь до MP3 файлов. Закончилось, правда, тем, что я устал играть в догонялки и выпускать апдейты и подзабил.
Во-вторых, я не часто участвую в каких-либо конкурсах, потому что я фанатик. Я очень увлечённый программированием человек. Да и вообще, в принципе, увлекающийся человек. Последний раз я участвовал в первом Highloadcup в 2017. Моё решение было пятым среди Java, но ниже 50го в общем зачёте. И должен сказать, FunCode Challenge умнее, сложнее, реальнее и интереснее. В Highloadcup дрочишь профайлер две недели, а побеждает Си на костылях с epoll(0). Ну бред.
Разрабатывая это решение, азарт был не просто в конкурсе, а в самом проекте, в идее, в сути. Это не академическая задача, а вполне реальная и совсем нетривиальная. Хотя это не очевидно.
Можно ли представить, что решение для того же Highloadcup будет развёрнуто на личных серверах просто ради удовольствия? Не думаю. Разворачивая это решение я очень боялся, что каааак выкачаю сейчас все интернеты и мне не хватит либо места, либо денег. Но уже через несколько дней появилась какая-то страсть, азарт, жажда накачать больше и больше. И уже сотни гигабайт не стали казаться большим объемом. Никогда бы не подумал, что у меня будет под полтерабайта мемов на немецком, французском и прочих незнакомых мне языках.
Я уже довольно давно пишу на Kotlin, но к своему стыду совсем не пробовал корутины. Я понимаю как они работают, что такое реактивный подход и что такое NIO. Откровенно говоря, NIO вообще было темой моей дипломной работы в своё время. Но почему-то хайп относительно этого подхода случился совсем недавно. Кстати, по этой же причине, я считаю, можно наблюдать рост популярности декларативного (функционального) программирования. И мне не до конца очевидны причины такого ажиотажа. Почему именно сейчас? Все это было доступно уже давно.
Понятное дело, что есть проекты, нуждающиеся в NIO, которые то и делают, что тратят треды на блокировку IO. Но асинхронный код труднее писать, поддерживать, понимать. У человека вообще склад ума таков, что параллельные и асинхронные взаимодействия даются трудно к восприятию. Однако Грейс Мюррей еще в 1962 году писала: «Совершенно очевидно, что нам следует отказаться от последовательного выполнения операций и не ограничивать компьютеры им. Мы должны формулировать определения, расставлять приоритеты и давать описания данных. Мы должны формулировать связи, а не процедуры.»
Абсолютно очевидно, что данный проект — идеальный кандидат на реализацию реактивного подхода, т.к. преимущественно занимается сетевыми вызовами и ожиданием операций ввода-вывода.
Однако лучше Tomcat в руках, чем Netty в небе, решил я и взялся за саму задачу на блокируемых потоках, как привычно, а дальше запланировал готовое приложение перенести на NIO. Хватило бы времени...
Так появилось две версии приложения:
Я бы очень хотел, чтобы обе версии были взяты во внимание. Несмотря на то, что блокировка потоков для работы с сетью – не лучшая идея, в целом я доволен реализацией первой версии. Она, кстати, получилась даже весьма эффективной.
P.S. Сорри за лонгрид. Люблю обсуждать технические детали. Многое хочется рассказать, многим хочется поделиться, а не с кем. Поэтому, буду надеяться, что тебе, читатель, будет не менее интересно это читать, чем мне разрабатывать.
Сборка
Сборка приложения
Для того, чтобы собрать приложение, необходимо иметь предустановленную JDK ≥ 8 и выполнить следующую команду:
$ ./gradlew assemble
Сборка приложения с тестами
Дело в том, что тесты, требующие БД производятся против реального сервера БД. На мой личный взгляд это один (или самый) из самых надежных способов тестирования.
Сложность заключается лишь в развёртывании необходимого окружения. В случае с CI/CD, GitLab Pipeline настроен таким образом, что перед сборкой поднимаются нужные сервисы и линкуются с тестируемым образом.
Для того, чтобы локально добиться того же результата, был создан docker-compose.yml файл.
Итак, для сборки приложения с запуском всех тестов локально необходимо выполнить две команды:
$ docker-compose up -d
$ ./gradlew build
Сборка Docker образа
Прежде, чем приступить к сборке Docker образа необходимо собрать само приложение. Необходимые шаги подробно описаны в соответствующем разделе.
Как только приложение создано, сборка Docker образа становится тривиальной задачей:
$ docker build -t ifunny .
Версионирование
Приложение версионируется на основе git тагов. Так, если последний коммит затаган с префиксом v, например v1.0.0,
тогда версия приложения будет равна имени тага без префикса: 1.0.0.
Однако если последний коммит впереди, то минорный разряд будет увеличен на единицу и добавлен постфикс -SNAPSHOT.
Например: 1.1.0-SNAPSHOT.
Запуск
Системные требования
Во-первых, при запуске JVM в Docker-контейнере есть свои особенности. Конечно, вы наверняка знаете о них. Но если вкратце – необходимо явно задавать ограничения приложению на ресурсы через опции JVM.
Обратите внимание на заданные параметры JVM из Dockerfile:
-Xms2G/-Xmx2G– размер кучи (heap'а). Минимальное рекомендуемое значение 2 ГБ в случае 8 ядерного (или меньше) процессора. В случае, если ядер больше, памяти нужно больше.-XX:MaxDirectMemorySize=1G– размер нативной памяти. Дело в том, что Netty большой любитель Unsafe и нативных буферов, поэтому этот параметр нужно обязательно задать, чтобы не словить сюрпризов. Минимальное рекомендуемое значение 1 ГБ, хотя на деле должно работать и с меньшим количеством нативной памяти (от 512 МБ).-XX:MaxMetaspaceSize=256M– для того, чтобы в продакшене не было сюрпризов, устанавливаем размер Metaspace раздела. 256 МБ достаточно.
Примечание.
С указанными выше параметрами я запускал приложение в Docker с приказом пристрелить, если потребляемая память пересекает границу в 3500 МБ.
Примерно через 6-7 часов все разработанные источники были полностью проиндексировны, используя одну виртуальную машину с 2 ядрами ЦП и 4 ГБ оперативной памяти.
Переменные окружения
MongoDB
MONGODB_HOST– Хост сервера MongoDB (По умолчаниюlocalhost).MONGODB_PORT– Порт сервера MongoDB (По умолчанию27017)MONGODB_DATABASE- Имя базы данных MongoDB (По умолчаниюifunny).MONGODB_USERNAME– Имя пользователя MongoDB (По умолчаниюifunny).MONGODB_PASSWORD– Пароль пользователя MongoDB (По умолчаниюifunny).MONGODB_AUTH_DB– База данных аутентификации MongoDB (По умолчаниюadmin).
AWS S3
AWS_S3_ACCESS_KEY– Ключ доступа к S3.AWS_S3_SECRET_KEY– Ключ доступа к S3.AWS_S3_REGION– S3 Регион.AWS_S3_BUCKET– S3 Bucket.
Если S3 Bucket не существует на этапе запуска приложения, он будет создан автоматически. Но не уверен, что это хорошая практика, поэтому получите warning: лучше контролировать создание корзин и/или делать это ручками.
Redis
REDIS_HOST- Хост сервера Redis (По умолчаниюlocalhost).REDIS_PORT– Порт сервера Redis (По умолчанию6379).REDIS_DB– Индекс БД в Redis (По умолчанию0).SPRING_REDIS_PASSWORD– Если для доступа к Redis требуется пароль, укажите эту переменную.
Локальное окружение
Для удобства развертывания локального окружения (в целях разработки) в корневом каталоге расположен docker-compose.yml.
Запуск через терминал
$ docker-compose up -d
Запуск через IDEA'ю
Если Вы используете Intellij IDEA, в репозиторий добавлена конфигурация запуска Docker-compose под именем Local Environment.
Swagger
Swagger Open API v3 Specification: /contract.yaml.
Swagger UI: /swagger-ui.html.
Можно было бы прикрутить Springdoc (Springfox), как и было в первой версии, который бы сам генерировал спецификацию API из описанных контроллеров, однако я считаю, что лучшее качество у спецификации, описанной человеком. Если уж не говорить про подход Contract-first, где перед реализацией сначала описывается контракт.
Prometheus
Metrics endpoint: /actuator/prometheus.
Реализация
Источники
Источник – веб-сайт с мемами, который является предметом для парсинга и обработки приложением.
Архитектура основана на предположении, что каждый источник возвращает ленту, разделенную на страницы (Pagination).
В объектной модели источник представляет класс Channel (канал).
Для добавления нового источника разработчику необходимо объявить новый бин класса Channel,
реализовав абстрактные методы: pagePath, parsePage, parseMeme.
Изначально была идея реализовать Kotlin DSL, позволяющий как-то простенько конфигурировать новые источники и даже иметь Hot-Reload,
но на деле оказалось, что на короткой дистанции выгода от этого подхода неоднозначна.
Очень упрощенно процесс обработки каждого источника следующий:
- Получаем URL интересующей нас страницы по её номеру используя метод
pagePath. Например,http://debeste.de/123. - Делаем запрос по полученному из предыдущего пункта адресу, получаем содержимое страницы и отправляем в метод
parsePage. Дело в том, что каждый источник очень специфичен и некоторые данные доступны только на этом этапе, а некоторые станут доступны на следующем. Например, в случае сFunpot, на этом этапе доступна дата публикации мема, а на самой странице
Related Skills
node-connect
329.7kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
81.2kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
329.7kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
81.2kCommit, push, and open a PR
