
Prefazione
Il mio sito, che gestisco come hobby, è dedicato alla raccolta di interessanti pagine personali e siti web. Questo tema ha iniziato a coinvolgermi all'inizio del mio percorso nella programmazione, quando ero affascinato dalla scoperta di grandi professionisti che scrivono su di loro, le loro passioni e progetti. L'abitudine di scoprirli è rimasta e ancora oggi: quasi su ogni sito commerciale e non, continuo a dare un'occhiata al footer in cerca di link agli autori.
Realizzazione dell'idea
La prima versione era semplicemente una pagina HTML sul mio sito personale, dove raccoglievo link con didascalie in una lista ul. Dopo aver accumulato circa 20 pagine in un certo periodo, ho cominciato a pensare che non fosse molto efficace e ho deciso di provare ad automatizzare il processo. Su stackoverflow notavo che molti indicavano siti nei loro profili, quindi ho scritto un parser in PHP, che semplicemente scorreva i profili a partire dal primo (gli indirizzi su SO e fino ad oggi hanno questo formato: `/users/1`), estraeva link dal tag giusto e li memorizzava in SQLite.
Questo può essere definito come la seconda versione: una collezione di decine di migliaia di URL in un database SQLite, che ha sostituito l'elenco statico in HTML. Su questa lista ho creato una ricerca semplice. Poiché c'erano solo URL, la ricerca è stata facile.
A questo punto ho abbandonato il progetto e sono tornato a distanza di tempo. La mia esperienza lavorativa era ormai superiora ai tre anni e sentivo di poter realizzare qualcosa di più impegnativo. Inoltre, avevo una grande voglia di apprendere tecnologie relativamente nuove per me.
Versione moderna
è eseguita in Docker, il database è stato migrato su MongoDB e, da poco, è stato aggiunto Redis, inizialmente utilizzato solo per la cache. Come base viene utilizzato uno dei microframework PHP.
Problema
I nuovi siti vengono aggiunti con un comando da console, che esegue simultaneamente le seguenti operazioni:
- Scarica il contenuto dall'URL
- Imposta un flag per indicare se l'HTTPS era disponibile
- Salva l'entità del sito web
- Salva l'HTML originale e le intestazioni nella cronologia dell'indicizzazione
- Estrae il contenuto, estraendo Title e Description
- I dati vengono salvati in una collezione separata
Questo era sufficiente per memorizzare semplicemente i siti e mostrarli in un elenco:

Tuttavia, l'idea di indicizzare, categorizzare e classificare tutto automaticamente, mantenendo tutto aggiornato, si adattava poco a questa logica. Anche la semplice aggiunta di un metodo web per inserire pagine ha richiesto la duplicazione del codice e bloccaggi per evitare potenziali attacchi DDoS.
In realtà, tutto può essere fatto in modo sincrono, e nel metodo web si potrebbe semplicemente salvare l'URL affinché un demone mostruoso esegua tutte le operazioni per gli URL dell'elenco. Ma anche qui si fa spazio alla parola "coda". E se si introduce una coda, si possono suddividere tutte le operazioni e portarle a termine almeno in modo asincrono.
Soluzione
Integrare le code e realizzare un sistema di elaborazione delle operazioni basato su eventi. E da tempo desideravo provare Redis Streams.
Utilizzo dei Redis Streams in PHP
Poiché il mio framework non appartiene ai tre giganti Symfony, Laravel, Yii, desideravo trovare una libreria indipendente. Ma, a quanto pare (dopo una prima valutazione), è impossibile trovare librerie serie separate. Tutto ciò che riguarda le code è o un progetto con 3 commit di cinque anni fa, o è legato a un framework.
Ho sentito parlare di Symfony come fornitore di componenti utili e ne utilizzo già alcuni. Inoltre, anche alcuni elementi di Laravel possono essere utilizzati, come il loro ORM, senza dover usare l'intero framework.
symfony/messenger
Il primo candidato mi è sembrato subito ideale e non ho avuto dubbi nel procedere all'installazione. Tuttavia, trovare esempi di utilizzo al di fuori di Symfony si è rivelato più complicato. Come costruire un bus per la trasmissione di messaggi da una serie di classi con nomi generici che non dicono nulla, utilizzando Redis?

La documentazione sul sito ufficiale era piuttosto dettagliata, ma l'inizializzazione era descritta solo per Symfony, usando il loro amato YML e altri metodi magici poco familiari per chi non è un simfonista. Non avevo particolare interesse nel processo di installazione, soprattutto durante le vacanze di Capodanno. Tuttavia, ho dovuto occuparmene e inaspettatamente ci ho messo molto tempo.
Cercare di capire come istanziare un sistema usando le fonti di Symfony non è affatto una questione banale, specialmente con i tempi stretti:

Dopo aver frugato tra tutto questo e aver provato a fare qualcosa manualmente, sono giunto alla conclusione che stavo solo creando delle soluzioni temporanee e ho deciso di provare qualcos'altro.
illuminate/queue
Si è scoperto che questa libreria è strettamente legata all'infrastruttura di Laravel e a molte altre dipendenze, quindi non ho dedicato molto tempo: l'ho installata, ho visto le dipendenze e l'ho eliminata.
yiisoft/yii2-queue
Qui si presupponeva fin dal nome, ancora una volta una forte dipendenza da Yii2. Ho dovuto utilizzare questa libreria ed era abbastanza buona, ma non pensavo che fosse completamente dipendente da Yii2.
Altre
Tutto il resto che ho trovato su GitHub erano progetti obsoleti e poco affidabili, senza stelle, fork e un gran numero di commit.
Ritorno a symfony/messenger, dettagli tecnici
Ho dovuto approfondire questa libreria e, dopo aver dedicato un po' di tempo, ci sono riuscito. Si è rivelato tutto abbastanza conciso e semplice. Per istanziare il bus ho creato una piccola fabbrica, poiché avevo in programma di avere più bus con diversi gestori.

Solo pochi passaggi:
- Creiamo gestori di messaggi che devono essere semplicemente callable
- Li avvolgiamo in HandlerDescriptor (classe della libreria)
- Questi 'Descrittori' li avvolgiamo in un'istanza di HandlersLocator
- Aggiungiamo HandlersLocator in un'istanza di MessageBus
- Passiamo a SendersLocator un insieme di `SenderInterface`, nel mio caso istanze delle classi `RedisTransport`, che vengono configurate in modo evidente.
- Aggiungiamo SendersLocator all'istanza di MessageBus.
MessageBus ha un metodo `->dispatch()`, che cerca i gestori corrispondenti in HandlersLocator e invia il messaggio a loro, utilizzando i corrispondenti `SenderInterface` per l'invio tramite il bus (flussi Redis).
Nella configurazione del contenitore (in questo caso php-di) tutta questa connessione può essere configurata in questo modo:
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);
},
Qui si può vedere che in SendersLocator abbiamo assegnato un "trasporto" diverso per due messaggi, ognuno dei quali ha la propria connessione ai relativi stream.
Ho creato un progetto demo separato che mostra un'applicazione composta da tre demoni che comunicano tra loro tramite un bus di questo tipo: .
Ma vi mostrerò come potrebbe essere strutturato il consumer:
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: messaggio gestito: / ' . $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();
L'uso di questa infrastruttura nell'applicazione
Implementando il bus nel mio backend, ho estratto fasi separate dal vecchio comando sincrono e creato gestori distinti, ognuno dei quali si occupa della propria parte.
Il pipeline per aggiungere un nuovo sito al database è stato il seguente:

E subito dopo, è diventato molto più facile aggiungere nuove funzionalità, come l'estrazione e il parsing di Rss. Poiché questo processo richiede anche contenuti di origine, il gestore per l'estrazione dei collegamenti RSS, così come il WebsiteIndexHistoryPersistor, si iscrive al messaggio "Content/HtmlContent", lo elabora e trasmette il messaggio necessario tramite il suo pipeline.

Alla fine, sono stati creati diversi demoni, ognuno dei quali mantiene connessioni solo con le risorse necessarie. Per esempio, il demone crawlers contiene tutti i gestori che richiedono una connessione a Internet per i contenuti, mentre il demone persister mantiene la connessione al database.
Ora, invece delle selezioni dal database, i necessari id dopo l'inserimento da parte del persister vengono semplicemente trasmessi tramite il bus a tutti i gestori interessati.
Fonte: habr.com
