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

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

  1. Instanciación del cliente: Directo: gateway/service.py Abstracto: async_processor.pypubsub.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

# 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

  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):

# 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

  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:

    @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:

    @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:

    {
        "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 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:

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 q0non-persistent://, q1/q2persistent:// 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