
Parathënie
Faqja ime, me të cilën merrem si një hob, është e destinuar për të ruajtur faqe interesante dhe faqe personale. Ky temë filloi të më interesojë në fillim të udhëtimit tim në programim, pikërisht në momentin kur më fascinonte gjetja e profesionistëve të mëdhenj që shkruanin për veten e tyre, pasionet dhe projektet e tyre. Zakoni për t'i bërë ato të njohura për vete ka mbetur edhe sot: pothuajse në çdo faqe komerciale dhe jo aq komerciale vazhdoj të shikoj në fund për lidhjet me autorët.
Zhvillimi i ideve
Versioni i parë ishte thjesht një faqe html në sitin tim personal, ku grumbulloja lidhje me përshkrime në një listë ul. Pas mbledhjes së rreth 20 faqeve në një kohë të caktuar, fillova të mendoj se kjo nuk ishte shumë efikase dhe vendosa të provoj automatizimin e procesit. Në stackoverflow kam vënë re se shumë njerëz tregojnë vende në profilet e tyre, prandaj shkrova një parser në php, i cili thjesht kalonte përmes profileve, duke filluar nga e para (adresa në SO dhe deri më sot janë të tilla: `/users/1`), nxirrte lidhjet nga tag-u përkatës dhe i ruante në SQLite.
Kjo mund të quhet versioni i dytë: një koleksion me disa mijëra URL-sh në një tabelë SQLite, e cila zëvendësoi listën statike në html. Në bazë të këtij listen bëra një kërkim të thjeshtë. Duke qenë se kishte vetëm URL, edhe kërkimi ishte vetëm për to.
Në këtë fazë kam lënë projektin dhe u ktheva tek ai pas një kohe të gjatë. Në këtë fazë përvoja ime e punës kishte arritur më shumë se tre vjet dhe ndihesha se mund të bëja diçka më të rëndësishme. Po ashtu isha i motivuar të mësoja teknologji relativisht të reja për vete.
Versioni modern
është instaluar në docker, baza është transferuar në mongoDb, dhe kohët e fundit është shtuar Redis, i cili në fillim ishte thjesht për memorizim me serf. Ndërsa si bazë përdoret një nga mikro-framorkat PHP.
Problemi
Uebfaqet e reja shtohen me një porosi nga konsola, e cila njëherazi bën këto:
- Shkarkon përmbajtjen nga URL
- Po ashtu vendos një flamur nëse HTTPS ka qenë i aksesueshëm
- Ruaj një entitet të uebfaqes
- Ruaj HTML-në origjinale dhe titujt në historinë "indeksimit"
- Parse 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 ideja për ta indekseuar, kategorizuar dhe renditur automatikisht, duke mbajtur gjithçka të përditësuar përshtatej dobët në këtë paradigmë. Edhe shtimi i thjeshtë i një metode web për të shtuar faqe kërkonte duplicimin e kodit dhe bllokime për të shmangur një potencial DDoS.
Në përgjithësi, sigurisht, gjithçka mund të bëhet ashtu si duhet, dhe në metodën e web-it të realizohet thjesht ruajtja e URL-së që një demon monstruoz të bënte të gjitha detyrat për URL-të nga lista. Megjithatë, edhe këtu shpesh përdoret fjala "radhë". Dhe nëse e implementoni një radhë, atëherë mund të ndahen të gjitha detyrat dhe të kryhen të paktën asinkronisht.
Zgjidhja
Të implementohet radhë dhe të jetë një sistem i motivuar nga ngjarje për përpunimin e të gjitha detyrave. Dhe sfida e madhe ishte të provoja Redis Streams.
Përdorimi i Redis streams në PHP
Meqë framerat e mi nuk janë nga tre gjigantët Symfony, Laravel, Yii, doja të gjeja një bibliotekë të pavarur. Por, siç rezultoi (në shqyrtimin e parë) - nuk ishte e mundur të gjeja biblioteka serioze të veçanta. Të gjitha ato që lidhen me radhët janë ose projekte me 3 komitete që janë 5 vjet mbrapa, ose janë lidhur me framerat.
Kam dëgjuar për Symfony si një ofrues të komponenteve të dobishme të veçanta, për më tepër disa prej tyre i përdor tashmë. Gjithashtu, disa gjëra nga Laravel gjithashtu mund të përdoren, për shembull ORM i tyre, pa pasur nevojë për framerin e vet.
symfony/messenger
Kandidati i parë u duk menjëherë i përsosur dhe pa dyshime e instalova atë. Por gjetja e shembujve të përdorimit jashtë Symfony ishte më e vështirë. Si të assemblez një bus të transmetimit të mesazheve nga një grumbull klasash me emra universale, që nuk thonë asgjë, dhe mbi të gjitha duke përdorur Redis?

