Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

PR Reviewer Assignment Service

Сервис назначения ревьюеров для Pull Request’ов по спецификации openapi.yaml.


Архитектура и слои

Сервис написан на Go, использует PostgreSQL в качестве хранилища и запускается через docker-compose up.

Слои:

  • HTTP (internal/api/http)

    • Обработчики (handlers) принимают/отдают JSON, парсят вход, делают базовую валидацию (обязательные поля).
    • Перекладывают всё в сервисный слой (никакой бизнес-логики внутри handler’ов).
    • Маппинг доменных ошибок (AppError) на HTTP-коды строго по спецификации.
  • Сервисный слой / usecase (internal/api/service)

    • Содержит все бизнес-правила:
      • выбор активных ревьюеров при создании PR;
      • проверка статуса PR перед merge/reassign;
      • логика переназначения ревьюера на активного участника той же команды с учётом ограничений;
      • логика массовой деактивации и безопасного переназначения;
      • подсчёт статистики.
    • Репозитории спрятаны за методами сервисов — удобно тестировать бизнес-логику отдельно.
  • Сущности (internal/entity)

    • Модели предметной области: User, Team, TeamMember, PullRequest, PullRequestShort, Stats, DeactivateResult, AppError и т.п.
    • Enum-подобные коды ошибок (ErrorCode), чтобы не разъезжались строковые значения между слоями.
  • Репозитории (internal/repository)

    • Инкапсулируют доступ к БД: SQL-запросы, сканы структур.
    • Для операций, которые должны быть атомарными, есть методы вида ...Tx (работают внутри переданной транзакции, не создают её сами).
  • Инфраструктура (internal/db, internal/app)

    • internal/db/postgres.go — создание *sql.DB, настройки пула соединений.
    • internal/app/app.go — сборка зависимостей (репозитории → сервисы → handler), настройка HTTP-сервера, middleware и запуск.
    • cmd/server/main.go — точка входа, просто вызывает app.Run().

Описание

Сервис позволяет:

  • управлять командами и пользователями;
  • создавать Pull Request’ы и автоматически назначать до двух ревьюеров из команды автора (только активные, без автора);
  • выполнять переназначение одного ревьювера на случайного активного участника его команды;
  • помечать PR как MERGED (операция идемпотентна);
  • получать список PR’ов, где пользователь назначен ревьювером;
  • (доп. задание) массово деактивировать пользователей команды и безопасно переназначать ревьюеров в открытых PR;
  • (доп. задание) получать простую статистику по назначениям;
  • работать через HTTP API, описанное в openapi.yaml.

Допущения и принятые решения

В задании явно указано, что при отсутствии ответа в условиях кандидат может самостоятельно принимать решения и зафиксировать их в README. Ниже перечислены ключевые допущения, которые были сделаны в этом проекте.

1. Следование openapi.yaml и расширения API

  • Что строго соблюдаем:

    • Все описанные в openapi.yaml эндпоинты реализованы по спецификации:
      • те же пути, методы, структуры запросов и ответов;
      • формат ошибок:
        { "error": { "code": "...", "message": "..." } }
      • коды ошибок (TEAM_EXISTS, PR_EXISTS, PR_MERGED, NOT_ASSIGNED, NO_CANDIDATE, NOT_FOUND).
  • Что добавлено сверх спецификации:

    • Эндпоинт статистики /stats (доп. задание).
    • Эндпоинт массовой деактивации /team/deactivateAndReassign (доп. задание).

2. Маппинг ошибок и HTTP-коды

  • Вся доменная ошибка представлена через AppError{Code, Message} (слой entity).
  • Маппинг на HTTP-коды:
    • TEAM_EXISTS → 400 Bad Request (/team/add уже существует).
    • PR_EXISTS, PR_MERGED, NOT_ASSIGNED, NO_CANDIDATE → 409 Conflict (нарушение доменных правил).
    • NOT_FOUND → 404 Not Found (PR, команда или пользователь не найдены).
    • BAD_REQUEST → 400 Bad Request (ошибка валидации тела запроса).
    • Любая нераспознанная ошибка → 500 Internal Server Error.

