mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-28 01:46:22 +02:00
Structure the tech specs directory (#836)
Tech spec some subdirectories for different languages
This commit is contained in:
parent
48da6c5f8b
commit
e7efb673ef
423 changed files with 0 additions and 0 deletions
965
docs/tech-specs/es/pubsub.es.md
Normal file
965
docs/tech-specs/es/pubsub.es.md
Normal file
|
|
@ -0,0 +1,965 @@
|
|||
---
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue