Необходимые инструменты: docker compose, git, make, go 1.24+, golangci-lint
- Клонирование репозитория
git clone [email protected]:golchanskiy23/Reviewers_Puller.git- Переход в целевую директорию
cd ./Reviewers_Puller- Запуск микросервиса
docker-compose up- docker-up: подъём контейнеров базы данных и сервиса
- lint: запуск линтеров
- unit-test: запуск юнит тестов
- integration-test: запуск интеграционных тестов
- build: получение бинарника
- run: запуск сервера микросервиса
- clean: остановка и удаление контейнеров
- start: по очереди запускает все команды - входная точка программы
Команды видны в Makefile репозитория.
make start
- POST /team/add — создать команду и участников
- GET /team/get — получить информацию о команде
- POST /users/setIsActive — установить активность пользователя
- GET /users/getReview — получить PR’ы, где пользователь назначен ревьювером
- POST /pullRequest/create — создать PR и автоматически назначить ревьюверов
- POST /pullRequest/merge — пометить PR как MERGED
- POST /pullRequest/reassign — переназначить ревьювера на другого пользователя
- GET /metrics - собирает актуальную статистику по числу PR для каждого участника и о числе участников для каждого PR
- GET /loadtest?freq&duration - нагрузочное тестирование через vegeta(freq-частота запросов в секунду, duration - время "атаки" сервера)
- Пример запроса:
curl "http://localhost:8080/loadtest?freq=10&duration=3s" - POST /users/deactivate - массовое изменение статуса на false нескольких участников одной команды
- Входной формат данных следующий:
{ "users": [ { "user_id": "u1", "username": "Alice", "team_name": "Backend", "is_active": true }, { "user_id": "u2", "username": "Bob", "team_name": "Backend", "is_active": false } ], "flag": false }
- Сервер автоматически выключается спустя n секунд(shutdown_timeout); значение указано в config.yml(по умолчанию 300 с). Предоставление дополнительного контроля на временем работы сервера
- Использование Prometheus + SQL queries как оптимальной реализации эндпоинта статистки, запускаемого по вызову с
/metrics - Реализован эндпоинт /loadtest, который запускает генератор нагрузочных тестов, реализованный на Go с использованием библиотеки vegeta
- Добавлены дополнительные константы возвращаемых кодов ошибок, для более точного логирования. В основном описывают ошибки при входной валидации
Ниже — компактный список реальных проблем, замеченных в процессе разработки, и объяснение, как каждая из них была решена.
-
Проблема: Каждый SQL-запрос выполняется в отдельной горутине и требует свободного соединения из пула. При росте нагрузки горутины начинали стремительно создавать коннекты, что перегружало базу. В моменты пиков это превращалось в шторм подключений, который мог положить БД.
- Решение: я добавил ограничение MaxPoolSize — максимальное количество активных соединений. Это нормализовало доступ к базе: теперь ни приложение, ни база не могут быть перегружены всплескам.
-
Проблема: нечёткая логика остановки сервера и возможные утечки ресурсов при завершении.
- Решение: обёртка
Serverс каналом ошибок иshutdownTimeoutдля graceful shutdown, а также с ограничением времени на чтение и запись. Запускается в отдельной горутине. Благодаря свойству блокировки на чтение небуферизированных каналов и select{} сервер может быть отключён как самими пользователем, так и поshutdownTimeout, так и в ходе ошибки. В данном решении широко применяются контексты, ведь именно они и управляют временем жизни горутин.
- Решение: обёртка
-
Проблема: без метрик невозможно объективно оценить загруженность сервиса.
- Решение: реализовать Prometheus-эндпоинт, который при обращении выполняет SQL-запросы к таблицам pull_request и pr_reviewers для подсчёта количества открытых PR на каждого ревьювера. Полученные данные формируются в метрики и возвращаются через /metrics. Данные также сохраняются в внутренний репозиторий, откуда могут быть использованы при следующем запросе или для анализа динамики.
-
Проблема: при моделировании поведения работы сервера с несколькими "злонамеренными" клиентами(открытие нескольких соединений и побайтовая отправка сообщений через длинные интервалы) он начинал зависать из-за медленных клиентов
- Решение: каждый такой клиент удерживает горутину и TCP-соединение. Если это не ограничивать, сервер постепенно забивается зависшими соединениями и начинает реагировать всё хуже. Поэтому я ввёл таймауты HTTP-сервера: ReadTimeout — ограничивает время чтения запроса, WriteTimeout — ограничивает время отправки ответа клиенту. Так сервер перестает удерживать горутины бесконечно, а медленные соединения автоматически освобождаются. Это стабилизирует работу под нагрузкой.
-
Проблема: при более тонкой настройки сервера/хранилища данных появляется всё больше параметров, образующих "телескопический"(линейно-возрастающий конструктор) конструктор, что приводит к огромным конструкциям, причём при добавлении новых элементов, так как все параметры жёстко зашиты в код, приходится менять API
- Решение: применение паттерна функциональных опций: он позволяет ослабить связность, добавлять только необходимые параметры и строить гибкие конструкторы, расчитанные на расширение, а не изменение.
-
Проблема: большая точка входа и тесная связность зависимостей в
main: функция сильно разрастается и содержит чересчур много бизнес-логики из-за чего дальнейший код становится слабочитаемым- Решение: использование паттерна «Фасад» - формирование единой точки входа в сервис —
Repository. Это уменьшило видимость деталей реализации вmain. агрегирует интерфейсы репозитория и передаётся как единая зависимость в сервисы/хендлеры. Это упростило мокирование в тестах.
- Решение: использование паттерна «Фасад» - формирование единой точки входа в сервис —
-
Проблема: в проекте становится трудно контролировать создание и конфигурацию пула соединений PostgreSQL,корректный старт и остановку базы при запуске приложения, переинициализацию и очистку БД в тестах. Таким образом код БД рассредоточен по проекту.
- Решение:
DatabaseSource— абстракция вокругpgxpool.Pool, отвечающая за всю инфраструктурную работу базы данных, точнее за жизненный цикл соединений, попытки retry с помощью jitter при недступности базы данных.
- Решение:
-
Проблема: при запуске docker compose приложение иногда пытается подключиться к PostgreSQL до того, как база успела подняться.
- Решение: добавлены healthchecks на PostgreSQL. В приложении предусмотрено подключение с retry (backoff + jitter), благодаря этому не нужен sleep в entrypoint, и система становится надёжнее.
-
Проблема: любые коды, отличные от 2xx расцениваются vegeta как некооректный ответ, даже если запрос отработал корректно и вернул ожидаемый код.
- Решение: Написание функции на go, обрабатывающей все случаи возвращаемых значений, содержание потокобезопасной map для users, teams, pull requests. При этом нужно хранить дополнительные мапы с ожидаемыми значениями и кодами, чтобы сравнивать их с приходящими ответами на запросы, таким образом тесты отражают ожидаемое поведение сервера и показывают, что обработка ошибок происходит корректно.
-
Проблема: из-за быстрого роста объёма проекта я стал замечать отсутствие единообразия стиля кода, наличие неприметных ошибок, вроде переопределения переменной во вложенной области, а также на местами сложный и запутанный код.
- Решение: использование линтера
golangci-lint, включающего в себя множество других линтеров. Я добавил сразу все, а после стал проходиться по ним и выбирать только те, что потенциально могут пригодиться в проекте. Среди них оказались shadow, revive, nestif, govet и др. После я запускалgolangci-lintфлагом --fix для возможности автоматических исправлений, вроде форматирования, а затем правил вручную. Таким образом код стал "следовать" чётко-определённым правилам, которые позволили заметно улучшить читабельность и уменьшить сложность.
- Решение: использование линтера
-
Проблема: хранение приватных данных. Польовательские данные учавствуют во многих авторизационных процессах, тем самым попадая в общий резпоиторий, становлясь общедоступными.
- Решение: для хранения секретов(имя, пароли) я использовал фиктивный
.envфайл из которого вызывал переменные. В репозиторий он отправился с фиктивными переменными, которые пользователь изменит при локальном развёртывании сервисов. Для нечувствительных настроек, описывающих параметры структур я использовалconfig.yml, который легко конвертируется в необходимую структуру через библиотеку viper.
- Решение: для хранения секретов(имя, пароли) я использовал фиктивный
-
Проблема: зачастую сервисам необходимы несколько репозиториев, поэтому каждую зависимость приходилось прокидывать отдельно, что сильно увеличивало сигнатуру функций, а при масштабировании привело бы лишь к большему числу аргументов
- Решение: из репозитория каждой доменной области можно выделить функции, образующие контракт - интерфейс репозитория. Совокупность таких интерфейсов позволила сделать конструкторы чище, организовать централизованное управление доступом к данным, что упрощает разработку
-
Проблема: повторные попытки подключения вызывают одновременные всплески нагрузки (thundering herd).
- Решение: экспоненциальный бэкофф с jitter. Вместо постоянного интервала он меняется экспоненциально с добавлением случайности, что уменьшает вероятность одновременных запросов и делает поведение устойивее.
-
Проблема: атомарность сложных операций (например, массовая деактивация пользователей и перераспределение ревью)
- Решение: использование транзакций в репозитории,(
MassDeactivateAndReassign) — обновления, удаления и возможные вставки выполняются внутри транзакции, rollback при ошибке. То есть блокируемся в некоторой области, и все за все операции отвечает транзакция, что позволяет недопустить аномалий в базах данных вроде фантомного чтения.
- Решение: использование транзакций в репозитории,(
-
Проблема: при выполнении долгих SQL-операций могло «зависать» соединение.
- Решение: все операции помещены под context.Context; чтение и запись имеют явные дедлайны, в случае отмены клиентским запросом — запросы в БД также корректно отменяются.
-
Неоптимальная компоновка структур в Go приводила к паддингу, который компилятор добавляет для выравнивания полей. Такие структуры занимали больше RAM, чем нужно, что особенно заметно при большом количестве экземпляров
- Решение: применение подсказок
govet(fieldalignment) — вызов golangci-lint run --fix , позволяющий перетасовать поля в некоторых структурах , тем самым уменьшив используемую память.
- Решение: применение подсказок