
Parathënie
Faqja ime, të cilën e menaxhoj si një hobit, është e destinuar për ruajtjen e faqeve interesante personale dhe të shtëpisë. Ky temë filloi të më interesojë që në fillim të rrugëtimit tim në programim, në atë moment isha i mahnitur nga gjetja e profesionistëve të mëdhenj që shkruanin për veten, pasionet dhe projektet e tyre. Njohuria për t'i zbuluar ata më është ruajtur edhe tani: pothuajse në çdo faqe tregtare dhe jo aq tregtare vazhdoj të shikoj në fund për lidhje me autorët.
Zbatimi i ideve
Versioni i parë ishte thjesht një faqe html në faqen time personale, ku unë grumbulloja lidhjet me shënime në një listë ul. Pasi mblodha rreth 20 faqe njëkohësisht, fillova të mendoj se kjo nuk ishte shumë efektive dhe vendosa të provoj të automatizoj procesin. Në stackoverflow vura re se shumë përmendnin faqet në profilin e tyre, prandaj shkrova një parser në php, i cili vetëm kalonte përmes profileve, duke filluar nga i pari (adresat në SO dhe deri më sot janë të këtij formati: `\/users\/1`), duke nxjerrë lidhjet nga etiketa e duhur dhe po i ruaja në SQLite.
Kjo mund të quhet versioni i dytë: një koleksion me dhjetëra mijëra url në një tabelë SQLite, e cila zëvendësoi listën statike në html. Në këtë listë krijova një kërkim të thjeshtë. Duke qenë se kishte vetëm url, kërkimi gjithashtu ishte thjesht mbi to.
Në këtë fazë, hoqa dorë nga projekti dhe u ktheva te ai pas një kohe të gjatë. Në këtë fazë, përvoja ime e punës ishte më shumë se tre vite dhe ndihesha se mund të bëja diçka më serioze. Njëkohësisht, kishte një dëshirë të madhe për të mësuar teknologji relativisht të reja për mua.
Versioni modern
është e debatuar në docker, baza është transferuar në mongoDb, dhe të fundit është shtuar Redis, i cili në fillim ishte thjesht për caching. Si bazë përdoret një nga mikro-framework-et e PHP.
Problemi
Faqet e reja shtohen me një komandë konsolë, e cila bën sinkronisht të siguientes:
- Shkarkon përmbajtjen për URL
- Vë një flamur për të treguar nëse HTTPS ishte i disponueshëm
- Ruaj shenjën e faqes në internet
- Ruajnë HTML-në origjinale dhe titujt në historinë e "indeksimit"
- Parse-on përmbajtjen, nxjerr Title dhe Description
- Të dhënat ruhen në një koleksion të veçantë
Kjo ishte e mjaftueshme për të ruajtur faqet dhe për t'i shfaqur ato në një listë:

Por ndonjëherë, ideja për të indeksuar, kategorizuar dhe renditur gjithçka automatikisht, duke mbajtur gjithçka të përditësuar, nuk ishte në përputhje me këtë paradigëm. Edhe shtimi i një metode web për të shtuar faqe kërkoi kopjimin e kodit dhe bllokimin për të shmangur potencialin DDoS.
Në thelb, natyrisht, gjithçka mund të bëhet edhe në mënyrë sinkrone, ndërsa në metodën web thjesht ruhet URL-ja që monstrositeti do të përmbushë të gjitha detyrat për URL-të e listës. Por prapë, këtu kërkohet fjala "radhë". Dhe nëse implementohet një radhë, atëherë mund të ndahen të gjitha detyrat dhe të kryhen të paktën asinkronisht.
Zgjidhja
Të implementojmë radhë dhe të krijojmë një sistem të procesimit të të gjitha detyrave që bazohet në ngjarje. Dhe për këtë, doja prej kohësh të provonim Redis Streams.
Përdorimi i Redis streams në PHP
Duke qenë se framework-u im nuk është nga tre gjigantët Symfony, Laravel, Yii, do të doja të gjeja një bibliotekë të pavarur. Por, siç ishte e qartë (në shqyrtimin e parë) - nuk është e mundur të gjejmë biblioteka të rëndësishme të veçanta. Çdo gjë që lidhet me radhët, është ose një projekt i vogël me 3 commit-e të para pesë vjetëve, ose e lidhur me framework-un.
Kam dëgjuar për Symfony si një ofrues të komponenteve të dobishme të veçanta, disa prej të cilave i përdor tashmë. Disa gjëra nga Laravel gjithashtu mund të përdoren, për shembull ORM e tyre, pa pasur nevojë për praninë e vetë framework-ut.
symfony/messenger
Kandidati i parë sapo duket ideal dhe pa asnjë dyshim e kam instaluar atë. Por të gjej shembuj përdorimi jashtë Symfony ishte më e komplikuar. Si të ndërtojmë nga një amullia klasash me emra universale, një autobusin për dërgimin e mesazheve, dhe madje edhe në Redis?