Dokumentacioni në faqen zyrtare ishte mjaft i detajuar, por inicializimi ishte përshkruar vetëm për Symfony me ndihmën e YML dhe metodave të tjera magjike për ata që nuk ishin nga Symfony. Nuk kisha ndonjë interes në procesin e instalimit, veçanërisht në pushimet e festa. Por isha i detyruar të merrem me këtë dhe papritur ndihesha shumë gjatë.
Përpjekja për të kuptuar instancimin e sistemit nga burimet nga Symfony gjithashtu nuk ishte një detyrë triviale për afatet e ngushta:

Pas një eksperimentimi me gjithë këtë dhe duke provuar të bëja diçka me duar, arrita në përfundimin se po merrem me disa zgjidhje të këqija dhe vendosa të provoj diçka tjetër.
illuminate/queue
Rastis, kjo bibliotekë është ngjitur ngushtë me infrastrukturën Laravel dhe një sërë varësish, prandaj nuk e kam kaluar shumë kohë me të: e instalova, e shqyrtova, pashë varësitë dhe e fshiva.
yiisoft/yii2-queue
Këtu menjëherë supozohej nga emri, e po ashtu një lidhje e fortë me Yii2. Kjo bibliotekë më është dashur ta përdor dhe ishte mjaft e mirë, por nuk e kisha menduar se ishte plotësisht e varur nga Yii2.
TĂ« tjerat
Ădo gjĂ« tjetĂ«r qĂ« kam gjetur nĂ« GitHub â projekte tĂ« pasigurta, tĂ« vjetra dhe tĂ« braktisura pa yje, forka dhe shumĂ« komitete.
Kthimi në symfony/messenger, detajet teknike
Më duhej të kuptoja këtë bibliotekë dhe, pas një kohe të shkurtër, ia arrita. Doli se gjithçka ishte mjaft e qartë dhe e thjeshtë. Për të instancuar autobusin, krijova një fabrikë të vogël, pasi më duhej disa autobusa me trajtues të ndryshëm.

Vetëm disa hapa:
- Krijojmë trajtuesit e mesazheve, të cilët duhet të jenë thjesht callable
- I vëmë në HandlerDescriptor (klasa nga biblioteka)
- Këta "Deskriptorë" i vëmë në një instancë të HandlersLocator
- Shtojmë HandlersLocator në një instancë të MessageBus
- Shkëmbejmë 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 gjen trajtuesit përkatës në HandlersLocator dhe i dërgon mesazhin atyre, duke përdorur `SenderInterface` përkatëse për dërgimin përmes autobusit (Redis streams).
Në konfigurimin e konteinerit (në këtë rast php-di) e gjithë kjo lidhje mund të konfigurohet si më poshtë:
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);
},
Këtu është e qartë se në SendersLocator për dy mesazhe të ndryshme i kemi caktuar një "transport" të ndryshëm, secili me lidhje të vetë përkatës në streamet përkatëse.
Krijova një projekt të veçantë demo, që demonstroi një aplikacion nga tre demonë, që komunikojnë mes tyre nëpërmjet një autobusi të tillë: .
Por do të tregoj se si mund të jetë një konsumer:
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();
Përdorimi i kësaj infrastrukture në aplikacion
Duke zbatuar autobus në backend-in tim, kam ndarë hapa të veçantë nga ekipi i vjetër sinjak dhe kam bërë trajtues të veçantë, secili që merret me punën e vet.
Pipeline i shtimit të një faqeje të re në bazën e të dhënave rezultoi të ishte kështu:

Dhe sapo ndodhi, më u bë shumë më e lehtë të shtoja funksionalitete të reja, siç është nxjerrja dhe analizimi i Rss. Sepse ky proces gjithashtu kërkon përmbajtje burimore, menaxheri-i nxjerrjes së lidhjeve për rss si dhe WebsiteIndexHistoryPersistor regjistrohet për mesazhin «Content/HtmlContent», e përpunon atë dhe e transmeton mesazhin e nevojshëm më tej në pipeline e tij.

Në fund, rezultoi se pati disa demonë, secili prej të cilëve mban lidhjet vetëm me burimet e nevojshme. Për shembull, demoni crawlers përmban të gjithë menaxherët që kërkojnë të shkojnë në internet për përmbajtje, ndërsa demoni persister mban një lidhje me bazën e të dhënave.
Tani, në vend të selektimeve nga baza e të dhënave, id-et e nevojshme, pas futur nga persister, thjesht kalohen përmes bus-it të gjithë menaxherëve të interesuar.
Burimi: habr.com
