Необходимо разработать приложение, которое будет предоставлять REST API для запуска команд. Команда представляет собой bash-скрипт. Приложение должно позволять параллельно запускать произвольное количество команд.
- Клонирование репозитория
make docker.run.db
- запуск postgresmake docker.run.migrate
- запуск миграций (либо использоватьmake migrate.up
, если утилита golang-migrate установлена локально)make local.run
- локальный запуск сервера
- Клонирование репозитория
make docker.run
В данном случае миграции для базы данных накатятся сами.
Для запуска тестов необходимо прописать make tests.run
Для остановки и удаления контейнеров используйте команду make docker.down
Во время работы использовалась система Ubuntu 22.04.4 LTS и golang версии 1.21.3. В качестве базы данных используется PostgreSQL.
Всё API описано в спецификации OpenAPI(Swagger) в этом файле
Для удобства тестирования сервиса предлагается воспользоваться коллекцией Postman. В коллекции описаны все endpoint'ы с возможностью изменения аргументов и тела запросов. Для подключения переменных среды необходимо подключить environment Bash.
- Создание и запуск команды
- Получение состояния команды по id
- Получение всех команд с пагинацией (offset, limit)
- Остановка команды по id
Состояние команды задается её статусом(работает, остановлена, выполнена с ошибкой, успешное выполнение), выводом в консоль и в id системе. При создании процесса выполнения команды создается контекст, который передается в обработчик, а функция отмены контекста сохраняется в key-value хранилище, к которому можно обратиться для отмены контекста и, следовательно, остановки процесса выполнения команды. Контекст имеет время жизни - 5 минут(задается через константу CtxTimeout в consts).
При создании команды пользователь получает id команды, с помощью которого он может следить за её состоянием. Сервис поддерживает обработку долгих комманд. Состояние команды проверяется и обновляется каждую секунду (задается через константу ReadOutputTime в consts). Во время выполнения команды, её состояние хранится в in-memory кеше,а в базу данных попадает только после окончания работы процесса.
Реализованы unit-тесты для слоя service. Для создания моков я использовал утилиту gomock. Для тестирования слоя сервисов я сгенерировал моки всех интерфейсов gateway: обращение к базе данных, кешу и хранилищу контекстов; а также был замокан интерфейс запуска процессов команд Executor. Были протестированы функции создания, получения и остановки команд.
Для более полноценного тестирования функционала хорошо бы подошли интеграционные или e2e тесты, но в данной работе они не реализованы.
В проекте раелизована чистая архитектура. Сервис состоит из 3 слоёв:
- Gateway (БД, кеш, in-memory хранилище)
- Service (валидация, бизнес-логика)
- Server (Считывание http запросов, валидация аргументов)
Слои связаны между собой интерфейсами, что позволяет удобно тестировать каждый слой изолированно. На слое сервиса есть отдельные интерфесы: Command для основной логики сервиса, прокидывается в слой Server; и Executor - отдельный интерфейс реализующий запуск команды и последующую её обработку.
В gateway реализованы интерфейсы: Command также для основных функций сервиса, в данном случае для обращения к базе данных; Storage для хранения функций для отмены контекста по ключу; Cache для реализации кеша команд.
Я настроил конфигурацию для docker compose так, чтоб можно было без проблем запустить сервис без установки сторонних утилит. Прописан Dockerfile для сборки образа golang сервиса. А также прописан docker-compose для запуска всех служб (сам сервис, база данных и утилита миграций). Перед запуском основного сервиса и миграций контейнер с Postges проверяется, что он Healthy (БД запущена и отвечает на запросы).
Настроен CI с запуском тестов и билдом сервиса с помощью GitHub Actions. Каждый коммит и pull request будет проверяться на соответствие требованиям (сервис сбилдился и тесты прошли).
Для того, чтоб не терялись данные при перезапуске сервера, реализован процесс Graceful Shutdown или же Плавное выключение. Когда сервер принимает сигнал от OS о его выключении, сервер сразу не перестает работать а выключается по такому алгоритму:
- Прекращается получение входящих http запросов
- Обработка всех полученных ранее запросов
- После отработки последнего запроса закрытие всех подключений к базе данных
- Выключение сервера
- Golang 1.21
- PostgreSQL 16
- Docker, Docker-compose
- Makefile
- Viper для считывания конфигурации
- Sqlx для работы с БД
- Testify для тестов
- Golang-mock для генерации моков
- Gorilla/Mux для роутинга
- Swagger Editor
- Postman
{
"script": "for ((i=1; i<=100; i++))\ndo\n echo $i\n sleep 2\ndone"
}
{
"script": "ls -l -1 -S"
}
{
"script": "cat Makefile | grep \"docker\""
}
Перед реализацией сервиса возникла необходимость описать API для понимания, что мы должны получить в итоге.
Было решено описать API в Swagger Editor с указанием всех endpoint'ов, их входных параметров и результатов.
Принято
Был представлен удобочитабельный формат для описания API. В последствии с помощью Swagger Editor были сгенерированы роуты для сервера и коллекция в Postman.
В проекте появилась необходимость вести миграции для базы данных.
Было решено использовать golang-migrate для управления миграциями. Утилита позволяет генерировать файлы миграций и накатывать их на базу данных.
Принято
Появилась возможность удобно управлять миграциями. Сама утилита не обязательна для установки на локальную машину, её можно запустить с помощью docker-compose.
Неходимо было реализовать возможность отмены выполнения команд.
Было решено запускать команды с помощью CommandContext, создавать контекст с таймаутом и сохранять функцию отмены в key-value хранилище. В качестве ключа используется id команды. Хранилище реализовано средствами языка Golang и представляет собой структуру с map[int] Context.Cancelfunc и RWMutex. Хранилище имеет методы Set, Get, Remove.
Принято
Появилась возможность останавливать выполнение команды с помощью отмены её контекста выполнения.
При обработке долгих команд сервис обновляет данные о ней каждую секунду. И каждую секунду происходило обращение к базе данных. При этом для просмотра статуса команды тоже нужно было обращаться к базе данных.
Для минимизации обращений к базе данных во время выполнения команды было решено добавить кеш, который хранит в себе выполняющиеся в данный момент команды. Кеш реализован средставами языка Golang и представляет собой структуру из map[int] models.Command и RWMutex. Кеш имеет методы Set, Get, Remove. После выполнения команды кеш очищается и данные сохраняются в базу данных.
Принято
В качестве альтернативы можно использовать Redis. Redis идеально подходит в качестве кеша из-за его скорости и возможности задать время жизни ключа. В дальнейшем можно будет заменить in-memory кеш на Redis.
Стало намного меньше обращений к базе данных во время работы команды. Также важно то, что GetAllCommands не получает актуальную информацию о выполняющихся в данный момент командах, так как она работает с базой данных напрямую и не актуализирует данные их кеша. Получение команды по id не имеет эту проблему, так как сразу обращается к кешу, а только потом к базе данных при необходимости.
Всю обработку команд выполняла одна структура CommandService. Но как только пришла необходимость изолировано протестировать фунцию CreateCommand, то пришла проблема, что функции CreateCommand и ExecCmd очень связаны и нельзя протестировать одно без другого.
Было решено вынести отдельный интерфейс Executor и реализовать структуру с методами данного интерейса. Сам интерфейс стал зависимостью CommandService.
Принято
Появилась возможность замокать фунцию обработки комманд ExecCmd и изолированно протестировать CreateCommand.
Необходимо проверить правильную работу системы в целом, так как unit-тесты не показывают всю картину целиком.
Написать интеграционные тесты для проверки взаимодействия всех функций системы
На рассмотрении
Будет видна более широкая картина того, как работает система и есть ли в ней проблемы.