Skip to content

Latest commit

 

History

History
149 lines (143 loc) · 18.9 KB

README.md

File metadata and controls

149 lines (143 loc) · 18.9 KB

Web-сервис, выполняющий функцию триггера билдов.

Спецификация

Чтобы вам удобнее было взаимодействовать с сервисом и API, я подготовил yaml-спецификацию с описанием всех endpoint'ов, типов данных и обязательности полей. Взаимодействовать с ней можно одним из способов:

  1. Получение спецификации от самого приложения.
  • Локально запустить приложение через IntellijIdea, либо сбилдив проект в jar.
  • Перейти в браузере по адресу вида:
http://localhost:{PORT}/specification/build-trigger-specification.html

где PORT, собственно - порт, на котором запустились. Например:

http://localhost:8080/specification/build-trigger-specification.html
  • В открывшейся странице можно посылать запросы к сервису через "Try it out", данные будут предзаполнены по примерам. Надо будет только слегка их скорректировать, см. раздел Создание / редактирование триггера.
  1. Скопировать содержимое из /resources/specification/build-trigger-specification.yaml и вставить в онлайн-редактор Swagger, по адресу: https://editor.swagger.io. Чисто для того, чтобы ознакомиться с API, но запросы оттуда не поделаешь, придётся использовать Postman.

Окружение для локального тестирования

Если взаимодействовать с сервисом на 100%, понадобится:

  • Postgres. Выбрал в качестве СУДБ. Если адрес, логин, пароль не дефолтные, настроить в datasource-master.properties.
  • Docker Desktop. Используется для интеграционных тестов, Testcontainers через него запускает БД для тестов. Также через него я запускаю Rabbit MQ.
  • Rabbit MQ. Выбрал в качестве MQ, т.к., на мой взгляд, проще конфигурируется. Если креды не дефолтные, настроить в rabbit-mq.properties.
  • JDK. В своём проекте я установил уровень 11, т.к. более привычен и распространён, в отличие от 17.
  • Тестовый репозиторий на GitHub. Чтобы была возможность потестировать функционал на настоящем репозитории.

Создание / редактирование триггера

Чтобы создать или обновить триггер, идём на /trigger/create или /trigger/update. Передаём все обязательные параметры. Если выбран тип Vcs, дополнительно нужно передать необязательный vcsTriggerData. Если выбран Scheduled, дополнительно передаём scheduledTriggerData.

Если обработка триггера завершилась с ошибкой

  • При процессинге триггера могут случиться исключительные ситуации. Например, недоступен удалённый репозиторий или ветка, которую указали при создании триггера, не найдена в нём. В таком случае обработка завершается с ошибкой, в логи пишется WARNING, а следующее время исполнения высчитывается относительно константы TriggerProperties.nextExecutionDelayOnError. Что здесь можно улучшить: не просто откладывать исполнение с константой, а иметь счётчик попыток, и только при достижении определённого порога откладывать надолго.
  • То же самое может произойти при попытке отправить сообщение в MQ - система может быть недоступна. Сейчас кейс с ошибкой просто логируется. Тут нужно глобально определиться со стратегией, что мы считаем успешной обработкой: отправили сообщения по всем веткам, хотя бы по одной и т.д. А также понять, что делать если часть отправилась успешно, а часть - нет: отправлять ещё раз, кажется, нехорошо, значит, надо хранить данные об этом.

