README ¶
simple-http-multiplexer (тестовое задание от НПО Фарватер)
Содержание:
- Постановка задачи
- Описание проделанной работы по задачам
- Краткое описание особенностей реализации
- Сборка и конфигурация
- Перспективы развития
Постановка задачи
- Приложение представляет собой http-сервер с одним хендлером
- Хендлер на вход получает POST-запрос со списком url в json-формате, сервер запрашивает данные по всем этим url и возвращает результат клиенту в json-формате
- Если в процессе обработки хотя бы одного из url получена ошибка, обработка всего списка прекращается и клиенту возвращается текстовая ошибка
- Для реализации задачи следует использовать Go 1.13 или выше
- Использовать можно только компоненты стандартной библиотеки Go
- Сервер не принимает запрос если количество url в в нем больше 20
- Сервер не обслуживает больше чем 100 одновременных входящих http-запросов
- Таймаут на обработку одного входящего запроса - 10 секунд
- Для каждого входящего запроса должно быть не больше 4 одновременных исходящих
- Таймаут на запрос одного url - секунда
- Обработка запроса может быть отменена клиентом в любой момент, это должно повлечь за собой остановку всех операций связанных с этим запросом
- Сервис должен поддерживать 'graceful shutdown': при получении сигнала от OS перестать принимать входящие запросы, завершить текущие запросы и остановиться
- Результат должен быть выложен на github и запускаться docker-compose
Описание проделанной работы по задачам
- ✅ Привязка хендлера в cmd/simple-http-multiplexer/main.go
- ✅ Реализация хендлера в internal/handler/multiplexer.go
- ✅ В файле с реализацией хендлера из пункта 2 предусмотрено несколько типов ошибок с различными HTTP-кодами ответа. Неизвестные ошибки возвращаются с кодом 500, таймауты возвращаются с кодом 408. При некорректном формате запроса возвращается ответ с кодом 400. Также предусмотрен возврат кода 429, когда достигнут лимит одновременных подключений к серверу (подробнее об этом в пункте 7).
- ✅ Сервис реализован на Go 1.18 (go.mod)
- ✅ Были использованы только компоненты стандартной библиотеки (go.sum отсутствует)
- ✅ Обработка данной ошибки есть в хендлере в internal/handler/multiplexer.go:160
- ✅ Лимитер по количеству одновременных входящих подключений реализован в internal/handler/muxwrappers.go:10
- ✅ Таймауты есть в хендлере в internal/handler/multiplexer.go:201
- ✅ Для каждого входящего запроса параллельно запускается не более 4 исходящих internal/handler/multiplexer.go:126
- ✅ Таймаут одного запроса - секунда internal/handler/multiplexer.go:101
- ✅ Обработка запроса может быть отменена клиентом в любой момент internal/handler/multiplexer.go:207 (помимо обработки контекста запроса в указанном блоке select, есть его обработка и при выполнении запросов по заданным url (см. здесь))
- ✅ Сервис поддерживает graceful shutdown: internal/server/httpserver.go
- ✅ Подробнее о сборке и запуске в Docker-Compose в разделе "Сборка и конфигурация"
Помимо основных задач, дополнительно была реализована тестирующая функция для проверки работы хендлера: test/handler_test.go. Был учтен запуск тестов при сборке Docker-образа (они запускаются при выборе нужной цели сборки). Подробнее об этом будет рассказано в разделе "Сборка и конфигурация". Были учтены дополнительные ошибки, об обработке которых не было уточнено в постановке задачи. Подробнее об этом в разделе "Краткое описание особенностей реализации"
Краткое описание особенностей реализации
Сервис принимает запрос в формате json, причем заголовок Content-Type: application/json
может быть не указан клиентом. Если сервис получает данные не в формате json, возвращается ошибочный ответ с кодом 400, с соответствующим сообщением об ошибке (получены данные не в формате json). Корректный запрос к сервису имеет следующий формат:
{
"urls": [
"https://example.com/",
"https://example.org/",
"https://example.net/",
"https://example.edu/"
]
}
По ключу urls
находится список адресов, к которым надо сделать запрос. Следуя постановке задачи, данных адресов может быть не более 20 (однако в конфигурации может быть указано любое, подробнее об этом в разделе "Сборка и конфигурация").
Если во время обращения по адресам произошел таймаут во время выполнения отдельного запроса или же всего запроса от пользователя в целом, возвращается ответ с кодом 408 и соответствуюшим сообщением об ошибке.
Активных одновременных подключений к сервису может быть не более 100 (однако в конфигурации может быть указано любое, подробнее об этом в разделе "Сборка и конфигурация"). Если пользователь подключится хотя бы 101-ым, он получит ответ с кодом 429 и значением заголовка Retry-After: <половина от таймаута запроса одного пользователя>
. Для примера, если таймаут обработки запроса от пользователя - 10 секунд, при неудачном подключении сервис вернет ответ с кодом 429 и заголовком Retry-After: 5
.
Максимальный размер тела входящего запроса: мегабайт (сомневаюсь, что с какими-либо параметрами конфигурации можно передать адекватное количество ссылок на мегабайт).
Таймаут обработки запроса пользователем по умолчанию: 10 секунд (однако в конфигурации может быть указано любое, подробнее об этом в разделе "Сборка и конфигурация").
Логично предположить, что если сервису приходит запрос с методом POST, то запросы по заданным адресам нужно делать так же методом POST, так как другой информации кроме адреса в клиентском запросе не предусмотрено, однако обработку дополнительного поля с HTTP-методом в запросе нетрудно внедрить в сервис. Подробнее об этом в разделе "Перспективы развития".
При удачном выполнении всех запросов, сервис возвращает json в следующем формате
{
"responses": [
{
"service_url": "https://example.com/",
"http_status_code": 200,
"base64_payload": "some base 64 encoded data",
"content_type": "text/html; charset=UTF-8"
},
{
"service_url": "https://example.org/non_existent_page",
"http_status_code": 404,
"base64_payload": "some base 64 encoded data",
"content_type": "text/html; charset=UTF-8"
},
...
]
}
Ключ responses
содержит список всех ответов от адресов, к которым делался запрос. Во вложенном в список объекте по ключу service_url
находится адрес, к которому делался запрос, в http_status_code
- код ответа, в base64_payload
- тело ответа, закодированное в base64, в content_type
- указание значения заголовка Content-Type
при получении ответа по адресу. Тело ответа закодировано в base64, так как в значении ключа могут находиться не только текстовые данные (в постановке задачи не указано, какого именно формата будут приходить данные, а json - текстовый), поэтому любое полученное тело запроса кодируется в base64 и клиенту сообщается значение заголовка Content-Type
, чтобы он мог корректно распарсить данные после декодирования из base64.
Сборка и конфигурация
По умолчанию сервис запускается на порту 8080, а запросы принимаются на http://localhost:8080/api/mux
. Запуск сервиса осуществляется при помощи docker-compose командой docker-compose up
. При желании, можно добавить флаг -d, если не хотите занимать термиинал, но тогда не будет видно логов. О лучшем способе логгирования подробнее в разделе "Перспективы развития". По завершении, выполните команду docker-compose down
, чтобы изящно завершить работу сервиса и удалить все созданные docker-compose артефакты (контейнеры/сети).
В docker-compose.yml указываются необходимые параметры конфигурации, такие как общий таймаут запроса от пользователя, количество одновременных подключений к серверу, таймаут запроса по одному адресу и прочие ограничения из постановки задачи. В самом Dockerfile есть несколько этапов сборки. Это необходимо, для того, чтобы при запуске сборки образов с разными параметрами, можно было получить различные образы, например при включении в сборку цели test, в промежуточном контейнере будут запущены тесты. Конечная сборка намного легче официального образа golang с DockerHub засчет того, что собранный на предыдущем этапе исполняемый файл копируется в образ на основе Alpine Linux.
Перспективы развития
В этом разделе описаны улучшения, которые в теории можно включить в данный проект, однако, это будет слишком изощренный сервис для тестового задания.
Кеширование ответов в Redis
Если размер возвращаемого тела ответа невелик, а данные с запрашиваемого ресурса редко обновляются, можно кешировать запросы в Redis для того, чтобы ускорить выдачу ответов на запросы. Прирост производительности будет особенно заметен, если ресурс будет часто запрашиваемым.
Добавление большего количества полей в запрос от пользователя
К каждому запросу помимо url можно добавить дополнительную информацию в роде версии HTTP, значения заголовков и HTTP-метод, для того чтобы запросы были более точные.
Использование сторонних библиотек для более элегантного кода
- Роутер из библиотеки gorilla/mux обладает намного бо́льшими возможностями, что делает возможным более точную обработку приходящих запросов исходя из большего числа параметров HTTP-запроса;
- Во внешней стандартной библиотеке Go golang.org/x/time/rate есть несколько типов рейт-лимитеров, в том числе по количеству одновременных подключений к серверу;
- Более элегантный хендлинг ошибок из горутин при помощи ErrorGroup из внешней стандартной библиотеки Go golang.org/x/sync/errgroup
Усовершенствование логгирования
Для того, чтобы логи не терялись в stdout или при запуске docker-compose с флагом -d
, можно прикрепить том к контейнеру и писать логи туда. Также можно настроить логгирование в БД.