Всем привет!

В этой статье расскажу, как решил написать библиотеку https://packagist.org/packages/xman12/persistent-request и что там внутри.

Как и любая библиотека, эта решает свои задачи, а именно гарантированное выполнение запроса и последующую обработку. Я находил, как минимум, одно решение, которое работает с подобной проблематикой — это https://temporal.io/, но система монструозная, а мне хотелось, чего-то более легкого и приземленного, поэтому я решил написать свое решение этой задачи.

Перед разработкой я обозначил для себя требования, которым должна соответствовать библиотека:

  • работать с laravel (на этом фреймворке построен сайт, где нужно было решить задачу с обработкой запросов)

  • легкое и гибкое использование

  • минимум зависимостей

Вот что в итоге вышло по зависимостям:

- php >=7.4

- guzzlehttp/guzzle >=6.3

- laravel/framework >=5.4

- laravel/serializable-closure "1.*"

На схеме ниже показал, как работает библиотека:

Ну что ж, приступим к установке и разбору. 

Для начала библиотеку нужно подключить через composer:

composer require xman12/persistent-request

Затем подключаем провайдера в config/app.php

'providers' => [PersistentRequest\ServiceProvider::class]

После подключения провайдера, нужно запустить команду, которая запускает очереди:

php artisan queue:work

Разберем более подробно на примере.

Для начала инициализируем сервис:

$requestService = app(\PersistentRequest\Services\RequestServiceInterface::class);

Теперь нужно создать объект запроса: 

$requestGuzzle = new \GuzzleHttp\Psr7\Request('get', 'https://google.com');

После нужно подготовить объект RequestDTO, и происходит вся магия:

$requestDTO = new \PersistentRequest\DTO\RequestDTO(
  $requestGuzzle, 
  \PersistentRequest\Events\SuccessEvent::class, 
  30,
  5,
  function (\GuzzleHttp\Psr7\Response $response) {
        if (200 !== $response->getStatusCode()) {
            throw new \Exception('error processed');
        }
});

Первым аргументом идет класс для запроса в guzzle.

Вторым идет \PersistentRequest\Events\SuccessEvent::class — это имя события, которое будет вызвано после успешного выполнения запроса, в него будет передан объект Psr\Http\Message\ResponseInterface, с которым можно работать в listener.

Третьим передается время в секундах, через которое библиотека повторит запрос, в случае неудачи.

Четвертым аргументом передается количество попыток после которого прекратятся запросы, тут нужно быть аккуратным если не передать тут значение то запрос будет выполняться бесконечно - пока не станет успешным. В случае если число попыток передается и по итогу времени количество отказов равняется лимиту, в таком случае очередь удаляется и отправляется событие DeleteRequestEvent, на него стоит подписаться если вы хотите отслеживать данное поведение.

Пятым аргументом передается анонимная функция, расширяющая работу с сервисом. Невалидные или валидные ответы обрабатываются, и, если нужно повторить запрос, то достаточно кинуть исключение. Так можно, не реализовывая listener, описать логику обработки ответа в анонимной функции.

Далее выполняется сам запрос:

$requestService->execute($requestDTO);

Внутри функции execute происходит запись данных запроса в бд, на случай, если он не будет валидным согласно описанной логике. Если ответ успешен, запись из базы данных удаляется.

Соберем все в кучу:

$requestService = app(\PersistentRequest\Services\RequestServiceInterface::class);
$requestGuzzle = new \GuzzleHttp\Psr7\Request('get', 'https://google.com');
$requestDTO = new \PersistentRequest\DTO\RequestDTO(
  $requestGuzzle, 
  \PersistentRequest\Events\SuccessEvent::class,
  30,
  5,
  function (\GuzzleHttp\Psr7\Response $response) {
  if (200 !== $response->getStatusCode()) {
      throw new \Exception('error processed');
  }
});

$requestService->execute($requestDTO);

Если запрос по какой-то причине «упал», то вызывается RetryRequestJob.php, который передается экземпляр RequestDTO с необходимыми параметрами.

Вот так просто написать логику запроса к сервису, который должен гарантированно выполниться. Без реализации повторных опросов на стороне самого сервиса. Этот алгоритм работы применим как в микросервисной архитектуре, где запрос/ответ не должны быть синхронными, так и в других системах, где ответ пользователю не отправляется в одной сессии.

Комментарии (6)


  1. Vest
    13.10.2023 16:50

    Извините, но очень похоже на изобретение того, что есть в message-driven architecture. Всякие там «кафки», ActiveMQ и так далее.

    И вообще, если у вас запрос «упал», то лучше разбираться почему это произошло, а не долбить в сервер n-раз.


    1. xman12 Автор
      13.10.2023 16:50

      на счет «кафки», ActiveMQ - я MQ систему не изобретал, как раз эта либа работает на очередях.

      И вообще, если у вас запрос «упал», то лучше разбираться почему это произошло, а не долбить в сервер n-раз. - ситуации бывают разные, бывают ситуации когда сеть моргнула в микросервисах или допустим система на другой стороне висит (я отталкиваюсь от реального кейса когда интеграция при интеграции с другой системой на их стороне висела api) в таких ситуациях библиотека помогает, тут так же можно внутри ее обработать и ситуации падения, внутри прописать сценарий логирования и увидеть проблему, не обязательно просто долбить - можно и залогировать и разобраться


      1. Vest
        13.10.2023 16:50

        А то, что вы пошлете один запрос, потом другой и в итоге API обработает его дважды — это лучше? Как по мне это слабое решение. А вдруг нам важна целостность запросов?

        Ладно, мы побеседовали, у нас с вами разные мнения и это хорошо.


  1. a1ez
    13.10.2023 16:50

    Звучит как transactional outbox паттерн


    1. xman12 Автор
      13.10.2023 16:50

      ранее я не знал про данный паттерн, но вы правы она реализует его по сути.


  1. toratoda
    13.10.2023 16:50

    у http клиента от laravel есть метод retry который позволяет повторить запрос сколько угодно раз, а так же обрабатывать ошибку по необходимости, я думаю это подошло бы на случай когда "моргнула сеть"