https://github.com/x-oc/link-tracker
Отслеживатель ссылок, оповещающий подписчиков об обновлениях на страницах, которые они захотели отслеживать.
https://github.com/x-oc/link-tracker
java junit kafka liquibase mockito postgresql redis resilience4j rest-api spring spring-boot telegrambot testcontainers
Last synced: 2 months ago
JSON representation
Отслеживатель ссылок, оповещающий подписчиков об обновлениях на страницах, которые они захотели отслеживать.
- Host: GitHub
- URL: https://github.com/x-oc/link-tracker
- Owner: x-oc
- Created: 2025-08-31T18:22:42.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2025-09-07T08:24:14.000Z (10 months ago)
- Last Synced: 2025-10-25T15:29:57.752Z (8 months ago)
- Topics: java, junit, kafka, liquibase, mockito, postgresql, redis, resilience4j, rest-api, spring, spring-boot, telegrambot, testcontainers
- Language: Java
- Homepage:
- Size: 195 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Link Tracker
Проект сделан в рамках курса Академия Бэкенда.
Приложение для отслеживания обновлений контента по ссылкам. При появлении новых событий отправляется уведомление в Telegram.
Проект написан на `Java 23` с использованием `Spring Boot 3`.
Для работы требуется БД `PostgreSQL`. Присутствует опциональная зависимость на `Kafka` и `Redis`. Всё это поднимается через `Docker`.
Для дополнительной справки: [HELP.md](./HELP.md)
Для запуска приложения бота, ему требуется передать в качестве переменной окружения токен телеграм бота.
Для запуска скраппера, ему требуется передать в качестве переменных окружения токены и ключи доступа к GitHub и StackOverflow API (опционально).
## Постановка проблемы
Как пользователю, мне приходится посещать множество веб-сайтов: GitHub, StackOverflow, Reddit, HackerNews, YouTube и т.п.
У каждого из сайтов реализован свой механизм оповещения об обновлениях
(если реализован вообще): почта, мобильные push-уведомления, popup-баннеры и т.п.
Чтобы получить все уведомления, мне нужно зайти, в лучшем случае, на все веб-страницы и проверить список оповещений.
В худшем сценарии мне нужно пользоваться механизмом закладок и делать проверку обновлений самостоятельно с какой-то периодичностью.
Я, как пользователь, хочу иметь единый способ отслеживания обновлений для множества ресурсов, которые мне интересны.
## Сценарий использования
Управление подписками на ссылки осуществляется через чат с ботом в Telegram.
Пример сценария использования включает в себя:
1. **Регистрация:** Пользователь инициирует взаимодействие с ботом и проходит процедуру регистрации.
2. **Добавление/удаление ссылок:** Пользователь может ввести команду, чтобы добавить или удалить ссылку для отслеживания
- Команда: `/track https://stackoverflow.com/questions/292357` — для подписки на обновления указанного вопроса.
- Команда: `/untrack https://stackoverflow.com/questions/292357` — для отмены подписки.
3. **Получение уведомлений:** При появлении новых ответов или обновлений по отслеживаемым ссылкам, бот автоматически отправляет уведомление в чат
- Уведомление: "В вопросе https://stackoverflow.com/questions/292357 появился новый ответ: <текст ответа>."
Кроме того, бот поддерживает команды для отображения списка всех текущих подписок и управления ими.
## Техническое описание
Проект состоит из 2-x приложений:
1. **Сервис обработки сообщений пользователей (Bot):** отвечает за регистрацию пользователей и управление их подписками.
Этот сервис обеспечивает возможность добавления и удаления ссылок для отслеживания, а также сохраняет информацию о подписках в `PostgreSQL` через scrapper.
2. **Сервис мониторинга ссылок (Scrapper):** осуществляет периодическую проверку заданных пользователями ссылок на наличие изменений.
При обнаружении обновлений данный сервис отправляет уведомления пользователям в первый сервис по `HTTP` и `Apache Kafka`.
Кроме базового функционала реализована следующая функциональность:
- **Возможность тэгирования ссылок:** создание тэгов по темам или проектам позволит удобно управлять несколькими подписками одновременно.
Например, можно создать тэг "Работа", в который войдут ссылки на профессиональные ресурсы, и тэг "Хобби" для развлечений.
Упрощенная схема работы системы выглядит следующим образом:
```
+-----------------+
+-------------------+ | |
| | /----- Postgres |
| Scrapper |------ | |
| |-- +-----------------+
+-------------------+ \---
| \--- +-----------------+
| \- |
| HTTP | Kafka |
| /- |
| /--- +-----------------+
+-------------------+ /---
| |--
| Bot | +-----------------+
| |------------ |
+-------------------+ | Telegram API |
| |
+-----------------+
```
Разработка проекта велась поэтапно.
## Первый этап
Задача - разработать сетевую часть приложения и основную логику бота.
Был написан скелета бота, добавлены сетевые вызовы согласно OpenAPI-контракту, реализованы HTTP-клиенты (для внешних и внутренних вызовов) и планировщик задач (проверятель ссылок).
Фактически получился работающий MVP: работала отправка сообщений в бота и была простая реакция на сообщения пользователей.
### Функциональные требования
- Бот должен поддерживать следующие команды:
- /start - регистрация пользователя.
- /help - вывод списка доступных команд.
- /track - начать отслеживание ссылки.
- /untrack - прекратить отслеживание ссылки.
- /list - показать список отслеживаемых ссылок (cписок ссылок, полученных при /track)
- Команда /list должна выводить специальное сообщение, если список отслеживаемых ссылок пуст.
- Неизвестные команды должны игнорироваться с уведомлением пользователю.
- При попытке добавить ссылку, которая уже отслеживается, выводится сообщение: "Ссылка уже отслеживается".
- Все endpoint'ы соответствуют OpenAPI-контракту: https://gist.github.com/sanyarnd/e35dc3d4e0c8000205ec5029dac38f5a.
- Реализован базовый планировщик (`@Scheduled`): бот присылает простейшее уведомление (заглушку) в случае обнаружения изменений.
- Общение между приложениями bot и scrapper происходит по HTTP.
- Автоматическая реализация меню команд бота (см API), т.е. бот должен регистировать доступные команды через метод setmycommands при запуске.
### Нефункциональные требования
- Токен авторизации должен храниться в конфигурационном файле, недоступном для общего доступа.
- Поддерживается скреппинг вопросов **StackOverflow** и репозиториев **GitHub** - написаны HTTP-клиенты для получения необходимой информации из API.
- Для установления, что появились обновления, достаточно использовать факт изменения поля, отвечающего за дату последнего изменения (lastUpdated и т.п.).
- Работа ведётся с API: [GitHub](https://docs.github.com/en/rest), [StackOverflow](https://api.stackexchange.com/), - а не с HTML.
- Клиенты написаны руками, без использования готовых SDK для доступа к API для HTTP-клиентов (GitHub, StackOverflow):
- Используются декларативные клиенты.
- Бот реализует концепт машины состояний (создание /track идёт в формате диалога, а не команды):
- \> /track https://foo.bar/baz
- < введите тэги (опционально)
- \> work hobby
- < настройте фильтры (опционально)
- \> user:dummy type:comment
- В тестах не происходят реальные вызовы к API внешних систем, используются заглушки (mocks).
- Используется типобезопасная конфигурация (`@ConfigurationProperties`).
- Используется структурное логирование (добавление key-value значений к логу, вместо зашивания данных в поле message) в коде приложения.
- При создании клиента есть возможность указать базовый URL. При этом если он не указывается, используется URL по умолчанию.
- Итоговый код является тестируемым; используется принцип Arrange-Act-Assert.
- Используется однотипность функций разбора входного сообщения и диспатчеризации по типам команд так, чтобы команды можно было легко добавлять и удалять.
### Тесты
- Корректный парсинг ссылок
- Правильное сохранение данных в репозиторий из запроса пользователя: ссылка, тэги, фильтры
- Бот кидает ошибку, если команда неизвестна
- Happy path: добавление и удаление ссылок из репозитория
- Добавление дубля ссылки
- Планировщик: обновление отправляется только пользователям, которые следят за ссылкой
- Корректная обработка ошибок HTTP-запросов (некорректное тело, код) в клиентах
- Проверка форматирования команды /list
## Второй этап
Задача - написать схему данных, адаптировать код к ней, а также написать два провайдера для работы с БД: на чистом SQL и при помощи ORM.
### Функциональные требования
- Scrapper (планировщик) при отправке сообщения с изменениями включает детализацию данных:
- Для **StackOverflow** новый ответ или комментарий, сообщение включает:
- текст темы вопроса
- имя пользователя
- время создания
- превью ответа или комментария (первые 200 символов)
- Для **GitHub** новый PR или Issue, сообщение включает:
- название PR или Issue
- имя пользователя
- время создания
- превью описания (первые 200 символов)
- Возможность тэгирования ссылок: создание тэгов по темам или проектам позволит группировать подписки. Например, можно создать тэг "Работа", в который войдут ссылки на профессиональные ресурсы, и тэг "Хобби" для развлечений. Соответственно должны появиться операции управления тэгами.
### Нефункциональные требования
- При проверке обновлений запрещено загружать все ссылки в память сразу.
- Логика планировщика (проверка ссылок) и отправки (уведомления) разнесены по разным сервисам.
- Объявлен интерфейс для сервиса отправки уведомлений scrapper -> bot.
- Пока что будет одна реализация - `HTTP` (далее появится `Kafka`).
- Для хранения данных используется Postgres
- Запуск `Postgres` осуществляется:
- через compose-файл для локальной разработки;
- через `Testcontainers` для автоматического тестирования.
- Миграции написаны на языке `SQL`.
- Для миграций используется `Liquibase`.
- Схема БД находится в каталоге migrations/ в корне проекта.
- Запуск миграций выполняется собственноручно написанной функцией.
- Реализованы 2 способа работы с БД: "голый" SQL и ORM.
- Выбор способа работы с БД осуществляется через конфигурацию: access-type=SQL или access-type=ORM.
- Используются индексы на полях, которые используются в `WHERE` условиях.
- Используется пагинация для обработки данных из таблиц, чтобы не перегружать память.
- Данные загружаются пакетами 50..500, размер пакета вынесен в конфиг.
- Настроен интервал задержки между проверками обновлений.
- Условие фильтрации для поиска новых обновлений выполняется непосредственно в SQL-запросах, чтобы минимизировать объем передаваемых данных.
- Бизнес-логика отделена от кода доступа к данным.
- В интерфейсах нет типов, специфичных для определенной имплементации, например, в LinkService не должно быть RowSet, но он может быть в SqlLinkService.
- Для выбора реализации используется `@ConditionalOnProperty`.
- Для работы с БД используется: JDBC + JPA (Hibernate).
### Тесты
- Тесты должны запускать БД в `Testcontainers`.
- Запросы к БД работают ожидаемым образом: вставка, удаление, обновление.
- В зависимости от конфигурации работа идёт через разные имплементации (SQL, ORM).
- Тесты на превью пользовательских сообщений в зависимости от типа обновления.
## Третий этап
До текущего момента сервисы bot и scrapper общались строго синхронно по протоколу `HTTP`.
Но что если в какой-то момент bot станет недоступен и планировщик не сможет отправить уведомление?
Для таких ситуаций часто предпочтительным оказывается асинхронный способ общения.
Дополнительно, чтобы снизить нагрузку на сервис, будем кэшировать некоторые типы запросов в `Redis`.
### Функциональные требования
- Приложения bot и scrapper могут общаться асинхронно при помощи `Apache Kafka`
- Используются compose и `Testcontainers`
- Некорректные (не парсится и т.п.) сообщения отправляются в отдельную очередь dead letter queue
- Все имена топиков задаются в конфигурационном файле
- bot кэширует запросы типа `/list` в `Redis`
### Нефункциональные требования
- Выбор транспорта происходит в конфигурационном файле при помощи свойства `app.message-transport: {Kafka, HTTP}`
- Отправка уведомлений реализована как абстракция (сервис): переиспользуется текущий код, в зависимости от значения в конфигурации сервис должен отправлять сообщения или в очередь, или по `HTTP`
- Уведомления в `Kafka` передаются в формате JSON
- bot делает инвалидацию кэша, если пользователь сделал изменение в списке своих отслеживаемых ссылок (добавил, удалил, изменил)
### Тесты
Тесты должны запускать `Redis`/`Kafka` в `Testcontainers`
- Валидное сообщение вычитывается и обрабатывается
- Невалидные сообщения перенаправляются в DLQ: ошибки парсинга, валидации
- Кэш подключен: он перехватывает обращения на чтение и инвалидируется при изменении данных
- Валидный JSON корректно мапится в DTO
- Для ручного тестирования `Kafka` используется `KafkaUI`
## Четвёртый этап
В рамках этого этапа не будет добавлено новых функций для пользователя, а будет повышена отказоустойчивость системы
с помощью реализации шаблонов Rate Limiting, Timeout, Retry, Circuit Breaker и Fallback.
### Функциональные требования
- Все HTTP-запросы поддерживают Timeout
- Все HTTP-запросы поддерживают Retry
- сконфигурирована поддержка Retry в HTTP-клиентах
- У каждого публичного endpoint'а есть выставленный Rate Limiting на основе IP-адреса клиента
- В случае недоступности сервиса продолжительное время вместо Retry соединение разрывается при помощи Circuit Breaker
- В случае отказа `HTTP` или `Kafka` при отправке уведомлений происходит fallback на альтернативный транспорт
### Нефункциональные требования
- Параметры Timeout настраиваются в конфигурации
- Параметры Rate Limiting настраиваются в конфигурации
- Политика Retry задаётся в конфигурации и позволяет задать количество повторов и время между попытками (constant backoff)
- Retry происходит только в случае если это имеет смысл: в конфигурации задан настраиваемый список кодов, на которые происходит retry
- Параметры Circuit Breaker настраиваются в конфигурации
- Circuit Breaker настроен в режиме скользящего окна, например:
```
slidingWindowSize = 1
minimumRequiredCalls = 1
failureRateThreshold = 100
permittedCallsInHalfOpenState = 1
waitDurationInOpenState = "1s"
```
### Тесты
- Тест на ретраи (см. `Stateful Behaviour у Wiremock`)
- в т.ч. проверка, что ретраи инициируются только для определенных кодов ошибок
- Интеграционный тест на rate limiting
- Тест на Circuit Breaker: ошибка должна возвращаться до того, как превышен таймаут
- Тест на fallback `HTTP` и `Kafka`