Skip to content

golchanskiy23/Reviewers_Puller

Repository files navigation

Service for assigning reviewers for Pull Requests

Инструкция по запуску

Необходимые инструменты: docker compose, git, make, go 1.24+, golangci-lint

  • Клонирование репозитория
git clone [email protected]:golchanskiy23/Reviewers_Puller.git
  • Переход в целевую директорию
cd ./Reviewers_Puller
  • Запуск микросервиса
docker-compose up

Makefile команды

  • 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 , позволяющий перетасовать поля в некоторых структурах , тем самым уменьшив используемую память.

About

Сервис, который назначает ревьюеров на PR из команды автора, позволяет выполнять переназначение ревьюверов и получать список PR’ов конкретного пользователя.

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages