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.
36 KiB
| layout | title | parent |
|---|---|---|
| default | Infraestructura Pub/Sub | 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
-
Instanciación del cliente: Directo:
gateway/service.pyAbstracto:async_processor.py→pubsub.py(PulsarClient) -
Transporte de mensajes: Consumidor:
consumer.py,consumer_spec.pyProductor:producer.py,producer_spec.pyPublicador:publisher.pySuscriptor:subscriber.py,subscriber_spec.py -
Sistema de esquemas: Tipos base:
schema/core/primitives.pyTodos los esquemas de servicio:schema/services/*.pyNomenclatura de temas:schema/core/topic.py -
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:
- Ubicuidad del sistema de esquemas: Cada definición de mensaje utiliza
pulsar.schema.Recordy los tipos de campo de Pulsar - Enums específicos de Pulsar:
InitialPosition,ConsumerType - Excepciones de Pulsar:
_pulsar.Timeout,_pulsar.Interrupted,_pulsar.InvalidConfiguration,_pulsar.AlreadyClosed - Fichas de método:
acknowledge(),negative_acknowledge(),subscribe(),create_producer(), etc. - 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:
- Crear una interfaz de abstracción para el sistema de cliente/esquemas
- Abstractar enums y excepciones específicas de Pulsar
- Crear envoltorios de esquema o definiciones de esquema alternativas
- Implementar la interfaz tanto para Pulsar como para sistemas alternativos (Kafka, RabbitMQ, Redis Streams, etc.)
- Actualizar
pubsub.pypara que sea configurable y admita varios backends - 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:
- Es práctico: funciona con el código existente.
- Demuestra el concepto con un riesgo mínimo.
- Puede evolucionar hacia un sistema de esquemas nativo más adelante, si es necesario.
- 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
# 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
# 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
- Creación del objeto de esquema: La instancia de la clase de datos (
TextCompletionRequest(...)) es idéntica independientemente del backend. - El backend se encarga de la codificación: Cada backend sabe cómo serializar su clase de datos al formato de cable.
- Definición del esquema en la creación: Al crear el productor/consumidor, se especifica el tipo de esquema.
- Se mantiene la seguridad de tipos: Se obtiene un objeto
TextCompletionRequestadecuado, no un diccionario. - 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):
# 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):
# 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
- Crear versiones de clases de datos de todos los esquemas en
trustgraph/schema/ - Actualizar clases de backend (Consumidor, Productor, Publicador, Suscriptor) para usar la serialización proporcionada por el backend
- Implementar PulsarBackend con esquema JSON o generación dinámica de registros
- Probar con Pulsar para garantizar la compatibilidad hacia atrás con las implementaciones existentes
- Agregar nuevos backends (MQTT, Kafka, Redis, etc.) según sea necesario
- 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
-
¿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
-
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
-
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
-
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:@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] -
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 cuandoNoneEjemplo:@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 absentSemántica de serialización importante:
Cuando
metadata = None:{ "system": "...", "prompt": "...", "streaming": false // metadata field NOT PRESENT }Cuando
metadata = {}(explícitamente vacío):{ "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 camposNone, no codificarlos comonull
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:
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:
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
# 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:
# 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:
# 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:
# 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
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."""
...
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."""
...
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."""
...
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:
# 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
- ✅ Nombres de cola genéricos: Formato
qos/tenant/namespace/queue-name - ✅ QoS en el ID de la cola: Determinado por la definición de la cola, no por la configuración
- ✅ Reconexión: Manejada por las clases Consumer/Producer, no por los backends
- ✅ Temas de MQTT: Incluir inquilino/espacio de nombres para un espaciado de nombres adecuado
- ✅ Historial de mensajes: MQTT ignora el parámetro
initial_position(mejora futura) - ✅ 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