Общее описание

  1. Входная точка - контроллер TriggerController. Не содержит никакой логики, делегирует обработку исполнителю команд. Таким образом, взаимодействие с endpoint'ами шаблонизировано, за обработку клиентских запросов отвечают классы с окончанием ...Command. Для исполнения запросов завёл отдельный thread pool, чтобы повысить throughput. Классы, описывающие эту логику, находятся в package com.jetbrains.buildtrigger.command.*
  2. За управление триггерами отвечает com.jetbrains.buildtrigger.trigger.service.BuildTriggerManager. Этот сервис является связующим звеном между БД и бизнес-логикой.
  3. Для CRUD-операций использовал Spring Data JPA + Hibernate. См. класс com.jetbrains.buildtrigger.trigger.dao.TriggerRepository, расширяющий JpaRepository.
  • Перед первым сохранением сразу высчитывается время следующей обработки - nextExecutionTime, согласно переданным настройкам и типу триггера.
  • После каждого обновления nextExecutionTime пересчитывается.
  1. Чтобы с определённой периодичностью выявлять триггеры, требующие обработки, использовал стандартный @Scheduled. За это отвечает класс com.jetbrains.buildtrigger.trigger.scheduling.UnprocessedTriggersDetectorTask, единственная ответственность которого - отрабатывать с периодичностью, заданной в scheduling.properties, и делегировать обработку BuildTriggerManager. Использует отдельный пул потоков, чтобы в дополнение ко многонодовости из условия задачи повысить производительность.
  2. Синхронизация во многопоточной и многонодовой среде. В кастомных методах TriggerRepository берётся пессимистическая row-level блокировка на обрабатываемый триггер, чтобы гарантировать уникальность обработки и избежать конфликта доступа для модифицирующих CRUD-операций (обновление, удаление триггера). Блокировка берётся двумя способами: средствами Postgres (метод fetchUnprocessedWithLock()) и средствами JPA (метод fetchForUpdateById()). В первом методе использовал нативный запрос, т.к. средствами JPA не было возможность вытащить одну запись (LIMIT 1), а тащить List<> не хотелось.
  • fetchUnprocessedWithLock() используется для обработки триггеров.
  • fetchForUpdateById() используется в CRUD-операциях, требующих модификации.
  1. Непосредственно обработка триггеров происходит через общий интерфейс: com.jetbrains.buildtrigger.trigger.service.processing.TriggerProcessor. По SOLID, чтобы быть более гибкими, если появятся новые типы триггеров.
  • С удалённым репозиторием взаимодействуем через VGit без локального сохранения репозитория, т.к. в данном случе это не нужно.
  • Обработчик триггеров Vcs идёт в удалённый репозиторий, получает список веток и последних коммитов.
    • Если в локальном хранилище ещё нет информации о последнем коммите ветки, то коммит первично будет сохранён в БД, сборка проведена не будет, т.к. мы только начали отслеживать состояние ветки.
    • Если локальные данные о последнем коммите отличаются от данных из удалённого репозитория, то обновляем у себя информацию и инициируем сборку.
    • Иначе, если коммиты совпадают, сборка проведена не будет.
  • Обработчик триггеров Scheduled идёт в удалённый репозиторий, получает список веток.
    • Если данные о ветке присутствуют в удалённом репозитории, инициируется сборка.
    • Иначе билд для ветки проведён не будет.
  • Если выполняются все условия: для триггера VCS найдено различие в коммите, а для триггера SCHEDULED - ветка найдена в репозитории, то пушим событие в очередь Rabbit MQ. Роутинг описан в rabbit-mq.properties и com.jetbrains.buildtrigger.config.RabbitMqConfiguration. За недостаточностью входных данных, он примерный, также, как и структура отправляемого сообщения.
  • После окончания обработки время следующего исполнения триггера - nextExecutionTime обновляется согласно текущим настройкам.
  1. Для высчитывания следующего времени исполнения используется стратегия - интерфейс com.jetbrains.buildtrigger.trigger.service.executiontime.NextExecutionTimeStrategy.
  • Следующее время исполнения высчитывается согласно заданным настройкам: ExecutionByTimeData.
    • В случае IntervalType == FIXED_RATE к текущей дате прибавляется значение параметра fixedRateInterval
    • В случае IntervalType == SCHEDULED высчитывается относительно текущей даты, через параметр cron
  1. Тесты. Тесты написаны на языке Kotlin.
  • Unit-тесты располагаются в директории test
  • Интеграционные тесты располагаются в директории slowTest. В этой директории не всё - сами тесты, т.к. потребовалось настроить окружение для возможности прогона интеграционных тестов. Если интересны сами тесты, их нужно искать по окончанию ...Test.
  1. Система сборки - Gradle. В slowTest.gradle сконфигурированы интеграционные тесты, в build.gradle - общая конфигурация и зависимости.