3. Автоназначение ревьюверов при создании PR

  • Ревьюверы выбираются из команды автора.
  • Выбираются только пользователи с is_active = true.
  • Автор PR никогда не может быть ревьювером самого себя.
  • Максимум 2 ревьювера:
    • если доступных ≥ 2 — назначаем двух;
    • если доступен только один — назначаем одного;
    • если никого нет — PR создаётся без ревьюверов (пустой список).

4. Поведение merge (идемпотентность)

  • Если PR в статусе OPEN:
    • статус переводится в MERGED,
    • выставляется mergedAt,
    • возвращается обновлённый PR.
  • Если PR уже в статусе MERGED:
    • состояние не меняется,
    • возвращается существующее состояние PR,
    • HTTP-код — 200 OK (это и есть идемпотентность).
  • Если PR с таким pull_request_id не существует → 404 NOT_FOUND.

Почему так:
Потому что логично отличать «повторный merge существующего PR» от «merge несуществующего PR».

5. Поведение reassign и массовой деактивации

Обычное reassign

  • Переназначать можно только существующий PR.
  • Если PR уже MERGED → ошибка PR_MERGED (HTTP 409).
  • Переназначать можно только пользователя, который реально назначен ревьювером для этого PR:
    • иначе — NOT_ASSIGNED (409).
  • Новый ревьювер выбирается:
    • из активных пользователей команды заменяемого ревьювера;
    • исключая:
      • самого автора PR,
      • уже назначенных ревьюверов.
  • Если доступных кандидатов нет → NO_CANDIDATE (409).

Массовая деактивация /team/deactivateAndReassign

  • Все указанные пользователи в команде помечаются как is_active = false.
  • Для всех открытых PR этой команды, где деактивированные пользователи были ревьюверами:
    • пытаемся найти замену по тем же правилам, что и в обычном reassign;
    • если замены нет — этот ревьювер просто удаляется из PR.
  • PR в статусе MERGED не изменяются.

Считаем, что:

  • изменения в MERGED PR недопустимы (они уже зафиксированы),
  • отсутствие возможного ревьювера лучше обработать удалением, чем падением всей операции.

6. Работа с БД и транзакциями

  • Для операций, где важна атомарность:

    • /team/add (создание команды + upsert пользователей),
    • /pullRequest/create (создание PR + назначение ревьюверов),
    • /pullRequest/merge,
    • reassign,
    • deactivateAndReassign,

    используется единая транзакция.

  • Репозитории не создают транзакций сами — они принимают *sql.Tx (...Tx-методы) и работают в рамках уже открытой транзакции.

Решение о границах транзакции — часть бизнес-логики, а не репозитория. Это упрощает тестирование и соблюдает принцип единой ответственности.

7. Сидинг данных и поведение при docker-compose up

  • При docker-compose up:
    • поднимается Postgres,
    • применяются миграции,
    • поднимается сервис.
  • Тестовые данные НЕ создаются автоматически.
  • Генерация данных (20 команд, ~200 пользователей, ~1000 PR) выполняется отдельным сидером (go run ./cmd/seed), описанным в README.

Почему я решил так:

  • Можно использовать сервис с чистой базой в других средах,
  • Можно воспроизводить конкретные кейсы.

При этом наличие скрипта и инструкции позволяет легко воспроизвести окружение для нагрузочного тестирования.

8. Конфигурация и .env

  • Все чувствительные данные (DSN для БД и т.п.) берутся из переменных окружения.
  • В репозитории хранится только .env.example с тестовыми значениями.
  • Реальный .env создаётся локально (cp .env.example .env) и не коммитится.
  • В docker-compose.yml есть дефолтные значения для переменных через ${VAR:-default}, но они считаются «локальными» и могут быть переопределены в реальной среде.

Таким образом

  • не ломается запуск docker-compose up «из коробки»;
  • но при этом не хранятся реальные секреты в репозитории.

9. Логирование и request-id

  • Для каждого HTTP-запроса в middleware генерируется request_id (UUID).
  • В логах все сообщения завязаны на request_id, чтобы в будущем их можно было собрать централизованно и трассировать сценарии
  • Логируем:
    • ошибки сервисов и неожиданные ситуации (500),
    • доменные ошибки через writeDomainError (без чувствительных данных),
    • факт старта приложения и основные параметры (порт, лимит памяти, GOMAXPROCS).

Это просто best practice, я посчитал нужным и полезным включить

10. Работа с ограничениями по данным и SLI

  • Проект ориентирован на объём:

    • до 20 команд,
    • до 200 пользователей,
    • до ~1000 PR.
  • С учётом таких объёмов:

    • не используется кэш, достаточно одной PostgreSQL;
    • подобраны индексы по ключевым полям (pull_request_id, user_id, team_name, статусам и связям).
  • SLI из условия:

    • RPS ≈ 5,
    • время ответа ≤ 300 мс,
    • успешность 99.9%

    проверялись через hey на сгенерированных данных. Реальные результаты (p95/p99 и успешность) приведены в разделе нагрузочного тестирования.

Почему так:
на таких объёмах разумнее сделать код простым и честно придерживаться SLI, чем усложнять архитектуру кэшем / шардированием, которые в данном масштабе не нужны.

Структура проекта

Упрощённое дерево (главные директории):

.
├── cmd
│   ├── server
│   │   └── main.go         # запуск HTTP-сервера
│   └── seed
│       └── main.go         # сидер для генерации тестовых данных через HTTP
├── internal
│   ├── app
│   │   └── app.go          # сборка и запуск приложения 
│   ├── api
│   │   ├── http
│   │   │   ├── handler.go  # роутер + регистрация эндпоинтов
│   │   │   ├── pr.go       # обработчики PR (/pullRequest/*)
│   │   │   ├── team.go     # обработчики команд (/team/*)
│   │   │   ├── user.go     # обработчики пользователей (/users/*)
│   │   │   ├── stats.go    # обработчик статистики (/stats)
│   │   │   ├── responses.go #WriteJSON, writeError, writeDomainError
│   │   │   └── middleware.go # логирование + request-id
│   │   └── service
│   │       ├── team_service.go   # логика по командам
│   │       ├── user_service.go   # логика по пользователям
│   │       ├── pr_service.go     # логика по Pull Request’ам
│   │       └── stats_service.go  # логика для ст_

Линтёр

Для проверки стиля и базовых ошибок используется golangci-lint.

Конфиг лежит в файле .golangci.yml в корне проекта.

  • govet — поиск типичных ошибок (подозрительные конструкции, неправильное использование Printf, и т.п.);
  • staticcheck — набор семантических проверок;
  • gosimple — замечания по чрезмерно усложнённым конструкциям;
  • errcheck — проверка, что ошибки не игнорируются;
  • ineffassign — поиск неиспользуемых присваиваний;
  • gofmt / goimports — автоформат и порядок импортов.

Запуск и проверка

Прописываем docker-compose up, при условии что запущен docker desktop ( 0_0 я в моменте не увидел когда впервые собирал, что он вырублен)

Не забываем тогда создать локально .env (можно просто сделать cp .env.example .env)

Потом чтобы заполнить БД тестовыми данными используем сидер

go run ./cmd/seed

После этого можем кидать запросы, смотреть ответы - я использовал hey и об результатах расписал в следующем разделе

ВАЖНО для следующей секции, тесты с hey написаны так чтобы работать с cmd в windows, чтобы они работали в линуксовском терминале нужно убрать оборачивание кавычек

Выводы тестов которые я прогнал сам

  1. тест на read - hey -z 60s -q 5 -c 5 "http://localhost:8080/users/getReview?user_id=team-01-u-01"

Summary: Total: 28.4082 secs Slowest: 0.0522 secs Fastest: 0.0027 secs Average: 0.0073 secs Requests/sec: 24.9928

Total data: 790230 bytes Size/request: 1113 bytes

Response time histogram: 0.003 [1] |

0.008 [498] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

