Прадмова
Мой сайт, якім я займаюся ў якасці хобі, прызначаны для захоўвання цікавых хатніх старонак і персанальных сайтаў. Гэтая тэма стала цікавіць мяне ў самым пачатку майго шляху ў праграмаванні, у той момант мяне захапляла знаходжанне вялікіх прафесіяналаў, якія пішуць пра сябе, свае захапленні і праекты. Звычка адчыняць іх для сябе засталася і цяпер: амаль на кожным камерцыйным і не вельмі сайце я працягваю зазіраць у футэр у пошуках спасылак на аўтараў.
Рэалізацыя ідэі
Першая версія была проста html-старонкай на маім персанальным сайце, дзе я складаў спасылкі з подпісамі ў ul-спіс. Набраўшы за нейкі час старонак 20, я пачаў думаць, што гэта не вельмі эфектыўна і вырашыў паспрабаваць аўтаматызаваць працэс. На stackoverflow я заўважаў, што шматлікія паказваюць сайты ў сваіх профілях, таму я напісаў парсер на php, які проста ішоў па профілях, пачынальна з першага (адрасы на SO і дагэтуль такога выгляду: `/users/1`), здабываў спасылкі з патрэбнага тэга і складаў у SQLite.
Гэта можна назваць другой версіяй: калекцыя з дзясятка тысяч урлаў у SQLite таблічцы, якая замяніла статычны спіс у html. Па гэтым спісе я зрабіў просты пошук. Т.к. былі толькі урлы, то і пошук быў проста па іх.
На гэтым этапе я закінуў праект і вярнуўся да яго, праз доўгі час. На гэтым этапе вопыт маёй працы складаў ужо больш за тры гады і я адчуваў, што магу зрабіць нешта больш сур'ёзнае. Да таго ж было вялікае жаданне асвойваць адносна новыя для сябе тэхналогіі.
Сучасная версія
праблема
Новыя сайты дадаюцца кансольнай камандай, якая сінхронна робіць наступнае:
- Спампоўвае кантэнт па URL
- Выстаўляе сцяг аб тым, ці даступны быў HTTPS.
- Захоўвае сутнасць вэб-сайта
- Зыходны HTML і загалоўкі захоўвае ў гісторыю "індэксавання"
- Парсіт кантэнт, здабывае Title і Description
- Дадзеныя захоўвае ў асобную калекцыю
Гэтага было дастаткова, каб проста захоўваць сайты і адлюстроўваць іх у спісе:
Але ідэя ўсё аўтаматычна індэксаваць, катэгарызаваць і ранжыраваць, трымаючы ўсё ў актуальным стане ў гэтую парадыгму ўкладвалася слаба. Нават простае даданне web-метаду для дадання старонак запатрабавала дубліраванні кода і блакіровак для пазбягання патэнцыйнага DDoS.
Наогул, вядома, усё можна рабіць і сінхронна, а ў web-метадзе вырабляць проста захаванне УРЛа для таго, каб монструозны дэман выконваў усе задачы для УРЛаў са спісу. Але ўсё роўна нават тут напрошваецца слова "чарга". А калі чарга ўкараніць, то можна ўсе задачы падзяліць і выконваць прынамсі асінхронна.
рашэнне
Укараніць чэргі і зрабіць event-driven сістэму апрацоўкі ўсіх задач. І якраз даўно хацелася паспрабаваць Redis Streams.
Выкарыстанне Redis streams у PHP
Т.к. фрэймворк у мяне не з тройкі гігантаў Symfony, Laravel, Yii, то і бібліятэку хацелася б знайсці незалежную. Але, як аказалася (пры першым разглядзе) — асобных сур'ёзных бібліятэк знайсці немагчыма. Усё, што звязана з чэргамі, альбо з'яўляецца праектыкам з 3 комітаў пяцігадовай даўніны, альбо прывязана да фрэймворка.
Я чуў аб Symfony, як аб пастаўшчыку асобных карысных кампанентаў, да таго ж некаторыя я ўжо выкарыстоўваю. А таксама ад Laravel тое-сёе таксама можна выкарыстоўваць, напрыклад іх ORM, без прысутнасці самога фрэймворка.
symfony/messenger
Першы ж кандыдат адразу ж здаўся ідэальным і без усялякіх сумневаў я яго ўсталяваў. Але нагугліць прыклады выкарыстання па-за Symfony аказалася складаней. Як сабраць з кучы класаў з універсальнымі, ні пра што не размаўлялымі назвамі, шыну для перадачы паведамленняў, ды яшчэ і на Redis?
Дакументацыя на афіцыйным сайце была дастаткова падрабязнай, але ініцыялізацыя была апісана толькі для Symfony з дапамогай іх каханага YML і іншых магічных метадаў для не сімфаніста. Цікавасці ў самім працэсе ўстаноўкі ў мяне не было, асабліва ў навагоднія канікулы. Але прыйшлося займацца гэтым і нечакана доўга.
Спроба разабрацца з інстанцыяваннем сістэмы па зыходніках Symfony задача таксама не самая трывіяльная для сціслых тэрмінаў:
Пакалупаўшыся ў гэтым усім і паспрабаваўшы нешта зрабіць рукамі, я прыйшоў да высновы, што займаюся нейкімі мыліцамі і вырашыў паспрабаваць што-небудзь яшчэ.
illuminate/queue
Аказалася, што гэтая бібліятэка намёртва прывязана да інфраструктуры Laravel і кучы іншых залежнасцяў, таму шмат часу я на яе не марнаваў: паставіў, паглядзеў, убачыў залежнасці і выдаліў.
yiisoft/yii2-queue
Ну тут адразу меркавалася з назову, ізноў жа цвёрдая прывязка да Yii2. Гэтай бібліятэкай мне даводзілася карыстацца і яна была нядрэннай, але пра тое, што яна поўнасцю залежыць ад Yii2 я не думаў.
Астатнія
Усё іншае, што я знаходзіў на гітхабе - ненадзейныя састарэлыя і закінутыя праектыкі без зорак, форкаў і вялікай колькасці коммітаў.
Зварот да symfony/messenger, тэхнічныя падрабязнасці
Прыйшлося разабрацца з гэтай бібліятэкай і, патраціўшы яшчэ нейкі час, я змог. Аказалася, што ўсё дастаткова лаканічна і проста. Для інстанцыявання шыны я зрабіў невялікую фабрыку, т.я. шын у мяне меркавалася некалькі і з рознымі апрацоўшчыкамі.
Усяго некалькі крокаў:
- Ствараем апрацоўшчыкі паведамленняў, якія павінны быць проста callable
- Заварочваем іх у HandlerDescriptor (клас з бібліятэкі)
- Гэтыя «Дэскрыптары» заварочваем у інстанс HandlersLocator
- Дадаем HandlersLocator у інстанс MessageBus
- Перадаем у SendersLocator набор `SenderInterface`, у маім выпадку інстансы класаў `RedisTransport`, якія канфігуруюцца відавочнай выявай
- Дадаем SendersLocator у інстанс MessageBus
MessageBus мае метад `->dispatch()`, які шукае адпаведныя апрацоўшчыкі ў HandlersLocator і перадае паведамленне ім, карыстаючыся адпаведнымі `SenderInterface` для адпраўкі праз шыну (Redis streams).
У канфігурацыі кантэйнера (у дадзеным выпадку php-di) увесь гэты звязак можа быць заканфігураваны так:
CONTAINER_REDIS_TRANSPORT_SECRET => function (ContainerInterface $c) {
return new RedisTransport(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_SECRET),
$c->get(CONTAINER_SERIALIZER))
;
},
CONTAINER_REDIS_TRANSPORT_LOG => function (ContainerInterface $c) {
return new RedisTransport(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_LOG),
$c->get(CONTAINER_SERIALIZER))
;
},
CONTAINER_REDIS_STREAM_RECEIVER_SECRET => function (ContainerInterface $c) {
return new RedisReceiver(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_SECRET),
$c->get(CONTAINER_SERIALIZER)
);
},
CONTAINER_REDIS_STREAM_RECEIVER_LOG => function (ContainerInterface $c) {
return new RedisReceiver(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_LOG),
$c->get(CONTAINER_SERIALIZER)
);
},
CONTAINER_REDIS_STREAM_BUS => function (ContainerInterface $c) {
$sendersLocator = new SendersLocator([
AppMessagesSecretJsonMessages::class => [CONTAINER_REDIS_TRANSPORT_SECRET],
AppMessagesDaemonLogMessage::class => [CONTAINER_REDIS_TRANSPORT_LOG],
], $c);
$middleware[] = new SendMessageMiddleware($sendersLocator);
return new MessageBus($middleware);
},
CONTAINER_REDIS_STREAM_CONNECTION_SECRET => function (ContainerInterface $c) {
$host = 'bu-02-redis';
$port = 6379;
$dsn = "redis://$host:$port";
$options = [
'stream' => 'secret',
'group' => 'default',
'consumer' => 'default',
];
return Connection::fromDsn($dsn, $options);
},
CONTAINER_REDIS_STREAM_CONNECTION_LOG => function (ContainerInterface $c) {
$host = 'bu-02-redis';
$port = 6379;
$dsn = "redis://$host:$port";
$options = [
'stream' => 'log',
'group' => 'default',
'consumer' => 'default',
];
return Connection::fromDsn($dsn, $options);
},
Тут відаць, што ў SendersLocator для двух розных паведамленняў мы прысвоілі розны транспарт, кожны з якіх мае свой канэкт на адпаведныя стрымы.
Я зрабіў асобны дэма-праект, які дэманструе дадатак з трох дэманаў, якія маюць зносіны паміж сабой з дапамогай такой шыны:
Але пакажу як можа быць уладкованы кансьюмер:
use AppMessagesDaemonLogMessage;
use SymfonyComponentMessengerHandlerHandlerDescriptor;
use SymfonyComponentMessengerHandlerHandlersLocator;
use SymfonyComponentMessengerMessageBus;
use SymfonyComponentMessengerMiddlewareHandleMessageMiddleware;
use SymfonyComponentMessengerMiddlewareSendMessageMiddleware;
use SymfonyComponentMessengerTransportSenderSendersLocator;
require_once __DIR__ . '/../vendor/autoload.php';
/** @var PsrContainerContainerInterface $container */
$container = require_once('config/container.php');
$handlers = [
DaemonLogMessage::class => [
new HandlerDescriptor(
function (DaemonLogMessage $m) {
error_log('DaemonLogHandler: message handled: / ' . $m->getMessage());
},
['from_transport' => CONTAINER_REDIS_TRANSPORT_LOG]
)
],
];
$middleware = [];
$middleware[] = new HandleMessageMiddleware(new HandlersLocator($handlers));
$sendersLocator = new SendersLocator(['*' => [CONTAINER_REDIS_TRANSPORT_LOG]], $container);
$middleware[] = new SendMessageMiddleware($sendersLocator);
$bus = new MessageBus($middleware);
$receivers = [
CONTAINER_REDIS_TRANSPORT_LOG => $container->get(CONTAINER_REDIS_STREAM_RECEIVER_LOG),
];
$w = new SymfonyComponentMessengerWorker($receivers, $bus, $container->get(CONTAINER_EVENT_DISPATCHER));
$w->run();
Выкарыстанне гэтай інфраструктуры ў дадатку
Рэалізаваўшы шыну ў сваім бэкендзе, я вылучыў асобныя прыступкі са старой сінхроннай каманды і зрабіў асобныя хэндлеры, кожны з якіх займаецца сваёй справай.
Пайплайн дадання новага сайта ў базу дадзеных атрымаўся такім:
І адразу пасля гэтага мне стала значна прасцей дадаваць новы функцыянал, напрыклад, выманне і парсінг Rss. Т.к. гэты працэс таксама патрабуе зыходны кантэнт, то хэндлер-атрымальнік спасылкі на rss таксама як і WebsiteIndexHistoryPersistor падпісваецца на паведамленне "Content/HtmlContent", апрацоўвае яго і перадае патрэбнае паведамленне па сваім пайплайну далей.
У канчатковым выніку атрымалася некалькі дэманаў, кожны з якіх трымае падключэнні толькі да патрэбных рэсурсаў. Напрыклад дэман паўзункі змяшчае ў сабе ўсе апрацоўшчыкі, якія патрабуюць паходу ў інтэрнэт за кантэнтам, а дэман захоўваюцца трымае падлучэнне да базы дадзеных.
Зараз замест селектаў з базы дадзеных, патрэбныя id пасля ўстаўкі persister'ам проста перадаюцца праз шыну ўсім зацікаўленым апрацоўшчыкам.
Крыніца: habr.com