Что можно доработать

Хочется всё сделать красиво, и я по максимуму старался так сделать. Поэтому проект получился немного большим. Тем не менее в рамках формата тестового задания и чтобы уложиться в заявленный срок, не всё сделано/продумано идеально и реализовано. Есть множество мыслей о том, что можно было бы доработать, ниже - лишь малая часть, первое, что пришло на ум:

  • Api-классы хорошо бы либо вынести в библиотеку. И либо генерить спецификацию на основании классов, либо классы - на основании .yaml-спецификации. Но для простоты оставил в директории com.jetbrains.buildtrigger.trigger.api.*.
  • Для полноценного CRUD добавить endpoint findAll, для получения списка триггеров
  • Для интеграционных тестов поднять тестовый RabbitMQ
  • Дописать ещё тестов, сейчас покрытие не полное
  • Подумать над нормализацией таблицы build_trigger, вынесение в отдельные @Entity вместо хранения в JSON данных триггеров Vcs и Scheduled, как я это сделал для веток - они находятся в таблице branch. В текстовом JSON-формате храню в целях экономии времени и потому, что в данном бизнес-кейсе я не увидел потребности в обратном.
  • Вынести данные, связанные с процессингом триггера, в отдельную таблицу. Сейчас это только колонка next_processing_date. В отдельную таблицу можно добавить:
    • current_attempts_count - текущее количество попыток обработки. Завести в TriggerProperties лимит на попытки и откладывать исполнение на более долгий срок.
    • lock_expiration_date - сейчас берём лок только в рамках транзакции, что не очень гибко и удобно, заметил это, когда дорабатывал тесты. А также на неудобность намекает название метода - BuildTriggerManager.detectAndProcess. Собственно, в этой колонке можно сохранять дату, по которую действует лок. Эту дату мы сохраним, когда зафетчим триггер с пессимистической блокировкой. И при фетче надо также будет учитывать колонку lock_expiration_date.
  • Валидация данных при создании триггера:
    • Сейчас в запросе на создание или обновление триггера можно передать и vcsTriggerData и scheduledTriggerData, что не совсем корректно, хоть и ничего не ломает
    • Валидировать cron, что он в правильном формате
    • Валидировать формат переданного repositoryUrl
    • Прикрутить Jsr-303 валидатор, чтобы если есть ошибки в формате полей, не возвращался стандартный текст в SC400
    • Возможно, прикрутить какие-то кастомные валидации, как в случае кейса с vcsTriggerData и scheduledTriggerData.
  • Смотреть, чтобы при создании триггера уже не существовало с таким же типом и расписанием
  • Credentials от Git, по-моему, не должны принимать здесь, должны получать из другого места. Предполагаю, это должен быть другой сервис или, как минимум, другая таблица. А ещё не хранить пароль в незашифрованном виде.
  • Уточнить данные, которые пушатся в RabbitMQ, и, при необходимости, расширить. Сейчас они сформированы примерно, в виду неполноты требований. То же касается и роутинга.
  • Написать более серьёзное решение для задач, выполняющихся периодически. Это должна быть настраиваемая библиотека, которая в отдельной таблице хранит некий контекст с данными, периодически к ней обращается и выполняет задачи произвольного формата с периодичностью, как и обычный @Scheduled.
  • Аналогично предыдущему пункту, только библиотека, которая по состоянию контекста в БД умеет "дожимать" бизнес-логику произвольного формата. Т.е. мы имеем:
    • Конфигурация и бизнес-логика описана в коде
    • Состояние процесса хранится в БД. В любой момент времени мы можем обратиться к ней и посмотреть, на какой стадии находимся. В зависимости от параметров и состояния, либо повторить попытку, не начиная всё с начала, либо завершить процесс.
  • И так далее, ведь нет предела совершенству :)