Тестовое задание реализовано в виде приложения, запускаемого в 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) нужно выполнить следующие команды
- mkdir ~/tmp
- cd ~/tmp
- git clone https://github.com/alexander154/test-task.git
- cd ~/tmp/test-task
- docker compose up -d
Для возможности проверки работы на 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 в консоли)
- Добавление подписчиков в базу данных из внешнего файла:
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] (такой подписчик же существует в БД)"
- Создание рассылки и её запуск.
Для создания рассылки нужно выполнить данную команду (метод 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". В результате полной блокировки ни одной таблицы не возникает.
Уникальный индекс имеется только в таблице с подписчиками, на таблице с оправленными сообщениями уникального индекса нет, соответственно выигрыш в производительности.