
Introduzione
Il mio sito, che gestisco come hobby, è destinato a conservare pagine web interessanti e siti personali. Questo tema ha iniziato a interessarmi all'inizio del mio percorso nella programmazione, quando ero affascinato nel trovare grandi professionisti che scrivono di sé, delle proprie passioni e dei progetti. L'abitudine di scoprirli è rimasta e ancora oggi: quasi su ogni sito commerciale e non, continuo a controllare il footer in cerca di link agli autori.
Implementazione dell'idea
La prima versione era semplicemente una pagina html sul mio sito personale, dove raccoglievo link con descrizioni in un elenco ul. Dopo un po' di tempo, avendo accumulato circa 20 pagine, ho cominciato a pensare che non fosse molto efficace e ho deciso di provare ad automatizzare il processo. Su stackoverflow notavo che molti indicavano i loro siti nei profili, quindi ho scritto un parser in php che semplicemente scorreva i profili, partendo dal primo (gli indirizzi su SO sono ancora oggi di questo tipo: `\/users\/1`), estraeva i link dal tag desiderato e li salvava in SQLite.
Questo può essere considerato la seconda versione: una collezione di decine di migliaia di url in una tabella SQLite, che ha sostituito l'elenco statico in html. A partire da questo elenco ho realizzato una semplice funzione di ricerca. Poiché c'erano solo url, anche la ricerca era semplicemente su di essi.
A questo punto ho abbandonato il progetto e dopo molto tempo ci sono ritornato. In quel periodo l'esperienza lavorativa era già superiore ai tre anni e sentivo che potevo fare qualcosa di più serio. Inoltre, avevo una forte voglia di apprendere tecnologie relativamente nuove per me.
Versione moderna
è stata distribuita in docker, il database è stato trasferito su mongoDb e da relativamente poco è stato aggiunto redis, inizialmente solo per la cache. Come base viene utilizzato uno dei microframework PHP.
Problema
I nuovi siti vengono aggiunti tramite un comando da console che esegue le seguenti operazioni in modo sincrono:
- Scarica il contenuto dall'URL
- Imposta un flag per segnalare se l'HTTPS fosse disponibile
- Salva l'entità del sito web
- Salva l'HTML originale e le intestazioni nella cronologia "indicizzazione"
- Analizza il contenuto, estraendo il Title e la Description
- Salva i dati in una collezione separata
Questo era sufficiente per semplicemente conservare i siti e mostrarli in un elenco:

Ma l'idea di indicizzare, categorizzare e classificare tutto automaticamente, mantenendo tutto aggiornato, si adattava poco a questa paradigma. Anche l'aggiunta di un semplice metodo web per aggiungere pagine ha richiesto la duplicazione del codice e blocchi per evitare potenziali DDoS.
In effetti, si può fare tutto in modo sincrono, e nel metodo web si potrebbe semplicemente salvare l'URL in modo che il mostruoso demone esegua tutte le operazioni per gli URL nella lista. Ma anche qui si fa strada la parola «coda». E se si introduce una coda, si possono separare tutte le operazioni e eseguirle almeno in modo asincrono.
Soluzione
Implementare le code e creare un sistema di elaborazione di tutte le operazioni basato su eventi. E da tempo volevo provare Redis Streams.
Uso dei Redis streams in PHP
Poiché il mio framework non è tra i giganti Symfony, Laravel, Yii, volevo trovare una libreria indipendente. Ma, come si è rivelato (dopo una prima analisi), era impossibile trovare librerie serie e separate. Tutto ciò che riguarda le code è o un piccolo progetto con 3 commit risalenti a cinque anni fa, o è legato a un framework.
Ho sentito parlare di Symfony come fornitore di singoli componenti utili, e alcuni li utilizzo già. Anche dal Laravel si può usare qualcosa, come il loro ORM, senza la necessità di avere il framework stesso.
symfony/messenger
Il primo candidato si è subito rivelato ideale e senza alcun dubbio l'ho installato. Ma trovare esempi di utilizzo al di fuori di Symfony si è rivelato più difficile. Come assemblare un bus per la trasmissione dei messaggi a partire da un insieme di classi con nomi universali e poco descrittivi, e inoltre su Redis?

La documentazione sul sito ufficiale era abbastanza dettagliata, ma l'inizializzazione era descritta solo per Symfony utilizzando il loro amato YML e altri metodi magici per chi non è un utente di Symfony. Non avevo interesse nel processo di installazione, soprattutto durante le vacanze di Capodanno. Ma è stato necessario dedicarsi a questo e, inaspettatamente, ha preso molto tempo.
Cercare di capire come istanziare il sistema basandomi sui sorgenti di Symfony non è affatto un compito banale per tempi ristretti:

Dopo aver esplorato tutto questo e provando a fare qualcosa a mano, sono giunto alla conclusione che stavo facendo dei patch e ho deciso di provare qualcos'altro.
illuminate/queue
Si è scoperto che questa libreria è strettamente legata all'infrastruttura Laravel e a molte altre dipendenze, quindi non ho speso molto tempo su di essa: l'ho installata, ho dato un'occhiata, ho visto le dipendenze e l'ho rimossa.
yiisoft/yii2-queue
Beh, qui si supponeva immediatamente dal nome una forte dipendenza da Yii2. Ho dovuto usare questa libreria e non era male, ma non pensavo che fosse completamente dipendente da Yii2.
Altre
Tutto il resto che ho trovato su GitHub erano progetti obsoleti e inaffidabili, senza stelle, fork e un gran numero di commit.
Ritorno a symfony/messenger, dettagli tecnici
Ho dovuto capire questa libreria e, dopo aver speso un po' di tempo, ci sono riuscito. Si è rivelato tutto abbastanza conciso e semplice. Per l'istanza del bus ho creato una piccola fabbrica, poiché prevedo di utilizzare diversi bus con diversi gestori.

Solo pochi passaggi:
- Creiamo i gestori dei messaggi, che devono essere semplicemente chiamabili
- Avvolgiamoli in HandlerDescriptor (una classe della libreria)
- Questi 'Descriptor' 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 si configurano in modo ovvio
- Aggiungiamo SendersLocator in un'istanza di MessageBus
MessageBus ha un metodo `->dispatch()`, che cerca i gestori corrispondenti in HandlersLocator e invia loro il messaggio, utilizzando i corrispondenti `SenderInterface` per l'invio attraverso il bus (flussi Redis).
Nella configurazione del contenitore (in questo caso php-di) tutto questo legame può essere configurato 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 vede che nel SendersLocator per due diversi messaggi abbiamo assegnato un diverso "trasporto", ognuno dei quali ha la sua connessione ai rispettivi stream.
Ho creato un progetto demo separato che dimostra un'applicazione composta da tre demoni che comunicano tra loro tramite un bus di questo tipo: .
Ma vi mostrerò come può essere strutturato un 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();
Utilizzo di questa infrastruttura nell'applicazione
Implementando il bus nel mio backend, ho separato singole fasi dalla vecchia chiamata sincrona e creato gestori separati, ciascuno dedicato al proprio compito.
Il pipeline per l'aggiunta di un nuovo sito al database è stato il seguente:

E subito dopo, è diventato molto più semplice aggiungere nuove funzionalità, ad esempio l'estrazione e il parsing di Rss. Poiché questo processo richiede anche contenuto di partenza, il gestore estrattore di link per rss, proprio come il WebsiteIndexHistoryPersistor, si iscrive al messaggio «Content/HtmlContent», lo elabora e inoltra il messaggio necessario lungo il suo pipeline.

Il risultato finale è stato un certo numero di demoni, ciascuno dei quali mantiene connessioni solamente con le risorse necessarie. Ad esempio, il demone crawlers contiene tutti i gestori che richiedono l'accesso a internet per il contenuto, mentre il demone persister mantiene la connessione al database.
Ora, invece di fare selezioni dal database, i necessari ID dopo l'inserimento dal persister vengono semplicemente trasmessi attraverso il bus a tutti i gestori interessati.
Fonte: habr.com
