SkillAgentSearch skills...

Ifunny

Решение для конкурса FunCode Java/Kotlin Challenge

Install / Use

/learn @ruslanys/Ifunny
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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. Хватило бы времени...

Так появилось две версии приложения:

  • 1.X.X на блокируемых потоках
  • 2.X.X на неблокируемых потоках и реактивном подходе

Я бы очень хотел, чтобы обе версии были взяты во внимание. Несмотря на то, что блокировка потоков для работы с сетью – не лучшая идея, в целом я доволен реализацией первой версии. Она, кстати, получилась даже весьма эффективной.

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

Если 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, но на деле оказалось, что на короткой дистанции выгода от этого подхода неоднозначна.

Очень упрощенно процесс обработки каждого источника следующий:

  1. Получаем URL интересующей нас страницы по её номеру используя метод pagePath. Например, http://debeste.de/123.
  2. Делаем запрос по полученному из предыдущего пункта адресу, получаем содержимое страницы и отправляем в метод parsePage. Дело в том, что каждый источник очень специфичен и некоторые данные доступны только на этом этапе, а некоторые станут доступны на следующем. Например, в случае с Funpot, на этом этапе доступна дата публикации мема, а на самой странице

Related Skills

View on GitHub
GitHub Stars24
CategoryDevelopment
Updated1y ago
Forks6

Languages

HTML

Security Score

80/100

Audited on Nov 15, 2024

No findings