
Prólogo
Mi sitio, que hago como hobby, está diseñado para almacenar páginas de inicio interesantes y sitios personales. Este tema comenzó a interesarme desde el comienzo de mi camino en la programación, en ese momento estaba encantado de encontrar excelentes profesionales que escriben sobre sí mismos, sus pasatiempos y proyectos. El hábito de descubrirlos ha permanecido por ahora: en casi todos los sitios web comerciales y no muy, sigo buscando en el pie de página en busca de enlaces a autores.
Implementación de la idea.
La primera versión era solo una página html en mi sitio personal, donde ponía enlaces con firmas en una lista ul. Después de haber escrito 20 páginas durante algún tiempo, comencé a pensar que no era muy efectivo y decidí intentar automatizar el proceso. En stackoverflow, noté que muchos indican sitios en sus perfiles, así que escribí un analizador de php que solo recorrió los perfiles, comenzando desde el primero (dirección en SO y hasta el día de hoy como este: `/ users / 1`), extrajo enlaces desde la etiqueta deseada y apilada en SQLite.
Esto se puede llamar la segunda versión: una colección de decenas de miles de URL en una placa SQLite que reemplazó la lista estática en html. Hice una búsqueda simple en esta lista. Porque solo había urls, luego la búsqueda era solo para ellos.
En esta etapa, abandoné el proyecto y volví a él, después de mucho tiempo. En esta etapa, mi experiencia laboral ya era de más de tres años y sentí que podía hacer algo más serio. Además, había un gran deseo de dominar tecnologías relativamente nuevas para ellos mismos.
Versión moderna
El proyecto se implementó en la ventana acoplable, la base de datos se transfirió a mongoDb y, relativamente recientemente, se agregaron rábanos, que al principio solo eran para el almacenamiento en caché. Como base, se utiliza uno de los microframes PHP.
El problema
Los sitios nuevos se agregan mediante un comando de consola que hace lo siguiente sincrónicamente:
- Descargar contenido por URL
- Indica si HTTPS estaba disponible
- Conserva la esencia del sitio web.
- El código fuente HTML y los encabezados se guardan en el historial de indexación
- Analiza contenido, recupera Título y Descripción
- Guarda datos en una colección separada.
Esto fue suficiente para almacenar sitios y mostrarlos en una lista:

Pero la idea de indexar, clasificar y clasificar automáticamente todo, mantener todo actualizado, encaja mal en este paradigma. Incluso agregar un método web para agregar páginas requería duplicación y bloqueo de código para evitar posibles DDoS.
En general, por supuesto, todo se puede hacer sincrónicamente, y en el método web, simplemente guarde la URL para asegurarse de que el monstruoso demonio realice todas las tareas para las URL de la lista. Pero de todos modos, incluso aquí la palabra "girar" suplica. Y si se implementa la cola, todas las tareas se pueden dividir y realizar al menos de forma asincrónica.
Solución
Introduzca colas y cree un sistema de procesamiento basado en eventos para todas las tareas. Y solo por mucho tiempo quise probar Redis Streams.
Usando flujos de Redis en PHP
Porque No tengo un marco de los tres gigantes Symfony, Laravel, Yii, y me gustaría encontrar una biblioteca independiente. Pero, como resultó (después del primer examen), es imposible encontrar bibliotecas individuales serias. Todo lo que está asociado con las colas es una proyección de 3 confirmaciones de hace cinco años o está vinculado a un marco.
He oído hablar de Symfony como proveedor de algunos componentes útiles, y ya uso algunos. Y también de Laravel, también se puede usar algo, por ejemplo, su ORM, sin la presencia del marco en sí.
Symfony / Messenger
El primer candidato parecía inmediatamente ideal, y sin ninguna duda lo instalé. Pero fue más difícil encontrar ejemplos de uso en Google fuera de Symfony. ¿Cómo recolectar de un montón de clases con nombres universales, sin decir nada, un autobús para enviar mensajes, e incluso en Redis?

La documentación en el sitio oficial era bastante detallada, pero la inicialización se describió solo para Symfony usando su YML favorito y otros métodos mágicos para un no sinfonista. No tenía interés en el proceso de instalación, especialmente durante las vacaciones de Año Nuevo. Pero tuve que hacer esto durante un tiempo inesperadamente largo.
Intentar descubrir cómo crear una instancia de un sistema utilizando fuentes de Symfony tampoco es la tarea más trivial para plazos ajustados:

Hurgando en esto e intentando hacer algo con mis manos, llegué a la conclusión de que estaba haciendo algún tipo de muletas y decidí probar otra cosa.
iluminar / hacer cola
Resultó que esta biblioteca estaba estrechamente vinculada a la infraestructura de Laravel y a un montón de otras dependencias, por lo que no pasé mucho tiempo en ella: instalé, miré, vi dependencias y la eliminé.
yiisoft / yii2-queue
Bueno, aquí se asumió de inmediato por el nombre, una vez más un vínculo estrecho con Yii2. Tuve que usar esta biblioteca y no estaba mal, pero no pensé que dependiera completamente de Yii2.
El resto
Todo lo demás que encontré en el github era proyecciones anticuadas y abandonadas poco confiables sin estrellas, tenedores y una gran cantidad de confirmaciones.
Regresar a Symfony / Messenger, detalles técnicos
Tuve que lidiar con esta biblioteca y después de pasar más tiempo pude. Resultó que todo es bastante conciso y simple. Para la instanciación del autobús, hice una pequeña fábrica, porque Tenía varios neumáticos con diferentes manipuladores.

Solo unos pocos pasos:
- Cree manejadores de mensajes que deberían ser invocables
- Envuélvalos en HandlerDescriptor (clase de la biblioteca)
- Envolvemos estos "Descriptores" en la instancia de HandlersLocator.
- Agregar HandlersLocator a la instancia de MessageBus
- Pasamos al SendersLocator un conjunto de `SenderInterface`, en mi caso, instancias de las clases` RedisTransport`, que están configuradas de manera obvia
- Agregar SendersLocator a la instancia de MessageBus
MessageBus tiene un método `-> dispatch ()`, que busca los manejadores apropiados en el HandlersLocator y les pasa el mensaje, utilizando el `SenderInterface` correspondiente para enviar a través del bus (transmisiones de Redis).
En la configuración del contenedor (en este caso php-di), todo este grupo se puede configurar de la siguiente manera:
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([ \App\Messages\SecretJsonMessages::class => [CONTAINER_REDIS_TRANSPORT_SECRET], \App\Messages\DaemonLogMessage::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); },
Se puede ver que en SendersLocator asignamos un "transporte" diferente para dos mensajes diferentes, cada uno de los cuales tiene su propia conexión con las secuencias correspondientes.
Hice un proyecto de demostración por separado que demuestra una aplicación de tres demonios que se comunican entre sí mediante un bus de este tipo:
https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus .
Pero le mostraré cómo se puede organizar un consumidor:
use App\Messages\DaemonLogMessage; use Symfony\Component\Messenger\Handler\HandlerDescriptor; use Symfony\Component\Messenger\Handler\HandlersLocator; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; use Symfony\Component\Messenger\Middleware\SendMessageMiddleware; use Symfony\Component\Messenger\Transport\Sender\SendersLocator; require_once __DIR__ . '/../vendor/autoload.php'; $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 \Symfony\Component\Messenger\Worker($receivers, $bus, $container->get(CONTAINER_EVENT_DISPATCHER)); $w->run();
Usar esta infraestructura en una aplicación
Después de implementar el bus en mi backend, seleccioné pasos individuales del antiguo equipo síncrono e hice controladores separados, cada uno de los cuales se dedica a su propio negocio.
La tubería para agregar un nuevo sitio a la base de datos es la siguiente:

Y justo después de eso, me resultó mucho más fácil agregar nueva funcionalidad, por ejemplo, extraer y analizar Rss. Porque Dado que este proceso también requiere contenido fuente, el controlador-extractor del enlace rss, así como WebsiteIndexHistoryPersistor, se suscribe al mensaje "Contenido / Contenido HTML", lo procesa y pasa el mensaje deseado a su canalización.

Al final, resultó que varios demonios, cada uno de los cuales mantiene conexiones solo con los recursos necesarios. Por ejemplo, el demonio de
rastreadores contiene todos los controladores que requieren ir a Internet para obtener contenido, y el demonio
persistente mantiene una conexión a la base de datos.
Ahora, en lugar de seleccionar de la base de datos, la identificación requerida después de ser insertada por la persistencia simplemente se pasa a través del bus a todos los manejadores interesados.