Если проект вышел за рамки локальной машины, скорее всего придётся интегрироваться с какими-нибудь сторонними системами.
Хочу рассмотреть случай, когда упомянутая внешняя система хочет получать уведомления о каких-либо изменениях в нашей системе. Например, обновление каталога товаров.
Задача
Существует торговая площадка, которая предоставляет доступ к своей товарной базе посредством 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 или соединений через сокеты или что-то вроде того. И нужно реализовать повторную отправку сообщения, если ответ был не успешен.