trustgraph/docs/tech-specs/pubsub.es.md
Alex Jenkins 8954fa3ad7 Feat: TrustGraph i18n & Documentation Translation Updates (#781)
Native CLI i18n: The TrustGraph CLI has built-in translation support
that dynamically loads language strings. You can test and use
different languages by simply passing the --lang flag (e.g., --lang
es for Spanish, --lang ru for Russian) or by configuring your
environment's LANG variable.

Automated Docs Translations: This PR introduces autonomously
translated Markdown documentation into several target languages,
including Spanish, Swahili, Portuguese, Turkish, Hindi, Hebrew,
Arabic, Simplified Chinese, and Russian.
2026-04-14 12:08:32 +01:00

965 lines
36 KiB
Markdown

---
layout: default
title: "Infraestructura Pub/Sub"
parent: "Spanish (Beta)"
---
# Infraestructura Pub/Sub
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
## Resumen
Este documento cataloga todas las conexiones entre el código base de TrustGraph y la infraestructura pub/sub. Actualmente, el sistema está codificado para usar Apache Pulsar. Este análisis identifica todos los puntos de integración para informar futuras refactorizaciones hacia una abstracción pub/sub configurable.
## Estado actual: Puntos de integración de Pulsar
### 1. Uso directo del cliente de Pulsar
**Ubicación:** `trustgraph-flow/trustgraph/gateway/service.py`
La puerta de enlace de la API importa y crea una instancia directamente del cliente de Pulsar:
**Línea 20:** `import pulsar`
**Líneas 54-61:** Creación directa de `pulsar.Client()` con `pulsar.AuthenticationToken()` opcional
**Líneas 33-35:** Configuración predeterminada del host de Pulsar desde variables de entorno
**Líneas 178-192:** Argumentos de la línea de comandos para `--pulsar-host`, `--pulsar-api-key` y `--pulsar-listener`
**Líneas 78, 124:** Pasa `pulsar_client` a `ConfigReceiver` y `DispatcherManager`
Esta es la única ubicación que crea directamente un cliente de Pulsar fuera de la capa de abstracción.
### 2. Marco base del procesador
**Ubicación:** `trustgraph-base/trustgraph/base/async_processor.py`
La clase base para todos los procesadores proporciona conectividad de Pulsar:
**Línea 9:** `import _pulsar` (para el manejo de excepciones)
**Línea 18:** `from . pubsub import PulsarClient`
**Línea 38:** Crea `pulsar_client_object = PulsarClient(**params)`
**Líneas 104-108:** Propiedades que exponen `pulsar_host` y `pulsar_client`
**Línea 250:** El método estático `add_args()` llama a `PulsarClient.add_args(parser)` para los argumentos de la línea de comandos
**Líneas 223-225:** Manejo de excepciones para `_pulsar.Interrupted`
Todos los procesadores heredan de `AsyncProcessor`, lo que convierte a este en el punto de integración central.
### 3. Abstracción del consumidor
**Ubicación:** `trustgraph-base/trustgraph/base/consumer.py`
Consume mensajes de colas e invoca funciones de controlador:
**Importaciones de Pulsar:**
**Línea 12:** `from pulsar.schema import JsonSchema`
**Línea 13:** `import pulsar`
**Línea 14:** `import _pulsar`
**Uso específico de Pulsar:**
**Líneas 100, 102:** `pulsar.InitialPosition.Earliest` / `pulsar.InitialPosition.Latest`
**Línea 108:** Envoltorio `JsonSchema(self.schema)`
**Línea 110:** `pulsar.ConsumerType.Shared`
**Líneas 104-111:** `self.client.subscribe()` con parámetros específicos de Pulsar
**Líneas 143, 150, 65:** Métodos `consumer.unsubscribe()` y `consumer.close()`
**Línea 162:** Excepción `_pulsar.Timeout`
**Líneas 182, 205, 232:** `consumer.acknowledge()` / `consumer.negative_acknowledge()`
**Archivo de especificación:** `trustgraph-base/trustgraph/base/consumer_spec.py`
**Línea 22:** Hace referencia a `processor.pulsar_client`
### 4. Abstracción del productor
**Ubicación:** `trustgraph-base/trustgraph/base/producer.py`
Envía mensajes a colas:
**Importaciones de Pulsar:**
**Línea 2:** `from pulsar.schema import JsonSchema`
**Uso específico de Pulsar:**
**Línea 49:** Envoltorio `JsonSchema(self.schema)`
**Líneas 47-51:** `self.client.create_producer()` con parámetros específicos de Pulsar (tema, esquema, habilitación de fragmentación)
**Líneas 31, 76:** Método `producer.close()`
**Líneas 64-65:** `producer.send()` con mensaje y propiedades
**Archivo de especificación:** `trustgraph-base/trustgraph/base/producer_spec.py`
**Línea 18:** Hace referencia a `processor.pulsar_client`
### 5. Abstracción del publicador
**Ubicación:** `trustgraph-base/trustgraph/base/publisher.py`
Publicación de mensajes asíncrona con almacenamiento en búfer de cola:
**Importaciones de Pulsar:**
**Línea 2:** `from pulsar.schema import JsonSchema`
**Línea 6:** `import pulsar`
**Uso específico de Pulsar:**
**Línea 52:** Envoltorio `JsonSchema(self.schema)`
**Líneas 50-54:** `self.client.create_producer()` con parámetros específicos de Pulsar
**Líneas 101, 103:** `producer.send()` con mensaje y propiedades opcionales
**Líneas 106-107:** Métodos `producer.flush()` y `producer.close()`
### 6. Abstracción del suscriptor
**Ubicación:** `trustgraph-base/trustgraph/base/subscriber.py`
Proporciona la distribución de mensajes a múltiples destinatarios desde colas:
**Importaciones de Pulsar:**
**Línea 6:** `from pulsar.schema import JsonSchema`
**Línea 8:** `import _pulsar`
**Uso específico de Pulsar:**
**Línea 55:** `JsonSchema(self.schema)` wrapper
**Línea 57:** `self.client.subscribe(**subscribe_args)`
**Líneas 101, 136, 160, 167-172:** Excepciones de Pulsar: `_pulsar.Timeout`, `_pulsar.InvalidConfiguration`, `_pulsar.AlreadyClosed`
**Líneas 159, 166, 170:** Métodos de consumidor: `negative_acknowledge()`, `unsubscribe()`, `close()`
**Líneas 247, 251:** Reconocimiento de mensajes: `acknowledge()`, `negative_acknowledge()`
**Archivo de especificaciones:** `trustgraph-base/trustgraph/base/subscriber_spec.py`
**Línea 19:** Referencias a `processor.pulsar_client`
### 7. Sistema de esquemas (Heart of Darkness)
**Ubicación:** `trustgraph-base/trustgraph/schema/`
Cada esquema de mensaje en el sistema se define utilizando el marco de esquemas de Pulsar.
**Primitivos principales:** `schema/core/primitives.py`
**Línea 2:** `from pulsar.schema import Record, String, Boolean, Array, Integer`
Todos los esquemas heredan de la clase base de Pulsar `Record`
Todos los tipos de campo son tipos de Pulsar: `String()`, `Integer()`, `Boolean()`, `Array()`, `Map()`, `Double()`
**Esquemas de ejemplo:**
`schema/services/llm.py` (Línea 2): `from pulsar.schema import Record, String, Array, Double, Integer, Boolean`
`schema/services/config.py` (Línea 2): `from pulsar.schema import Record, Bytes, String, Boolean, Array, Map, Integer`
**Nomenclatura de temas:** `schema/core/topic.py`
**Líneas 2-3:** Formato del tema: `{kind}://{tenant}/{namespace}/{topic}`
Esta estructura de URI es específica de Pulsar (por ejemplo, `persistent://tg/flow/config`)
**Impacto:**
Todas las definiciones de mensajes de solicitud/respuesta en todo el código base utilizan esquemas de Pulsar
Esto incluye servicios para: config, flow, llm, prompt, query, storage, agent, collection, diagnosis, library, lookup, nlp_query, objects_query, retrieval, structured_query
Las definiciones de esquemas se importan y utilizan ampliamente en todos los procesadores y servicios
## Resumen
### Dependencias de Pulsar por Categoría
1. **Instanciación del cliente:**
Directo: `gateway/service.py`
Abstracto: `async_processor.py``pubsub.py` (PulsarClient)
2. **Transporte de mensajes:**
Consumidor: `consumer.py`, `consumer_spec.py`
Productor: `producer.py`, `producer_spec.py`
Publicador: `publisher.py`
Suscriptor: `subscriber.py`, `subscriber_spec.py`
3. **Sistema de esquemas:**
Tipos base: `schema/core/primitives.py`
Todos los esquemas de servicio: `schema/services/*.py`
Nomenclatura de temas: `schema/core/topic.py`
4. **Conceptos específicos de Pulsar requeridos:**
Mensajería basada en temas
Sistema de esquemas (Registro, tipos de campo)
Suscripciones compartidas
Reconocimiento de mensajes (positivo/negativo)
Posicionamiento del consumidor (más temprano/más reciente)
Propiedades del mensaje
Posiciones iniciales y tipos de consumidor
Soporte de fragmentación
Temas persistentes frente a no persistentes
### Desafíos de refactorización
La buena noticia: la capa de abstracción (Consumidor, Productor, Publicador, Suscriptor) proporciona una encapsulación limpia de la mayoría de las interacciones de Pulsar.
Los desafíos:
1. **Ubicuidad del sistema de esquemas:** Cada definición de mensaje utiliza `pulsar.schema.Record` y los tipos de campo de Pulsar
2. **Enums específicos de Pulsar:** `InitialPosition`, `ConsumerType`
3. **Excepciones de Pulsar:** `_pulsar.Timeout`, `_pulsar.Interrupted`, `_pulsar.InvalidConfiguration`, `_pulsar.AlreadyClosed`
4. **Fichas de método:** `acknowledge()`, `negative_acknowledge()`, `subscribe()`, `create_producer()`, etc.
5. **Formato de URI de tema:** Estructura de Pulsar `kind://tenant/namespace/topic`
### Próximos pasos
Para hacer que la infraestructura de pub/sub sea configurable, necesitamos:
1. Crear una interfaz de abstracción para el sistema de cliente/esquemas
2. Abstractar enums y excepciones específicas de Pulsar
3. Crear envoltorios de esquema o definiciones de esquema alternativas
4. Implementar la interfaz tanto para Pulsar como para sistemas alternativos (Kafka, RabbitMQ, Redis Streams, etc.)
5. Actualizar `pubsub.py` para que sea configurable y admita varios backends
6. Proporcionar una ruta de migración para implementaciones existentes
## Borrador de enfoque 1: Patrón de adaptador con capa de traducción de esquemas
### Idea clave
El **sistema de esquemas** es el punto de integración más profundo; todo lo demás se deriva de él. Necesitamos resolver esto primero, o tendremos que reescribir todo el código base.
### Estrategia: Interrupción mínima con adaptadores
**1. Mantener los esquemas de Pulsar como la representación interna**
No reescribir todas las definiciones de esquemas.
Los esquemas permanecen `pulsar.schema.Record` internamente.
Utilizar adaptadores para traducir en la frontera entre nuestro código y el backend de publicación/suscripción.
**2. Crear una capa de abstracción de publicación/suscripción:**
```
┌─────────────────────────────────────┐
│ Existing Code (unchanged) │
│ - Uses Pulsar schemas internally │
│ - Consumer/Producer/Publisher │
└──────────────┬──────────────────────┘
┌──────────────┴──────────────────────┐
│ PubSubFactory (configurable) │
│ - Creates backend-specific client │
└──────────────┬──────────────────────┘
┌──────┴──────┐
│ │
┌───────▼─────┐ ┌────▼─────────┐
│ PulsarAdapter│ │ KafkaAdapter │ etc...
│ (passthrough)│ │ (translates) │
└──────────────┘ └──────────────┘
```
**3. Defina interfaces abstractas:**
`PubSubClient` - conexión del cliente
`PubSubProducer` - envío de mensajes
`PubSubConsumer` - recepción de mensajes
`SchemaAdapter` - traducción de esquemas de Pulsar a/desde JSON o formatos específicos del backend
**4. Detalles de implementación:**
Para el **adaptador de Pulsar**: Casi una transmisión directa, traducción mínima.
Para **otros backends** (Kafka, RabbitMQ, etc.):
Serializar objetos de registro de Pulsar a JSON/bytes.
Mapear conceptos como:
`InitialPosition.Earliest/Latest` → auto.offset.reset de Kafka
`acknowledge()` → confirmación de Kafka
`negative_acknowledge()` → patrón de re-cola o cola de mensajes no entregados (DLQ).
URIs de temas → nombres de temas específicos del backend.
### Análisis
**Ventajas:**
✅ Cambios mínimos en el código de los servicios existentes.
✅ Los esquemas permanecen sin cambios (sin reescritura masiva).
✅ Ruta de migración gradual.
✅ Los usuarios de Pulsar no notan ninguna diferencia.
✅ Se agregan nuevos backends a través de adaptadores.
**Desventajas:**
⚠️ Aún mantiene la dependencia de Pulsar (para las definiciones de esquemas).
⚠️ Algunos problemas de compatibilidad al traducir conceptos.
### Consideración alternativa
Crear un sistema de esquemas **TrustGraph** que sea independiente de pub/sub (usando dataclasses o Pydantic), y luego generar esquemas de Pulsar/Kafka/etc a partir de él. Esto requiere reescribir cada archivo de esquema y podría provocar cambios importantes.
### Recomendación para la versión preliminar 1
Comience con el **enfoque de adaptador** porque:
1. Es práctico: funciona con el código existente.
2. Demuestra el concepto con un riesgo mínimo.
3. Puede evolucionar hacia un sistema de esquemas nativo más adelante, si es necesario.
4. Impulsado por la configuración: una variable de entorno cambia entre backends.
## Enfoque de la versión preliminar 2: Sistema de esquemas independiente del backend con dataclasses
### Concepto central
Utilice **dataclasses** de Python como el formato de definición de esquema neutral. Cada backend de pub/sub proporciona su propia serialización/deserialización para dataclasses, eliminando la necesidad de que los esquemas de Pulsar permanezcan en el código base.
### Polimorfismo de esquema a nivel de fábrica
En lugar de traducir esquemas de Pulsar, **cada backend proporciona su propia gestión de esquemas** que funciona con dataclasses de Python estándar.
### Flujo del publicador
```python
# 1. Get the configured backend from factory
pubsub = get_pubsub() # Returns PulsarBackend, MQTTBackend, etc.
# 2. Get schema class from the backend
# (Can be imported directly - backend-agnostic)
from trustgraph.schema.services.llm import TextCompletionRequest
# 3. Create a producer/publisher for a specific topic
producer = pubsub.create_producer(
topic="text-completion-requests",
schema=TextCompletionRequest # Tells backend what schema to use
)
# 4. Create message instances (same API regardless of backend)
request = TextCompletionRequest(
system="You are helpful",
prompt="Hello world",
streaming=False
)
# 5. Send the message
producer.send(request) # Backend serializes appropriately
```
### Flujo del consumidor
```python
# 1. Get the configured backend
pubsub = get_pubsub()
# 2. Create a consumer
consumer = pubsub.subscribe(
topic="text-completion-requests",
schema=TextCompletionRequest # Tells backend how to deserialize
)
# 3. Receive and deserialize
msg = consumer.receive()
request = msg.value() # Returns TextCompletionRequest dataclass instance
# 4. Use the data (type-safe access)
print(request.system) # "You are helpful"
print(request.prompt) # "Hello world"
print(request.streaming) # False
```
### ¿Qué sucede detrás de escena?
**Para el backend de Pulsar:**
`create_producer()` → crea un productor de Pulsar con un esquema JSON o un registro generado dinámicamente.
`send(request)` → serializa la clase de datos a formato JSON/Pulsar y lo envía a Pulsar.
`receive()` → recibe un mensaje de Pulsar, lo deserializa de nuevo a la clase de datos.
**Para el backend de MQTT:**
`create_producer()` → se conecta a un broker de MQTT, no es necesario registrar ningún esquema.
`send(request)` → convierte la clase de datos a JSON y lo publica en un tema de MQTT.
`receive()` → se suscribe a un tema de MQTT y deserializa el JSON a la clase de datos.
**Para el backend de Kafka:**
`create_producer()` → crea un productor de Kafka y registra el esquema Avro si es necesario.
`send(request)` → serializa la clase de datos a formato Avro y lo envía a Kafka.
`receive()` → recibe un mensaje de Kafka y lo deserializa de Avro de nuevo a la clase de datos.
### Puntos Clave del Diseño
1. **Creación del objeto de esquema**: La instancia de la clase de datos (`TextCompletionRequest(...)`) es idéntica independientemente del backend.
2. **El backend se encarga de la codificación**: Cada backend sabe cómo serializar su clase de datos al formato de cable.
3. **Definición del esquema en la creación**: Al crear el productor/consumidor, se especifica el tipo de esquema.
4. **Se mantiene la seguridad de tipos**: Se obtiene un objeto `TextCompletionRequest` adecuado, no un diccionario.
5. **Sin filtración del backend**: El código de la aplicación nunca importa bibliotecas específicas del backend.
### Ejemplo de Transformación
**Actual (específico de Pulsar):**
```python
# schema/services/llm.py
from pulsar.schema import Record, String, Boolean, Integer
class TextCompletionRequest(Record):
system = String()
prompt = String()
streaming = Boolean()
```
**Nuevo (Independiente del backend):**
```python
# schema/services/llm.py
from dataclasses import dataclass
@dataclass
class TextCompletionRequest:
system: str
prompt: str
streaming: bool = False
```
### Integración con el Backend
Cada backend se encarga de la serialización/deserialización de dataclasses:
**Backend de Pulsar:**
Genera clases `pulsar.schema.Record` dinámicamente a partir de dataclasses
O serializa dataclasses a JSON y utiliza el esquema JSON de Pulsar
Mantiene la compatibilidad con implementaciones de Pulsar existentes
**Backend de MQTT/Redis:**
Serialización directa de instancias de dataclass a JSON
Utiliza `dataclasses.asdict()` / `from_dict()`
Ligero, no se necesita un registro de esquemas
**Backend de Kafka:**
Genera esquemas Avro a partir de definiciones de dataclass
Utiliza el registro de esquemas de Confluent
Serialización con seguridad de tipos y soporte para la evolución del esquema
### Arquitectura
```
┌─────────────────────────────────────┐
│ Application Code │
│ - Uses dataclass schemas │
│ - Backend-agnostic │
└──────────────┬──────────────────────┘
┌──────────────┴──────────────────────┐
│ PubSubFactory (configurable) │
│ - get_pubsub() returns backend │
└──────────────┬──────────────────────┘
┌──────┴──────┐
│ │
┌───────▼─────────┐ ┌────▼──────────────┐
│ PulsarBackend │ │ MQTTBackend │
│ - JSON schema │ │ - JSON serialize │
│ - or dynamic │ │ - Simple queues │
│ Record gen │ │ │
└─────────────────┘ └───────────────────┘
```
### Detalles de implementación
**1. Definiciones de esquema:** Clases de datos simples con sugerencias de tipo
`str`, `int`, `bool`, `float` para tipos primitivos
`list[T]` para arreglos
`dict[str, T]` para mapas
Clases de datos anidadas para tipos complejos
**2. Cada backend proporciona:**
Serializador: `dataclass → bytes/wire format`
Deserializador: `bytes/wire format → dataclass`
Registro de esquema (si es necesario, como Pulsar/Kafka)
**3. Abstracción de consumidor/productor:**
Ya existe (consumer.py, producer.py)
Actualizar para usar la serialización del backend
Eliminar importaciones directas de Pulsar
**4. Mapeos de tipo:**
Pulsar `String()` → Python `str`
Pulsar `Integer()` → Python `int`
Pulsar `Boolean()` → Python `bool`
Pulsar `Array(T)` → Python `list[T]`
Pulsar `Map(K, V)` → Python `dict[K, V]`
Pulsar `Double()` → Python `float`
Pulsar `Bytes()` → Python `bytes`
### Ruta de migración
1. **Crear versiones de clases de datos** de todos los esquemas en `trustgraph/schema/`
2. **Actualizar clases de backend** (Consumidor, Productor, Publicador, Suscriptor) para usar la serialización proporcionada por el backend
3. **Implementar PulsarBackend** con esquema JSON o generación dinámica de registros
4. **Probar con Pulsar** para garantizar la compatibilidad hacia atrás con las implementaciones existentes
5. **Agregar nuevos backends** (MQTT, Kafka, Redis, etc.) según sea necesario
6. **Eliminar importaciones de Pulsar** de los archivos de esquema
### Beneficios
**Sin dependencia de pub/sub** en las definiciones de esquema
**Python estándar** - fácil de entender, tipificar, documentar
**Herramientas modernas** - funciona con mypy, autocompletado de IDE, analizadores
**Optimizada para el backend** - cada backend utiliza la serialización nativa
**Sin sobrecarga de traducción** - serialización directa, sin adaptadores
**Seguridad de tipos** - objetos reales con tipos adecuados
**Validación fácil** - se puede usar Pydantic si es necesario
### Desafíos y soluciones
**Desafío:** El `Record` de Pulsar tiene validación de campo en tiempo de ejecución
**Solución:** Usar clases de datos de Pydantic para la validación si es necesario, o características de clase de datos de Python 3.10+ con `__post_init__`
**Desafío:** Algunas características específicas de Pulsar (como el tipo `Bytes`)
**Solución:** Mapear al tipo `bytes` en la clase de datos, el backend se encarga de la codificación apropiadamente
**Desafío:** Nombres de temas (`persistent://tenant/namespace/topic`)
**Solución:** Abstracto los nombres de los temas en las definiciones de esquema, el backend convierte al formato adecuado
**Desafío:** Evolución y versionado del esquema
**Solución:** Cada backend maneja esto de acuerdo con sus capacidades (versiones de esquema de Pulsar, registro de esquema de Kafka, etc.)
**Desafío:** Tipos complejos anidados
**Solución:** Usar clases de datos anidadas, los backends serializan/deserializan recursivamente
### Decisiones de diseño
1. **¿Clases de datos simples o Pydantic?**
**Decisión: Usar clases de datos de Python simples**
Más simple, sin dependencias adicionales
La validación no es necesaria en la práctica
Más fácil de entender y mantener
2. **Evolución del esquema:**
**Decisión: No se necesita un mecanismo de versionado**
Los esquemas son estables y duraderos
Las actualizaciones normalmente agregan nuevos campos (compatible con versiones anteriores)
Los backends manejan la evolución del esquema según sus capacidades
3. **Compatibilidad hacia atrás:**
**Decisión: Cambio de versión importante, no se requiere compatibilidad hacia atrás**
Será un cambio importante con instrucciones de migración
La ruptura limpia permite un mejor diseño
Se proporcionará una guía de migración para las implementaciones existentes
4. **Tipos anidados y estructuras complejas:**
**Decisión: Usar clases de datos anidadas de forma natural**
Las clases de datos de Python manejan el anidamiento perfectamente
`list[T]` para arreglos, `dict[K, V]` para mapas
Los backends serializan/deserializan recursivamente
Ejemplo:
```python
@dataclass
class Value:
value: str
is_uri: bool
@dataclass
class Triple:
s: Value # Nested dataclass
p: Value
o: Value
@dataclass
class GraphQuery:
triples: list[Triple] # Array of nested dataclasses
metadata: dict[str, str]
```
5. **Valores predeterminados y campos opcionales:**
✅ **Decisión: Combinación de campos obligatorios, valores predeterminados y campos opcionales**
Campos obligatorios: Sin valor predeterminado
Campos con valores predeterminados: Siempre presentes, tienen un valor predeterminado razonable
Campos verdaderamente opcionales: `T | None = None`, omitidos de la serialización cuando `None`
Ejemplo:
```python
@dataclass
class TextCompletionRequest:
system: str # Required, no default
prompt: str # Required, no default
streaming: bool = False # Optional with default value
metadata: dict | None = None # Truly optional, can be absent
```
**Semántica de serialización importante:**
Cuando `metadata = None`:
```json
{
"system": "...",
"prompt": "...",
"streaming": false
// metadata field NOT PRESENT
}
```
Cuando `metadata = {}` (explícitamente vacío):
```json
{
"system": "...",
"prompt": "...",
"streaming": false,
"metadata": {} // Field PRESENT but empty
}
```
**Diferencia clave:**
`None` → campo ausente en JSON (no se serializa)
Valor vacío (`{}`, `[]`, `""`) → campo presente con valor vacío
Esto es importante semánticamente: "no proporcionado" vs "explícitamente vacío"
Los sistemas de serialización deben omitir los campos `None`, no codificarlos como `null`
## Esquema de Implementación Borrador 3: Detalles de Implementación
### Formato Genérico para Nombres de Colas
Reemplace los nombres de colas específicos del sistema de respaldo con un formato genérico que los sistemas de respaldo puedan mapear adecuadamente.
**Formato:** `{qos}/{tenant}/{namespace}/{queue-name}`
Donde:
`qos`: Nivel de Calidad de Servicio
`q0` = mejor esfuerzo (enviar y olvidar, sin confirmación)
`q1` = al menos una vez (requiere confirmación)
`q2` = exactamente una vez (confirmación de dos fases)
`tenant`: Agrupación lógica para multi-inquilino
`namespace`: Sub-agrupación dentro del inquilino
`queue-name`: Nombre real de la cola/tema
**Ejemplos:**
```
q1/tg/flow/text-completion-requests
q2/tg/config/config-push
q0/tg/metrics/stats
```
### Mapeo de temas del backend
Cada backend mapea el formato genérico a su formato nativo:
**Backend de Pulsar:**
```python
def map_topic(self, generic_topic: str) -> str:
# Parse: q1/tg/flow/text-completion-requests
qos, tenant, namespace, queue = generic_topic.split('/', 3)
# Map QoS to persistence
persistence = 'persistent' if qos in ['q1', 'q2'] else 'non-persistent'
# Return Pulsar URI: persistent://tg/flow/text-completion-requests
return f"{persistence}://{tenant}/{namespace}/{queue}"
```
**Backend de MQTT:**
```python
def map_topic(self, generic_topic: str) -> tuple[str, int]:
# Parse: q1/tg/flow/text-completion-requests
qos, tenant, namespace, queue = generic_topic.split('/', 3)
# Map QoS level
qos_level = {'q0': 0, 'q1': 1, 'q2': 2}[qos]
# Build MQTT topic including tenant/namespace for proper namespacing
mqtt_topic = f"{tenant}/{namespace}/{queue}"
return mqtt_topic, qos_level
```
### Función de ayuda de tema actualizada
```python
# schema/core/topic.py
def topic(queue_name, qos='q1', tenant='tg', namespace='flow'):
"""
Create a generic topic identifier that can be mapped by backends.
Args:
queue_name: The queue/topic name
qos: Quality of service
- 'q0' = best-effort (no ack)
- 'q1' = at-least-once (ack required)
- 'q2' = exactly-once (two-phase ack)
tenant: Tenant identifier for multi-tenancy
namespace: Namespace within tenant
Returns:
Generic topic string: qos/tenant/namespace/queue_name
Examples:
topic('my-queue') # q1/tg/flow/my-queue
topic('config', qos='q2', namespace='config') # q2/tg/config/config
"""
return f"{qos}/{tenant}/{namespace}/{queue_name}"
```
### Configuración e Inicialización
**Argumentos de Línea de Comandos + Variables de Entorno:**
```python
# In base/async_processor.py - add_args() method
@staticmethod
def add_args(parser):
# Pub/sub backend selection
parser.add_argument(
'--pubsub-backend',
default=os.getenv('PUBSUB_BACKEND', 'pulsar'),
choices=['pulsar', 'mqtt'],
help='Pub/sub backend (default: pulsar, env: PUBSUB_BACKEND)'
)
# Pulsar-specific configuration
parser.add_argument(
'--pulsar-host',
default=os.getenv('PULSAR_HOST', 'pulsar://localhost:6650'),
help='Pulsar host (default: pulsar://localhost:6650, env: PULSAR_HOST)'
)
parser.add_argument(
'--pulsar-api-key',
default=os.getenv('PULSAR_API_KEY', None),
help='Pulsar API key (env: PULSAR_API_KEY)'
)
parser.add_argument(
'--pulsar-listener',
default=os.getenv('PULSAR_LISTENER', None),
help='Pulsar listener name (env: PULSAR_LISTENER)'
)
# MQTT-specific configuration
parser.add_argument(
'--mqtt-host',
default=os.getenv('MQTT_HOST', 'localhost'),
help='MQTT broker host (default: localhost, env: MQTT_HOST)'
)
parser.add_argument(
'--mqtt-port',
type=int,
default=int(os.getenv('MQTT_PORT', '1883')),
help='MQTT broker port (default: 1883, env: MQTT_PORT)'
)
parser.add_argument(
'--mqtt-username',
default=os.getenv('MQTT_USERNAME', None),
help='MQTT username (env: MQTT_USERNAME)'
)
parser.add_argument(
'--mqtt-password',
default=os.getenv('MQTT_PASSWORD', None),
help='MQTT password (env: MQTT_PASSWORD)'
)
```
**Función de Fábrica:**
```python
# In base/pubsub.py or base/pubsub_factory.py
def get_pubsub(**config) -> PubSubBackend:
"""
Create and return a pub/sub backend based on configuration.
Args:
config: Configuration dict from command-line args
Must include 'pubsub_backend' key
Returns:
Backend instance (PulsarBackend, MQTTBackend, etc.)
"""
backend_type = config.get('pubsub_backend', 'pulsar')
if backend_type == 'pulsar':
return PulsarBackend(
host=config.get('pulsar_host'),
api_key=config.get('pulsar_api_key'),
listener=config.get('pulsar_listener'),
)
elif backend_type == 'mqtt':
return MQTTBackend(
host=config.get('mqtt_host'),
port=config.get('mqtt_port'),
username=config.get('mqtt_username'),
password=config.get('mqtt_password'),
)
else:
raise ValueError(f"Unknown pub/sub backend: {backend_type}")
```
**Uso en AsyncProcessor:**
```python
# In async_processor.py
class AsyncProcessor:
def __init__(self, **params):
self.id = params.get("id")
# Create backend from config (replaces PulsarClient)
self.pubsub = get_pubsub(**params)
# Rest of initialization...
```
### Interfaz de Backend
```python
class PubSubBackend(Protocol):
"""Protocol defining the interface all pub/sub backends must implement."""
def create_producer(self, topic: str, schema: type, **options) -> BackendProducer:
"""
Create a producer for a topic.
Args:
topic: Generic topic format (qos/tenant/namespace/queue)
schema: Dataclass type for messages
options: Backend-specific options (e.g., chunking_enabled)
Returns:
Backend-specific producer instance
"""
...
def create_consumer(
self,
topic: str,
subscription: str,
schema: type,
initial_position: str = 'latest',
consumer_type: str = 'shared',
**options
) -> BackendConsumer:
"""
Create a consumer for a topic.
Args:
topic: Generic topic format (qos/tenant/namespace/queue)
subscription: Subscription/consumer group name
schema: Dataclass type for messages
initial_position: 'earliest' or 'latest' (MQTT may ignore)
consumer_type: 'shared', 'exclusive', 'failover' (MQTT may ignore)
options: Backend-specific options
Returns:
Backend-specific consumer instance
"""
...
def close(self) -> None:
"""Close the backend connection."""
...
```
```python
class BackendProducer(Protocol):
"""Protocol for backend-specific producer."""
def send(self, message: Any, properties: dict = {}) -> None:
"""Send a message (dataclass instance) with optional properties."""
...
def flush(self) -> None:
"""Flush any buffered messages."""
...
def close(self) -> None:
"""Close the producer."""
...
```
```python
class BackendConsumer(Protocol):
"""Protocol for backend-specific consumer."""
def receive(self, timeout_millis: int = 2000) -> Message:
"""
Receive a message from the topic.
Raises:
TimeoutError: If no message received within timeout
"""
...
def acknowledge(self, message: Message) -> None:
"""Acknowledge successful processing of a message."""
...
def negative_acknowledge(self, message: Message) -> None:
"""Negative acknowledge - triggers redelivery."""
...
def unsubscribe(self) -> None:
"""Unsubscribe from the topic."""
...
def close(self) -> None:
"""Close the consumer."""
...
```
```python
class Message(Protocol):
"""Protocol for a received message."""
def value(self) -> Any:
"""Get the deserialized message (dataclass instance)."""
...
def properties(self) -> dict:
"""Get message properties/metadata."""
...
```
### Refactorización de Clases Existentes
Las clases existentes `Consumer`, `Producer`, `Publisher`, `Subscriber` permanecen en gran medida intactas:
**Responsabilidades actuales (mantener):**
Modelo de subprocesos asíncronos y grupos de tareas
Lógica de reconexión y manejo de reintentos
Recopilación de métricas
Limitación de velocidad
Gestión de la concurrencia
**Cambios necesarios:**
Eliminar importaciones directas de Pulsar (`pulsar.schema`, `pulsar.InitialPosition`, etc.)
Aceptar `BackendProducer`/`BackendConsumer` en lugar del cliente de Pulsar
Delegar las operaciones de publicación/suscripción reales a instancias de backend
Mapear conceptos genéricos a llamadas de backend
**Ejemplo de refactorización:**
```python
# OLD - consumer.py
class Consumer:
def __init__(self, client, topic, subscriber, schema, ...):
self.client = client # Direct Pulsar client
# ...
async def consumer_run(self):
# Uses pulsar.InitialPosition, pulsar.ConsumerType
self.consumer = self.client.subscribe(
topic=self.topic,
schema=JsonSchema(self.schema),
initial_position=pulsar.InitialPosition.Earliest,
consumer_type=pulsar.ConsumerType.Shared,
)
# NEW - consumer.py
class Consumer:
def __init__(self, backend_consumer, schema, ...):
self.backend_consumer = backend_consumer # Backend-specific consumer
self.schema = schema
# ...
async def consumer_run(self):
# Backend consumer already created with right settings
# Just use it directly
while self.running:
msg = await asyncio.to_thread(
self.backend_consumer.receive,
timeout_millis=2000
)
await self.handle_message(msg)
```
### Comportamientos Específicos del Backend
**Backend de Pulsar:**
Mapea `q0` → `non-persistent://`, `q1`/`q2` → `persistent://`
Soporta todos los tipos de consumidores (compartido, exclusivo, de respaldo)
Soporta la posición inicial (earliest/latest)
Reconocimiento nativo de mensajes
Soporte para el registro de esquemas
**Backend de MQTT:**
Mapea `q0`/`q1`/`q2` → Niveles de QoS de MQTT 0/1/2
Incluye el inquilino/espacio de nombres en la ruta del tema para el espaciado de nombres
Genera automáticamente ID de cliente a partir de los nombres de suscripción
Ignora la posición inicial (no hay historial de mensajes en MQTT básico)
Ignora el tipo de consumidor (MQTT utiliza ID de cliente, no grupos de consumidores)
Modelo de publicación/suscripción simple
### Resumen de Decisiones de Diseño
1. ✅ **Nombres de cola genéricos**: Formato `qos/tenant/namespace/queue-name`
2. ✅ **QoS en el ID de la cola**: Determinado por la definición de la cola, no por la configuración
3. ✅ **Reconexión**: Manejada por las clases Consumer/Producer, no por los backends
4. ✅ **Temas de MQTT**: Incluir inquilino/espacio de nombres para un espaciado de nombres adecuado
5. ✅ **Historial de mensajes**: MQTT ignora el parámetro `initial_position` (mejora futura)
6. ✅ **ID de cliente**: El backend de MQTT genera automáticamente a partir del nombre de la suscripción
### Mejoras Futuras
**Historial de mensajes de MQTT:**
Se podría agregar una capa de persistencia opcional (por ejemplo, mensajes retenidos, almacenamiento externo)
Permitiría soportar `initial_position='earliest'`
No es necesario para la implementación inicial