Dokumentacioni në faqen zyrtare ishte mjaft i detajuar, por inicializimi u përshkrua vetëm për Symfony me YML-në e tyre të preferuar dhe metoda të tjera magjike për ata që nuk janë pjesë e Symfony. Nuk kisha interes për procesin e instalimit, veçanërisht gjatë pushimeve të Vitit të Ri. Por isha i detyruar të merrem me këtë dhe me befasi zgjati shumë.
Të kuptoj sistemin e instancimit nga kodet burimore të Symfony gjithashtu ishte një detyrë e cila nuk ishte triviale për afatet e ngushta:

Pas një përpjekjeje për të gjetur dhe për të provuar diçka vetë, arrita në përfundimin se po merrem me disa këste dhe vendosa të provoj diçka tjetër.
illuminate/queue
Doli rezultoi se që kjo bibliotekë është ngjitur ngushtë me infrastrukturën Laravel dhe një mori varësish të tjera, prandaj nuk kam kaluar shumë kohë me të: e instalova, hodha një sy, pashë varësitë dhe e fshiva.
yiisoft/yii2-queue
Këtu menjëherë supozohej nga emri, përsëri një lidhje e fortë me Yii2. Kjo bibliotekë më ka qenë e nevojshme dhe ka qenë e mirë, por nuk kisha menduar se ajo varet plotësisht nga Yii2.
Të tjera
Gjithçka tjetër që kam gjetur në GitHub ishte projekte të paqëndrueshme, të vjetra dhe të braktisura pa yje, forkime dhe një numër të madh commit-esh.
Kthimi te symfony/messenger, detajet teknike
Më duhej të kuptoja këtë bibliotekë dhe, pas disa kohësh, arrita. Doli se gjithçka ishte mjaft e thjeshtë dhe e qartë. Për të instancuar busin, krijova një fabrikë të vogël, pasi supozohesha se do të kisha disa busa me përpunues të ndryshëm.

Vetëm disa hapa:
- Krijojmë përpunues të mesazheve, të cilët duhet të jenë thjesht callable
- I mbështjellim ata në HandlerDescriptor (klasë nga biblioteka)
- Këto "Deskriptorë" i mbështjellim në një instancë të HandlersLocator
- Shtojmë HandlersLocator në një instancë të MessageBus
- Kalon në SendersLocator një grup `SenderInterface`, në rastin tim instancat e klasave `RedisTransport`, të cilat konfigurohen në mënyrë të qartë
- Shtojmë SendersLocator në një instancë të MessageBus
MessageBus ka një metodë `->dispatch()`, e cila kërkon përpunuesit përkatës në HandlersLocator dhe u kalon mesazhin atyre, duke përdorur `SenderInterface` përkatëse për dërgim përmes busit (Redis streams).
Në konfigurimin e kontejnerit (në këtë rast php-di), e gjithë kjo lidhje mund të konfigurohet siç vijon:
KONTENIERI_REDIS_TRANSPORT_SECRET => funksioni (ContainerInterface $c) {
return new RedisTransport(
$c->get(KONTENIERI_REDIS_STREAM_CONNECTION_SECRET),
$c->get(KONTENIERI_SERIALIZER))
;
},
KONTENIERI_REDIS_TRANSPORT_LOG => funksioni (ContainerInterface $c) {
return new RedisTransport(
$c->get(KONTENIERI_REDIS_STREAM_CONNECTION_LOG),
$c->get(KONTENIERI_SERIALIZER))
;
},
KONTENIERI_REDIS_STREAM_RECEIVER_SECRET => funksioni (ContainerInterface $c) {
return new RedisReceiver(
$c->get(KONTENIERI_REDIS_STREAM_CONNECTION_SECRET),
$c->get(KONTENIERI_SERIALIZER)
);
},
KONTENIERI_REDIS_STREAM_RECEIVER_LOG => funksioni (ContainerInterface $c) {
return new RedisReceiver(
$c->get(KONTENIERI_REDIS_STREAM_CONNECTION_LOG),
$c->get(KONTENIERI_SERIALIZER)
);
},
KONTENIERI_REDIS_STREAM_BUS => funksioni (ContainerInterface $c) {
$sendersLocator = new SendersLocator([
AppMessagesSecretJsonMessages::class => [KONTENIERI_REDIS_TRANSPORT_SECRET],
AppMessagesDaemonLogMessage::class => [KONTENIERI_REDIS_TRANSPORT_LOG],
], $c);
$middleware[] = new SendMessageMiddleware($sendersLocator);
return new MessageBus($middleware);
},
KONTENIERI_REDIS_STREAM_CONNECTION_SECRET => funksioni (ContainerInterface $c) {
$host = 'bu-02-redis';
$port = 6379;
$dsn = "redis://$host:$port";
$options = [
'stream' => 'secret',
'group' => 'default',
'consumer' => 'default',
];
return Connection::fromDsn($dsn, $options);
},
KONTENIERI_REDIS_STREAM_CONNECTION_LOG => funksioni (ContainerInterface $c) {
$host = 'bu-02-redis';
$port = 6379;
$dsn = "redis://$host:$port";
$options = [
'stream' => 'log',
'group' => 'default',
'consumer' => 'default',
];
return Connection::fromDsn($dsn, $options);
},
Këtu është e qartë se në SendersLocator për dy mesazhe të ndryshme kemi caktuar transport të ndryshëm, secili me lidhjen e tij për stream-at përkatës.
Kam bërë një projekt demo të veçantë, që demonstrohet një aplikacion me tre demonë që komunikojnë mes tyre përmes një autobusi të tillë: .
Por do të tregoj se si mund të organizohet konsumatori:
përdor AppMessagesDaemonLogMessage;
përdor SymfonyComponentMessengerHandlerHandlerDescriptor;
përdor SymfonyComponentMessengerHandlerHandlersLocator;
përdor SymfonyComponentMessengerMessageBus;
përdor SymfonyComponentMessengerMiddlewareHandleMessageMiddleware;
përdor SymfonyComponentMessengerMiddlewareSendMessageMiddleware;
përdor SymfonyComponentMessengerTransportSenderSendersLocator;
kërkohet një herë në __DIR__ . ' / .. / vendor / autoload.php';
/** @var PsrContainerContainerInterface $container * /
$container = kërkohet një herë('config / container.php');
$handlers = [
DaemonLogMessage::class => [
new HandlerDescriptor(
function (DaemonLogMessage $m) {
error_log('DaemonLogHandler: mesazhi u trajtua: / ' . $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();
Përdorimi i kësaj infrastrukture në aplikacion
Duke implementuar bus-in në backend-in tim, kam nxjerrë nivele të veçanta nga komandat e vjetra sinkrone dhe kam bërë handlers të veçantë, secila prej të cilëve merret me detyrat e veta.
Pipeline-i për të shtuar një faqe të re në bazën e të dhënave është i tillë:

Dhe menjëherë pas kësaj, më u bë shumë më e lehtë të shtoja funksionalitete të reja, për shembull, nxjerrjen dhe analizimin e Rss. Duke qenë se ky proces gjithashtu kërkon përmbajtjen origjinale, handler-i që nxjerr lidhjet për rss, ashtu si dhe WebsiteIndexHistoryPersistor, nënshkruhet në mesazhin "Content / HtmlContent", eproceson dhe dërgon mesazhin e dëshiruar përmes pipeline-it të tij më tej.

Në fund të fundit, dolën disa demonë, secili prej të cilëve mban lidhje vetëm me burimet e nevojshme. Për shembull, demo crawlers përmban të gjithë handlers që kërkojnë të shkojnë në internet për përmbajtje, ndërsa demo persister merr lidhjen me bazën e të dhënave.
Tani, në vend të selektimeve nga baza e të dhënave, ID-të e nevojshme pas vendosjes nga persister-i thjesht dërgohen përmes bus-it të gjithë handlers të interesuar.
Burimi: habr.com