0.013 [174] |■■■■■■■■■■■■■■

0.018 [26] |■■

0.022 [3] | 0.027 [3] | 0.032 [2] | 0.037 [1] | 0.042 [0] | 0.047 [1] | 0.052 [1] |

Latency distribution: 10% in 0.0044 secs

25% in 0.0054 secs

50% in 0.0064 secs

75% in 0.0083 secs

90% in 0.0109 secs

95% in 0.0128 secs

99% in 0.0234 secs

Details (average, fastest, slowest): DNS+dialup: 0.0001 secs, 0.0027 secs, 0.0522 secs

DNS-lookup: 0.0001 secs, 0.0000 secs, 0.0193 secs

req write: 0.0001 secs, 0.0000 secs, 0.0088 secs

resp wait: 0.0067 secs, 0.0026 secs, 0.0314 secs

resp read: 0.0002 secs, 0.0000 secs, 0.0121 secs

Status code distribution: [200] 710 responses

2)Тест на merge hey -z 30s -q 5 -c 5 -m POST -H "Content-Type: application/json" -d "{\"pull_request_id\":\"pr-0002\"}" "http://localhost:8080/pullRequest/merge"

Summary: Total: 17.8319 secs Slowest: 0.0687 secs Fastest: 0.0042 secs Average: 0.0099 secs Requests/sec: 24.9553

Total data: 111250 bytes Size/request: 250 bytes

Response time histogram: 0.004 [1] |

0.011 [318] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

0.017 [101] |■■■■■■■■■■■■■

0.024 [16] |■■

0.030 [1] |

0.036 [6] |■

0.043 [0] |

0.049 [0] |

0.056 [0] |

0.062 [0] |

0.069 [2] |

Latency distribution:

10% in 0.0058 secs

25% in 0.0067 secs

50% in 0.0085 secs

75% in 0.0117 secs

90% in 0.0151 secs

95% in 0.0180 secs

99% in 0.0348 secs

Details (average, fastest, slowest):

DNS+dialup: 0.0003 secs, 0.0042 secs, 0.0687 secs

DNS-lookup: 0.0002 secs, 0.0000 secs, 0.0214 secs

req write: 0.0001 secs, 0.0000 secs, 0.0164 secs

resp wait: 0.0092 secs, 0.0041 secs, 0.0685 secs

resp read: 0.0001 secs, 0.0000 secs, 0.0078 secs

Status code distribution: [200] 445 responses

  1. Тест на Доп.задание DeactivateReassign >hey -z 30s -q 4 -c 4 -m POST -H "Content-Type: application/json" -d "{\"team_name\":\"team-01\",\"user_ids\":[\"team-01-u-05\",\"team-01-u-06\"]}" "http://localhost:8080/team/deactivateAndReassign"

Summary:

Total: 30.0143 secs

Slowest: 0.0771 secs

Fastest: 0.0035 secs

Average: 0.0075 secs

Requests/sec: 15.9924

Total data: 49441 bytes

Size/request: 103 bytes

Response time histogram: 0.003 [1] |

0.011 [433] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

0.018 [39] |■■■■

0.026 [2] |

0.033 [1] |

0.040 [0] |

0.048 [0] |

0.055 [0] |

0.062 [0] |

0.070 [0] |

0.077 [4] |

Latency distribution:

10% in 0.0044 secs

25% in 0.0051 secs

50% in 0.0060 secs

75% in 0.0084 secs

90% in 0.0108 secs

95% in 0.0125 secs

99% in 0.0718 secs

Details (average, fastest, slowest):

DNS+dialup: 0.0002 secs, 0.0035 secs, 0.0771 secs

DNS-lookup: 0.0002 secs, 0.0000 secs, 0.0201 secs

req write: 0.0001 secs, 0.0000 secs, 0.0074 secs

resp wait: 0.0070 secs, 0.0033 secs, 0.0557 secs

resp read: 0.0001 secs, 0.0000 secs, 0.0085 secs

Status code distribution:

[200] 480 responses

В целом видно что по требованиям к времени и rps укладываемся с большим запасом

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages