Skip to content

alexander154/test-task

Repository files navigation

Тестовое задание реализовано в виде приложения, запускаемого в Docker.

Используется следующий стек:

PHP 7.4, MySQL 8

Приложение совместимо с PHP 8.2.

Дамп структуры БД содержится в файле dump.sql. Структура загружается в БД автоматически при запуске приложения через docker-compose.

Приложение (REST API) доступно по адресу http://127.0.0.1:30080 Так же, для удобства обозрения таблиц БД доступен phpMyAdmin по адресу http://127.0.0.1:30081. Логин root, пароль пустой, БД "mydb".

Комментарии к выполненному заданию даны внизу.

1. Копирование репозитория и запуск приложения.

При наличии соответствущего ПО (git, Docker) нужно выполнить следующие команды

Для возможности проверки работы на PHP 8.2 нужно в файле Dockerfile-PHP заменить строку "FROM php:7.4-apache" на "FROM php:8.2-apache", и затем выполнить команду

docker compose down;docker compose build;docker compose up -d

2. Выполнение методов API (используя curl в консоли)

  1. Добавление подписчиков в базу данных из внешнего файла:

curl -i -X POST -H "Content-Type: multipart/form-data" -F "[email protected]" http://127.0.0.1:30080/addSubscribersFromFile

Пример ответа успешного ответа, код 200:

HTTP/1.1 200 OK
Date: Sat, 21 Dec 2024 18:29:02 GMT
Server: Apache/2.4.62 (Debian)
X-Powered-By: PHP/8.2.27
Content-Length: 77
Content-Type: application/json; charset=utf-8

"Файл обработан, добавлено 10000 подписчиков."

Пример неуспешного ответа, в случае если хотя бы один подписчик из файла с таким номером уже есть в БД (данный момент не был оговорен в ТЗ, в связи с этим разработал собственную логику).

HTTP/1.1 409 Conflict
Date: Sat, 21 Dec 2024 18:39:41 GMT
Server: Apache/2.4.62 (Debian)
X-Powered-By: PHP/8.2.27
Content-Length: 255
Content-Type: application/json; charset=utf-8

"Ошибка добавления пользователей в БД, подробнее: Не получилось сохранить подписчика Peter Montgomery[6048764759382] (такой подписчик же существует в БД)"

  1. Создание рассылки и её запуск.

Для создания рассылки нужно выполнить данную команду (метод POST, в теле передается JSON):

curl -i http://127.0.0.1:30080/mailing/create -X 'POST' -d '{"title": "abc", "text": "qwe"}'

В случае если всё прошло в нормально и были переданы title и text, то будет создана рассылка в БД, и будет возвращён её id для дальнейших операций с ней.

Пример успешного ответа:

HTTP/1.1 201 Created
Date: Sat, 21 Dec 2024 18:48:59 GMT
Server: Apache/2.4.62 (Debian)
X-Powered-By: PHP/8.2.27
Content-Length: 22
Content-Type: application/json; charset=utf-8

{"id":1}

Пример неуспешного ответа при некорректном (неполном) запросе:

curl -i http://127.0.0.1:30080/mailing/create -X 'POST' -d '{"title": "abc"}'

HTTP/1.1 400 Bad Request
Date: Sat, 21 Dec 2024 18:54:58 GMT
Server: Apache/2.4.62 (Debian)
X-Powered-By: PHP/8.2.27
Content-Length: 74
Connection: close
Content-Type: application/json; charset=utf-8

"Не переданы заголовок и текст рассылки."

Как запустить или перезапустить рассылку (для досылки тем, кому в первый раз не получилось отправить в очередь, а так же отправить её новым подписчикам, добавленным уже после создания рассылки). Так же поддерживается использование "быстрого" режима:

curl -i http://127.0.0.1:30080/mailing/run -X 'POST' -d '{"id": 1}'

Пример ответа:

HTTP/1.1 200 OK
Date: Sat, 21 Dec 2024 18:57:43 GMT
Server: Apache/2.4.62 (Debian)
X-Powered-By: PHP/8.2.27
Content-Length: 20
Content-Type: application/json; charset=utf-8

{"id":1,"cntSent":10000}

Если нужно полностью перезапустить рассылку и отправить сообщения всем пользователям, вне зависимости от того было ли ранее им уже отправлено сообщение, то нужно передать в JSON значение "resend": true, например:

curl -i http://127.0.0.1:30080/mailing/run -X 'POST' -d '{"id": 1, "resend": true}'

Пример ответа:

HTTP/1.1 200 OK
Date: Sat, 21 Dec 2024 18:57:43 GMT
Server: Apache/2.4.62 (Debian)
X-Powered-By: PHP/8.2.27
Content-Length: 20
Content-Type: application/json; charset=utf-8

{"id":1,"cntSent":10000}

Приложение базово было проверено на отсутствие известных уязвимостей, таких как SQL Injection или загрузка файла с производным PHP кодом и последующим его исполнением злоумышленником.

Комментарии к заданию:

  • Так как в задаче не было указано как поступать с очевидно некорректными именами или номерами подписчиков (пустые, например), сделал проверку на длину (хотя бы один символ должен содержать номер или имя)

  • Многие события (в т.ч. исключения) логируются (custom_log.txt в каталоге log). Однако, если, например подписчик не был добавлен по причине повторения номера в таблице (на колонке с номером уникальный индекс), то это событие не пишется в лог, так как это нормальное поведение бизнес-логики, хоть и выбрасывается исключение (не делал проверку до вставки номера, что бы экономить ресурсы).

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

  • При выполнении задания не использовались фреймворки и сторонние библиотеки, composer использовался только для применения автозагрузки (vendor/autoload.php).

  • Имеется возможность добавить альтернативное хранилище (например, БД другого типа) имплементируя интерфейс StorageInterface

  • Так же можно добавить альтернативные импортёры (расширяя абстрактный класс AbstractSubscribersImporter), например помимо импорта из CSV файла реализована возможность импорта подписчиков в БД из JSON переданного в теле запроса.

  • Помимо этого, имплементируя интерфейс SenderInterface.php имеется возможность добавить альтернативный способ отправки сообщений (например, не в очередь, а посредством вызова метода REST API какой-либо внешней системы, или подобное).

Логика отправки в очередь сообщений следующая:

Для решения задачи по восстановлению рассылки после сбоя, при этом избегая отправки повторных сообщений тем, кому отправлено ранее, происходит запись в таблицу messages информации об отправленном сообщении, в случае успешной отправки. Однако, если вставлять в БД запись после каждой успешной отправки каждый раз, то это забирает значительные ресурсы и приводит к снижению производительности и при отправке рассылки большому числу пользователей (например всем 10000 из тестового файла), время работы (а соответственно и отклика по REST API) составляет около 15-20 секунд (машина не самая мощная, но, тем не менее, это недопустимо, особенно при работе по REST API, а не фоновым сервисом). Для улучшения производительности найдено решение вставлять строки батчами размером N вставок (можно настроить вызовом метода $storage->setSentMsgQueryBatchSize()). По умолчанию значение составляет 100 вставок за один батч. Чем больше значение размера батча, тем выше производительность. Однако жертвуем отсутствием информации об отправленных соообщениях в размере батча, если рассылка будет внезапно остановлена (например, упадёт веб-сервер, обслуживающий REST API).

Видится логичным установить размер в 1 запрос на батч для запуска рассылке виде сервиса в бэкграунде на сервере, где не так важно время отклика, а при обработке отправки внутри REST API котроллера использовать значение 100 или выше для увеличения производительности.

В случае, если станет недоступен сервер БД во время рассылки, то для уменьшения отклика метода отправки REST API, вместо многих ретраев запросов, отправка продолжится, а несохранённые батчи запросов будут сохраняться в специальный файл. При повторном запуске данной рассылки, будет проверено наличие файла и если в нём имеются незаписанные строки, то будет произведена их запись в таблицу messages, и только потом будет запущена сама отправка сообщений. Соответственно, так же будет исключена ситуация повторной отправки тем подписчикам, которым уже было отправлено сообщение в рамках данной рассылки.

Проблема состояния гонки (например два одновременных запроса в REST API метод запуска рассылки) решается использованием флагов состояния рассылки (0 - не запущена, 1 - в работе) и блокировкой записи рассылки в БД в таблице mailings_list путём вызова конструкции "SELECT ... FOR UPDATE". В результате полной блокировки ни одной таблицы не возникает.

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

About

Test task

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages