Skip to content

Сервис обработки и запуска bash-скриптов

Notifications You must be signed in to change notification settings

ykkssyaa/Bash_Service

Repository files navigation

Сервис обработки и запуска bash-скриптов

Ссылка на задание

Описание задачи

Необходимо разработать приложение, которое будет предоставлять REST API для запуска команд. Команда представляет собой bash-скрипт. Приложение должно позволять параллельно запускать произвольное количество команд.

Запуск

Локальный запуск

  1. Клонирование репозитория
  2. make docker.run.db - запуск postgres
  3. make docker.run.migrate - запуск миграций (либо использовать make migrate.up, если утилита golang-migrate установлена локально)
  4. make local.run - локальный запуск сервера

Запуск в Docker

  1. Клонирование репозитория
  2. make docker.run

В данном случае миграции для базы данных накатятся сами.

Запуск тестов

Для запуска тестов необходимо прописать make tests.run

Остановка работы Docker

Для остановки и удаления контейнеров используйте команду make docker.down

Описание системы

Во время работы использовалась система Ubuntu 22.04.4 LTS и golang версии 1.21.3. В качестве базы данных используется PostgreSQL.

API

Всё API описано в спецификации OpenAPI(Swagger) в этом файле

Для удобства тестирования сервиса предлагается воспользоваться коллекцией Postman. В коллекции описаны все endpoint'ы с возможностью изменения аргументов и тела запросов. Для подключения переменных среды необходимо подключить environment Bash.

Реализованный функционал

Endpoint'ы сервиса

  1. Создание и запуск команды
  2. Получение состояния команды по id
  3. Получение всех команд с пагинацией (offset, limit)
  4. Остановка команды по 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

Я настроил конфигурацию для docker compose так, чтоб можно было без проблем запустить сервис без установки сторонних утилит. Прописан Dockerfile для сборки образа golang сервиса. А также прописан docker-compose для запуска всех служб (сам сервис, база данных и утилита миграций). Перед запуском основного сервиса и миграций контейнер с Postges проверяется, что он Healthy (БД запущена и отвечает на запросы).

Github-actions

Настроен CI с запуском тестов и билдом сервиса с помощью GitHub Actions. Каждый коммит и pull request будет проверяться на соответствие требованиям (сервис сбилдился и тесты прошли).

Graceful shutdown

Для того, чтоб не терялись данные при перезапуске сервера, реализован процесс Graceful Shutdown или же Плавное выключение. Когда сервер принимает сигнал от OS о его выключении, сервер сразу не перестает работать а выключается по такому алгоритму:

  1. Прекращается получение входящих http запросов
  2. Обработка всех полученных ранее запросов
  3. После отработки последнего запроса закрытие всех подключений к базе данных
  4. Выключение сервера

Стек технологий

  1. Golang 1.21
  2. PostgreSQL 16
  3. Docker, Docker-compose
  4. Makefile
  5. Viper для считывания конфигурации
  6. Sqlx для работы с БД
  7. Testify для тестов
  8. Golang-mock для генерации моков
  9. Gorilla/Mux для роутинга
  10. Swagger Editor
  11. 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\""
}

Принятые в ходе разработки решения

Написание Swagger API

Контекст

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


Вынесение обработчика Executor в отдельный интерфейс

Контекст

Всю обработку команд выполняла одна структура CommandService. Но как только пришла необходимость изолировано протестировать фунцию CreateCommand, то пришла проблема, что функции CreateCommand и ExecCmd очень связаны и нельзя протестировать одно без другого.

Решение

Было решено вынести отдельный интерфейс Executor и реализовать структуру с методами данного интерейса. Сам интерфейс стал зависимостью CommandService.

Статус

Принято

Последствия

Появилась возможность замокать фунцию обработки комманд ExecCmd и изолированно протестировать CreateCommand.


Написание интеграционных тестов

Контекст

Необходимо проверить правильную работу системы в целом, так как unit-тесты не показывают всю картину целиком.

Решение

Написать интеграционные тесты для проверки взаимодействия всех функций системы

Статус

На рассмотрении

Последствия

Будет видна более широкая картина того, как работает система и есть ли в ней проблемы.

About

Сервис обработки и запуска bash-скриптов

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published