
Eessõna
Minu veebisait, millega tegelen hobi korras, on mõeldud huvitavate kodumaiste ja isiklike saitide säilitamiseks. See teema hakkas mind huvitama minu programmeerimistee alguses, kui mind üllatas, et leidub palju professionaale, kes kirjutavad endast, oma hobidest ja projektidest. See harjumus avastada nende töid on jäänud minuga ka praegu: peaaegu igal kommerts- ja mitte nii väga saital piilun jalust linkide leidmiseks autorite juurde.
Idee elluviimine
Esimene versioon oli lihtsalt html-leht minu isiklikul veebisaidil, kuhu kogusin allikaid koos kirjeldustega ul-loendis. Aja jooksul, kui lehti kogunes 20, hakkasin mõtlema, et see pole eriti efektiivne, ning otsustasin proovida protsessi automatiseerida. Stackoverflow'is märkasin, et paljud inimesed viitavad oma profiilideslehtedele, seega kirjutasin php-l põhineva parsi, mis lihtsalt käis läbi profiilid, alustades esimesest (SO aadressid on siiani sellised: `/users/1`), ekstraheerides lingid vajalikust sildist ning salvestades need SQLite'i.
Seda võib pidada teiseks versiooniks: kogu linkide kollektsioon kümnete tuhandete URL-idega SQLite tabelis, mis asendas staatilise html loendi. Selle loendi põhjal tegin lihtsa otsingu. Kuna oli ainult URL-e, oli ka otsing lihtsalt nende põhjal.
Sellel etapil viskasin projekti kõrvale ning naasin sellele kaua hiljem. Selles etapis oli minu töökogemus juba rohkem kui kolm aastat ja tundsin, et suudan teha midagi tõsiselt. Lisaks oli suur soov omandada suhteliselt uusi tehnoloogiaid.
Kaasaegne versioon
on käivitatud dockeri kaudu, andmebaas on üle viidud mongoDb-le, ja suhteliselt hiljuti on lisatud redis, mis alguses oli lihtsalt vahemälu jaoks. Aluseks on üks PHP mikrofraime.
Probleem
Uued saidid lisatakse konsooli käsu abil, mis samaaegselt teeb järgmist:
- Laeb sisu URL-ilt
- Seab flagi, kas HTTPS oli kergesti kättesaadav
- Salvestab veebisaidi entiteedi
- Algne HTML ja teadete ajalugu salvestatakse indekseerimise ajaloo alla
- Parsib sisu, ekstraheerib pealkirja ja kirjeldust
- Andmed salvestatakse eraldi kollektsiooni
Seda oli piisavalt, et lihtsalt säilitada saidid ja kuvada neid loendis:

Kuid idee automaatselt kõike indekseerida, kategoriseerida ja järjestada, hoida kõik ajakohasena, ei sobinud selle paradigmasse. Isegi lihtne veebimeetodi lisamine lehekülgede lisamiseks nõudis koodi dubleerimist ja lukustusi, et vältida potentsiaalset DDoS-i.
Tegelikult saab kõike teha ka sünkroonselt ja veebimeetodis lihtsalt URL-i talletada, et monsteriprogramm täidaks kõik ülesanded loendis olevate URL-ide jaoks. Kuid isegi siin kisub see sõnaks "järjekord". Ja kui helekirjas sõna "järjekord" sisse viia, siis saab kõik ülesanded jagada ja vähemalt asünkroonselt täita.
Lahendus
Sisse viia järjekorrad ja luua sündmuspõhine süsteem ülesannete töötlemiseks. Olen juba ammu soovinud proovida Redis Streams'i.
Redis Streams'i kasutamine PHP-s
Kuna minu raamistik ei kuulu kolme suurima: Symfony, Laravel, Yii hulka, siis soovisin leida sõltumatut raamatukogu. Kuid nagu selgus (esimese ülevaate põhjal) - tõsiseid eraldiseisvaid raamatukogusid ei olnud võimalik leida. Kõik, mis seondub järjekordadega, kas on viieaastase ajalooga kolmepunktine projekt või seotud raamistikuga.
Olen kuulnud, et Symfony on kasulike komponentide pakkuja, ja mõned neist olen juba kasutanud. Samuti saab mõningaid Laravel'ist kasutada, näiteks nende ORM-i, ilma et raamistik ise kohal oleks.
symfony/messenger
Esimene kandidaat tundus kohe ideaalne ja ei jätnud mingit kahtlust, et paigaldasin selle. Kuid kõigeasjalike nimedega klasside hulgast sõnumite edastamise bussi kogumine Redisiga osutus keerulisemaks, kui oleksin oodanud.

Ametlikul kodulehel olev dokumentatsioon oli piisavalt detailne, kuid initsialiseerimine oli kirjeldatud ainult Symfony jaoks, kasutades nende lemmik YML-i ja muid müstilisi meetodeid mitte-Symfony kasutajatele. Kogu-installimise protsess ei pakkunud mulle suurt huvi, eriti uusaasta puhkuse ajal. Kuid pidin sellega tegelema ning see osutus ootamatult aeganõudvaks.
Symfonyst lähtumajanduse süsteemi instantsi kohandamine ei ole samuti kõige lihtsam ülesanne piiratud ajaga:

Pärast kõikide nende asjade uurimist ja proovimist jõudsin järeldusele, et tegelen mingite poolikute lahendustega, ja otsustasin proovida midagi muud.
illuminate/queue
Selgus, et see teekond on tihedalt seotud Laravel infrastruktuuri ja hulga teiste sõltuvustega, seetõttu ei kulutanud ma sellele palju aega: paigaldasin, vaatasin, nägin sõltuvusi ja eemaldasin.
yiisoft/yii2-queue
Noh, siin oli kohe eeldus nime põhjal, et see on taas tugevalt seotud Yii2-ga. Olen seda teeki kasutanud ja see oli päris hea, kuid et see sõltub täielikult Yii2-st, ei olnud ma kunagi mõelnud.
Ülejäänud
Kõik muu, mida ma GitHubist leidsin — ebastabiilsed, vananenud ja mahajäetud projektid, millel ei olnud tähti, fork'e ega suurt arvu commite.
Tagasi symfony/messenger juurde, tehnilised detailid
Pidin selle teegiga tutvuma ja kulutades veel natuke aega, sain ma sellega hakkama. Selgus, et kõik on piisavalt selge ja lihtne. Toru instantsimiseks tegin väikese tehase, kuna mul oli plaanis mitu toru ja erinevad käitlejad.

Koheselt paar sammu:
- Loome sõnumite käitlejad, mis peavad olema lihtsalt kutsutavad
- Mähkime need HandlerDescriptor'isse (klass teegist)
- Need "Deskriptorid" mähime HandlersLocator'i instantsi
- Lisame HandlersLocator'i MessageBus'i instantsi
- Edastame SendersLocator'ile komplekti `SenderInterface`, minu puhul on see `RedisTransport` klasside instantsid, mis konfigureeritakse ilmsete viiside kaudu
- Lisame SendersLocator'i MessageBus'i instantsi
MessageBus'il on meetod `->dispatch()`, mis otsib HandlersLocator'ist vastavad käitlejad ja edastab sõnumid neile, kasutades sobivaid `SenderInterface`'d toru kaudu edastamiseks (Redis vood).
Konteineri konfiguratsioonis (antud juhul php-di) saab kogu seda sidurit konfigureerida järgmiselt:
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);
},
Siit nähtavasti, et SendersLocatoris oleme kahe erineva sõnumi jaoks määranud erineva "transporti", millest igaühel on oma ühendus vastavatele stream'idele.
Tehtud on eraldi demoprojekt, mis näitab rakendust kolmest demonist, kes suhtlevad üksteisega sellise bussiga: .
Kuid näitan, kuidas võib consumer välja näha:
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();
Selle infrastruktuuri kasutamine rakenduses
Rakendades bussi oma taustal, eraldas olenematu astmed vanast sünkroonsest käsklasest ja tegin eraldi käsitlejad, millest igaüks tegeleb oma asjadega.
Uue saidi andmebaasi lisamise toru näeb välja järgmine:

Ja kohe pärast seda oli mul tunduvalt lihtsam lisada uut funktsionaalsust, näiteks Rss-i väljavõtte ja analüüsi. Kuna see protsess nõuab ka algset sisu, siis rss-linkide väljavõtte käsitleja, nagu ka WebsiteIndexHistoryPersistor, tellib sõnumit "Content/HtmlContent", töötleb seda ja edastab vajaliku sõnumi oma toru edasi.

Lõppkokkuvõttes said mitmed deemonid, millest igaüks hoiab ühendust ainult vajalike ressurssidega. Näiteks deemon crawlers sisaldab kõiki käsitlejaid, kes nõuavad internetti sisu hankimiseks, ning deemon persister hoiab ühendust andmebaasiga.
Nüüd, asemel, et pärida andmebaasist, edastatakse pärast persisteriga sisestamist vajalikud id-d lihtsalt bussi kaudu kõigile huvitatud käsitlejatele.
Allikas: habr.com
