[Из песочницы] Remote observer

?v=1

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

Хочу рассмотреть случай, когда упомянутая внешняя система хочет получать уведомления о каких-либо изменениях в нашей системе. Например, обновление каталога товаров.


Задача

Существует торговая площадка, которая предоставляет доступ к своей товарной базе посредством WEB-сервисов. Партнёры площадки хотят узнавать об изменениях в базе в кратчайшие сроки.


Вариант решения

Мы знаем всех наших партнёров, можем запросить документацию по их ПО.

Можно реализовать работы с API наших партнёров, и при изменении товарного каталога оповещать их напрямую.

При таком подходе каждого нового клиента придётся подключать индивидуально. Если у партнёра что-то изменилось в софте, потребуются ресурсы на восстановление работоспособности. В общем, дополнительные траты и лишняя зона ответственности.

Очень не хочется устанавливать такие жесткие связи. Поэтому будем делать следующее.

За основу возьмём паттерн «наблюдатель». Предоставим нашим коллегам подписываться на события и получать необходимые им уведомления.


Реализация

Записи о подписках должны где-то храниться. Тип хранилища останется на совести разработчика. Рассмотрим ЧТО нужно хранить.


  • Событие. Типов событий может быть достаточно много и чтобы не рассылать оповещения всем подряд, нужно знать кто на что подписался.
  • URL. Берем самый простой вариант. Оповещение предполагает отправку HTTP-запроса на указанный URL. Если идея себя оправдает, то можно будет добавить поддержку других протоколов и технологий.
  • Количество отказов. Оповещение подписчиков это работа, которая требует ресурсов (процессорное время, память, трафик). После отправки запроса, мы ожидает положительного ответа. Но на той стороне может что-то сломаться и слать туда запросы становится бессмысленным. Соответственно нужно следить за отказами и прекращать оповещение при достижении некоторого значения.

Предположим, что программная часть написана на PHP.

Для подписки на событие, необходимо отправить POST-запрос на некоторый URL. Например, b2b.api.my-soft.ru/event-subscription. И передать параметры URL и event(алиас события).

Обрабатываем так (на базе Laravel):

public function subscribe()
{
    $request = $this->getRequest();
    $eventName = $request->input('event');
    $url = $request->input('callback');

    $validator = \Validator::make([
        'url' => $url,
        'event' => $eventName
    ], [
        'url' => 'url|required',
        'event' => 'required'
    ]);

    if ($validator->fails()) {
        throw new BadRequestHttpException($validator->errors()->first());
    }

    $repository = $this->getRepository();
    if (!$repository->eventAvailable($eventName)) {
        throw new BadRequestHttpException(trans('api.error.eventSubscription.wrongEvent'));
    }

    if (!$repository->canAddEvent($eventName)) {
        throw new BadRequestHttpException(trans('api.error.eventSubscription.maxCallbacks'));
    }

    $model = $repository->createModel([
        'client_id' => $request->attributes->get('client')->id,
        'event' => $eventName,
        'url' => $url
    ]);
    if ($repository->save($model)) {
        return $this->response($model);
    }
    throw new \Exception();
}

Алгоритм действий прост:


  • Проверяем, что все нужные данные пришли и пришли в нужном формате
  • Проверяем, что тип события доступен
  • Проверяем возможность подписки (метод canAddEvent)
  • Сохраняем
  • Сообщаем, что подписка успешна

Далее клиент ждёт оповещения.

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

Чтобы произошло оповещение, сначала нужно выбрать их хранилища все подписки, пройтись по ним и послать соответствующие запросы.

Это можно организовать в один или два этапа.

В два, это когда сначала, в очередь ставится само событие. А на втором этапе, в порядке очереди, происходит выборка подписчиков и их оповещение.

Но, можно и сразу сделать выборку и постановку задач в очередь. Операция достаточно дешевая.

$subscribersRepository->with(['event' => $event->getEventName()])->getActive()->each(function ($model) use ($event) {
    $this->dispatch(new \Commands\RemoteCallback(
        $model->id,
        $model->url,
        $event->getData()->toArray()
    ));
});

Выбираем активных подписчиков на произошедшее событие и в цикле ставим их в очередь.

RemoteCallback реализован следующим образом:

public function handle(EventSubscriptionRepository $subscriptionRepository)
{
    $client = new \Guzzle\Http\Client();
    $res = $client->post($this->url, [], $this->data, ['connect_timeout' => 3, 'timeout' => 3]);
    try {
        if ($res->send()->getStatusCode() !== 200) {
            throw new \Exception();
        }
        $subscriptionRepository->dropErrors($this->subscriptionId);
    } catch (\Exception $e) {
        $subscriptionRepository->incrementError($this->subscriptionId);
    }
}

Порядок действий таков. Делаем POST-запрос на указанный URL. Если успех, то обнуляем счетчик отказов, иначе увеличиваем.

Здесь стоит пройтись по условиям и ограничениям. Про количество отказов уже было сказано выше. Отказом считается HTTP статус != 200, либо медленный ответ. В примере выше, клиенту выделяется 3 секунды на установление соединения и 3 секунды на обработку запроса. Если партнёрская система за это время не уложилась, то считать отказом.

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

Плюс, конечно же, для работы с такими методами API необходима авторизация.

Вот, в принципе, и всё. Описан самый простой вариант. При необходимости можно добавить поддержку протокола SOAP или соединений через сокеты или что-то вроде того. И нужно реализовать повторную отправку сообщения, если ответ был не успешен.

© Habrahabr.ru