mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 08:26:21 +02:00
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.
965 lines
35 KiB
Markdown
965 lines
35 KiB
Markdown
---
|
|
layout: default
|
|
title: "Infraestrutura Pub/Sub"
|
|
parent: "Portuguese (Beta)"
|
|
---
|
|
|
|
# Infraestrutura 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.
|
|
|
|
## Visão Geral
|
|
|
|
Este documento cataloga todas as conexões entre o código-fonte do TrustGraph e a infraestrutura pub/sub. Atualmente, o sistema está codificado para usar o Apache Pulsar. Esta análise identifica todos os pontos de integração para informar futuras refatorações em direção a uma abstração pub/sub configurável.
|
|
|
|
## Estado Atual: Pontos de Integração do Pulsar
|
|
|
|
### 1. Uso Direto do Cliente Pulsar
|
|
|
|
**Localização:** `trustgraph-flow/trustgraph/gateway/service.py`
|
|
|
|
O gateway da API importa e instancia diretamente o cliente Pulsar:
|
|
|
|
**Linha 20:** `import pulsar`
|
|
**Linhas 54-61:** Instanciação direta de `pulsar.Client()` com `pulsar.AuthenticationToken()` opcional
|
|
**Linhas 33-35:** Configuração padrão do host Pulsar a partir de variáveis de ambiente
|
|
**Linhas 178-192:** Argumentos da linha de comando para `--pulsar-host`, `--pulsar-api-key` e `--pulsar-listener`
|
|
**Linhas 78, 124:** Passa `pulsar_client` para `ConfigReceiver` e `DispatcherManager`
|
|
|
|
Esta é a única localização que instancia diretamente um cliente Pulsar fora da camada de abstração.
|
|
|
|
### 2. Framework Base do Processador
|
|
|
|
**Localização:** `trustgraph-base/trustgraph/base/async_processor.py`
|
|
|
|
A classe base para todos os processadores fornece conectividade Pulsar:
|
|
|
|
**Linha 9:** `import _pulsar` (para tratamento de exceções)
|
|
**Linha 18:** `from . pubsub import PulsarClient`
|
|
**Linha 38:** Cria `pulsar_client_object = PulsarClient(**params)`
|
|
**Linhas 104-108:** Propriedades que expõem `pulsar_host` e `pulsar_client`
|
|
**Linha 250:** O método estático `add_args()` chama `PulsarClient.add_args(parser)` para argumentos da linha de comando
|
|
**Linhas 223-225:** Tratamento de exceções para `_pulsar.Interrupted`
|
|
|
|
Todos os processadores herdam de `AsyncProcessor`, tornando este o ponto de integração central.
|
|
|
|
### 3. Abstração do Consumidor
|
|
|
|
**Localização:** `trustgraph-base/trustgraph/base/consumer.py`
|
|
|
|
Consome mensagens de filas e invoca funções de tratamento:
|
|
|
|
**Importações do Pulsar:**
|
|
**Linha 12:** `from pulsar.schema import JsonSchema`
|
|
**Linha 13:** `import pulsar`
|
|
**Linha 14:** `import _pulsar`
|
|
|
|
**Uso específico do Pulsar:**
|
|
**Linhas 100, 102:** `pulsar.InitialPosition.Earliest` / `pulsar.InitialPosition.Latest`
|
|
**Linha 108:** Wrapper `JsonSchema(self.schema)`
|
|
**Linha 110:** `pulsar.ConsumerType.Shared`
|
|
**Linhas 104-111:** `self.client.subscribe()` com parâmetros específicos do Pulsar
|
|
**Linhas 143, 150, 65:** Métodos `consumer.unsubscribe()` e `consumer.close()`
|
|
**Linha 162:** Exceção `_pulsar.Timeout`
|
|
**Linhas 182, 205, 232:** `consumer.acknowledge()` / `consumer.negative_acknowledge()`
|
|
|
|
**Arquivo de especificação:** `trustgraph-base/trustgraph/base/consumer_spec.py`
|
|
**Linha 22:** Referencia `processor.pulsar_client`
|
|
|
|
### 4. Abstração do Produtor
|
|
|
|
**Localização:** `trustgraph-base/trustgraph/base/producer.py`
|
|
|
|
Envia mensagens para filas:
|
|
|
|
**Importações do Pulsar:**
|
|
**Linha 2:** `from pulsar.schema import JsonSchema`
|
|
|
|
**Uso específico do Pulsar:**
|
|
**Linha 49:** Wrapper `JsonSchema(self.schema)`
|
|
**Linhas 47-51:** `self.client.create_producer()` com parâmetros específicos do Pulsar (tópico, esquema, chunking_enabled)
|
|
**Linhas 31, 76:** Método `producer.close()`
|
|
**Linhas 64-65:** `producer.send()` com mensagem e propriedades
|
|
|
|
**Arquivo de especificação:** `trustgraph-base/trustgraph/base/producer_spec.py`
|
|
**Linha 18:** Referencia `processor.pulsar_client`
|
|
|
|
### 5. Abstração do Publicador
|
|
|
|
**Localização:** `trustgraph-base/trustgraph/base/publisher.py`
|
|
|
|
Publicação de mensagens assíncrona com buffer de fila:
|
|
|
|
**Importações do Pulsar:**
|
|
**Linha 2:** `from pulsar.schema import JsonSchema`
|
|
**Linha 6:** `import pulsar`
|
|
|
|
**Uso específico do Pulsar:**
|
|
**Linha 52:** Wrapper `JsonSchema(self.schema)`
|
|
**Linhas 50-54:** `self.client.create_producer()` com parâmetros específicos do Pulsar
|
|
**Linhas 101, 103:** `producer.send()` com mensagem e propriedades opcionais
|
|
**Linhas 106-107:** Métodos `producer.flush()` e `producer.close()`
|
|
|
|
### 6. Abstração do Assinante
|
|
|
|
**Localização:** `trustgraph-base/trustgraph/base/subscriber.py`
|
|
|
|
Fornece distribuição de mensagens para múltiplos destinatários a partir de filas:
|
|
|
|
**Importações do Pulsar:**
|
|
**Linha 6:** `from pulsar.schema import JsonSchema`
|
|
**Linha 8:** `import _pulsar`
|
|
|
|
**Uso específico do Pulsar:**
|
|
**Linha 55:** `JsonSchema(self.schema)` wrapper
|
|
**Linha 57:** `self.client.subscribe(**subscribe_args)`
|
|
**Linhas 101, 136, 160, 167-172:** Exceções do Pulsar: `_pulsar.Timeout`, `_pulsar.InvalidConfiguration`, `_pulsar.AlreadyClosed`
|
|
**Linhas 159, 166, 170:** Métodos do consumidor: `negative_acknowledge()`, `unsubscribe()`, `close()`
|
|
**Linhas 247, 251:** Reconhecimento de mensagens: `acknowledge()`, `negative_acknowledge()`
|
|
|
|
**Arquivo de especificação:** `trustgraph-base/trustgraph/base/subscriber_spec.py`
|
|
**Linha 19:** Referencia `processor.pulsar_client`
|
|
|
|
### 7. Sistema de Schema (Heart of Darkness)
|
|
|
|
**Localização:** `trustgraph-base/trustgraph/schema/`
|
|
|
|
Cada schema de mensagem no sistema é definido usando o framework de schema do Pulsar.
|
|
|
|
**Primitivos principais:** `schema/core/primitives.py`
|
|
**Linha 2:** `from pulsar.schema import Record, String, Boolean, Array, Integer`
|
|
Todos os schemas herdam da classe base do Pulsar `Record`
|
|
Todos os tipos de campo são tipos do Pulsar: `String()`, `Integer()`, `Boolean()`, `Array()`, `Map()`, `Double()`
|
|
|
|
**Schemas de exemplo:**
|
|
`schema/services/llm.py` (Linha 2): `from pulsar.schema import Record, String, Array, Double, Integer, Boolean`
|
|
`schema/services/config.py` (Linha 2): `from pulsar.schema import Record, Bytes, String, Boolean, Array, Map, Integer`
|
|
|
|
**Nomeação de tópicos:** `schema/core/topic.py`
|
|
**Linhas 2-3:** Formato do tópico: `{kind}://{tenant}/{namespace}/{topic}`
|
|
Esta estrutura de URI é específica do Pulsar (por exemplo, `persistent://tg/flow/config`)
|
|
|
|
**Impacto:**
|
|
Todas as definições de mensagens de solicitação/resposta em todo o código-fonte usam schemas do Pulsar
|
|
Isso inclui serviços para: config, flow, llm, prompt, query, storage, agent, collection, diagnosis, library, lookup, nlp_query, objects_query, retrieval, structured_query
|
|
As definições de schema são importadas e usadas extensivamente em todos os processadores e serviços
|
|
|
|
## Resumo
|
|
|
|
### Dependências do Pulsar por Categoria
|
|
|
|
1. **Instanciação do cliente:**
|
|
Direto: `gateway/service.py`
|
|
Abstrato: `async_processor.py` → `pubsub.py` (PulsarClient)
|
|
|
|
2. **Transporte de mensagens:**
|
|
Consumidor: `consumer.py`, `consumer_spec.py`
|
|
Produtor: `producer.py`, `producer_spec.py`
|
|
Publicador: `publisher.py`
|
|
Assinante: `subscriber.py`, `subscriber_spec.py`
|
|
|
|
3. **Sistema de schema:**
|
|
Tipos base: `schema/core/primitives.py`
|
|
Todos os schemas de serviço: `schema/services/*.py`
|
|
Nomeação de tópicos: `schema/core/topic.py`
|
|
|
|
4. **Conceitos específicos do Pulsar necessários:**
|
|
Mensagens baseadas em tópicos
|
|
Sistema de schema (Registro, tipos de campo)
|
|
Assinaturas compartilhadas
|
|
Reconhecimento de mensagens (positivo/negativo)
|
|
Posicionamento do consumidor (mais cedo/mais tarde)
|
|
Propriedades da mensagem
|
|
Posições iniciais e tipos de consumidor
|
|
Suporte de fragmentação
|
|
Tópicos persistentes vs não persistentes
|
|
|
|
### Desafios de Refatoração
|
|
|
|
A boa notícia: A camada de abstração (Consumidor, Produtor, Publicador, Assinante) fornece um encapsulamento limpo da maioria das interações do Pulsar.
|
|
|
|
Os desafios:
|
|
1. **Ubiquidade do sistema de schema:** Cada definição de mensagem usa `pulsar.schema.Record` e tipos de campo do Pulsar
|
|
2. **Enums específicos do Pulsar:** `InitialPosition`, `ConsumerType`
|
|
3. **Exceções do Pulsar:** `_pulsar.Timeout`, `_pulsar.Interrupted`, `_pulsar.InvalidConfiguration`, `_pulsar.AlreadyClosed`
|
|
4. **Assinaturas de método:** `acknowledge()`, `negative_acknowledge()`, `subscribe()`, `create_producer()`, etc.
|
|
5. **Formato de URI do tópico:** Estrutura do Pulsar `kind://tenant/namespace/topic`
|
|
|
|
### Próximos Passos
|
|
|
|
Para tornar a infraestrutura de pub/sub configurável, precisamos:
|
|
|
|
1. Criar uma interface de abstração para o sistema de cliente/schema
|
|
2. Abstrair enums e exceções específicos do Pulsar
|
|
3. Criar wrappers de schema ou definições de schema alternativas
|
|
4. Implementar a interface para sistemas Pulsar e sistemas alternativos (Kafka, RabbitMQ, Redis Streams, etc.)
|
|
5. Atualizar `pubsub.py` para ser configurável e suportar vários backends
|
|
6. Fornecer um caminho de migração para implantações existentes
|
|
|
|
## Abordagem Preliminar 1: Padrão Adapter com Camada de Tradução de Schema
|
|
|
|
### Principio Fundamental
|
|
O **sistema de schema** é o ponto de integração mais profundo - tudo o mais deriva dele. Precisamos resolver isso primeiro, ou teremos que reescrever todo o código-fonte.
|
|
|
|
### Estratégia: Encapsulamento Mínimo com Adapters
|
|
|
|
**1. Manter os esquemas Pulsar como a representação interna**
|
|
Não reescrever todas as definições de esquema
|
|
Os esquemas permanecem `pulsar.schema.Record` internamente
|
|
Usar adaptadores para traduzir na fronteira entre nosso código e o backend de publicação/assinatura
|
|
|
|
**2. Criar uma camada de abstração de publicação/assinatura:**
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ Existing Code (unchanged) │
|
|
│ - Uses Pulsar schemas internally │
|
|
│ - Consumer/Producer/Publisher │
|
|
└──────────────┬──────────────────────┘
|
|
│
|
|
┌──────────────┴──────────────────────┐
|
|
│ PubSubFactory (configurable) │
|
|
│ - Creates backend-specific client │
|
|
└──────────────┬──────────────────────┘
|
|
│
|
|
┌──────┴──────┐
|
|
│ │
|
|
┌───────▼─────┐ ┌────▼─────────┐
|
|
│ PulsarAdapter│ │ KafkaAdapter │ etc...
|
|
│ (passthrough)│ │ (translates) │
|
|
└──────────────┘ └──────────────┘
|
|
```
|
|
|
|
**3. Defina interfaces abstratas:**
|
|
`PubSubClient` - conexão do cliente
|
|
`PubSubProducer` - envio de mensagens
|
|
`PubSubConsumer` - recebimento de mensagens
|
|
`SchemaAdapter` - tradução de esquemas Pulsar para/de JSON ou formatos específicos do backend
|
|
|
|
**4. Detalhes de implementação:**
|
|
|
|
Para o **adaptador Pulsar**: Quase transparente, tradução mínima
|
|
|
|
Para **outros backends** (Kafka, RabbitMQ, etc.):
|
|
Serializa objetos de registro Pulsar para JSON/bytes
|
|
Mapeia conceitos como:
|
|
`InitialPosition.Earliest/Latest` → auto.offset.reset do Kafka
|
|
`acknowledge()` → commit do Kafka
|
|
`negative_acknowledge()` → padrão de re-fila ou DLQ
|
|
URIs de tópicos → nomes de tópicos específicos do backend
|
|
|
|
### Análise
|
|
|
|
**Prós:**
|
|
✅ Alterações mínimas no código dos serviços existentes
|
|
✅ Os esquemas permanecem como estão (sem reescrita massiva)
|
|
✅ Caminho de migração gradual
|
|
✅ Os usuários do Pulsar não percebem diferença
|
|
✅ Novos backends adicionados via adaptadores
|
|
|
|
**Contras:**
|
|
⚠️ Ainda possui dependência do Pulsar (para definições de esquema)
|
|
⚠️ Alguma incompatibilidade na tradução de conceitos
|
|
|
|
### Consideração Alternativa
|
|
|
|
Crie um sistema de esquemas **TrustGraph** que seja agnóstico de pub/sub (usando dataclasses ou Pydantic), e então gere esquemas Pulsar/Kafka/etc a partir dele. Isso requer reescrever todos os arquivos de esquema e pode causar alterações disruptivas.
|
|
|
|
### Recomendação para a Versão 1
|
|
|
|
Comece com a **abordagem de adaptador** porque:
|
|
1. É pragmática - funciona com o código existente
|
|
2. Demonstra o conceito com risco mínimo
|
|
3. Pode evoluir para um sistema de esquemas nativo posteriormente, se necessário
|
|
4. Impulsionado por configuração: uma variável de ambiente alterna entre backends
|
|
|
|
## Abordagem da Versão 2: Sistema de Esquemas Agnostic de Backend com Dataclasses
|
|
|
|
### Conceito Central
|
|
|
|
Use **dataclasses** do Python como o formato de definição de esquema neutro. Cada backend de pub/sub fornece sua própria serialização/desserialização para dataclasses, eliminando a necessidade de que os esquemas Pulsar permaneçam no código-fonte.
|
|
|
|
### Polimorfismo de Esquema no Nível da Fábrica
|
|
|
|
Em vez de traduzir esquemas Pulsar, **cada backend fornece seu próprio tratamento de esquema** que funciona com dataclasses Python padrão.
|
|
|
|
### Fluxo do 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
|
|
```
|
|
|
|
### Fluxo do 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
|
|
```
|
|
|
|
### O que acontece nos bastidores
|
|
|
|
**Para o backend Pulsar:**
|
|
`create_producer()` → cria um produtor Pulsar com esquema JSON ou um registro gerado dinamicamente.
|
|
`send(request)` → serializa a classe de dados para o formato JSON/Pulsar e envia para o Pulsar.
|
|
`receive()` → recebe a mensagem do Pulsar, desserializa de volta para a classe de dados.
|
|
|
|
**Para o backend MQTT:**
|
|
`create_producer()` → conecta a um broker MQTT, não é necessário registro de esquema.
|
|
`send(request)` → converte a classe de dados para JSON e publica em um tópico MQTT.
|
|
`receive()` → assina um tópico MQTT e desserializa o JSON para a classe de dados.
|
|
|
|
**Para o backend Kafka:**
|
|
`create_producer()` → cria um produtor Kafka e registra o esquema Avro, se necessário.
|
|
`send(request)` → serializa a classe de dados para o formato Avro e envia para o Kafka.
|
|
`receive()` → recebe a mensagem do Kafka e desserializa o Avro de volta para a classe de dados.
|
|
|
|
### Pontos-chave do design
|
|
|
|
1. **Criação do objeto de esquema**: A instância da classe de dados (`TextCompletionRequest(...)`) é idêntica, independentemente do backend.
|
|
2. **O backend lida com a codificação**: Cada backend sabe como serializar sua classe de dados para o formato de transmissão.
|
|
3. **Definição do esquema na criação**: Ao criar o produtor/consumidor, você especifica o tipo de esquema.
|
|
4. **Segurança de tipo preservada**: Você recebe um objeto `TextCompletionRequest` adequado, não um dicionário.
|
|
5. **Nenhum vazamento do backend**: O código da aplicação nunca importa bibliotecas específicas do backend.
|
|
|
|
### Exemplo de transformação
|
|
|
|
**Atual (específico para Pulsar):**
|
|
```python
|
|
# schema/services/llm.py
|
|
from pulsar.schema import Record, String, Boolean, Integer
|
|
|
|
class TextCompletionRequest(Record):
|
|
system = String()
|
|
prompt = String()
|
|
streaming = Boolean()
|
|
```
|
|
|
|
**Novo (Independente do backend):**
|
|
```python
|
|
# schema/services/llm.py
|
|
from dataclasses import dataclass
|
|
|
|
@dataclass
|
|
class TextCompletionRequest:
|
|
system: str
|
|
prompt: str
|
|
streaming: bool = False
|
|
```
|
|
|
|
### Integração com o Backend
|
|
|
|
Cada backend lida com a serialização/desserialização de dataclasses:
|
|
|
|
**Backend Pulsar:**
|
|
Gera classes `pulsar.schema.Record` dinamicamente a partir de dataclasses
|
|
Ou serializa dataclasses para JSON e usa o esquema JSON do Pulsar
|
|
Mantém a compatibilidade com implantações Pulsar existentes
|
|
|
|
**Backend MQTT/Redis:**
|
|
Serialização direta de instâncias de dataclass para JSON
|
|
Use `dataclasses.asdict()` / `from_dict()`
|
|
Leve, não requer registro de esquema
|
|
|
|
**Backend Kafka:**
|
|
Gera esquemas Avro a partir de definições de dataclass
|
|
Use o registro de esquema da Confluent
|
|
Serialização com segurança de tipo com suporte à evolução do esquema
|
|
|
|
### Arquitetura
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ 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 │ │ │
|
|
└─────────────────┘ └───────────────────┘
|
|
```
|
|
|
|
### Detalhes de Implementação
|
|
|
|
**1. Definições de esquema:** Dataclasses simples com dicas de tipo
|
|
`str`, `int`, `bool`, `float` para tipos primitivos
|
|
`list[T]` para arrays
|
|
`dict[str, T]` para mapas
|
|
Dataclasses aninhados para tipos complexos
|
|
|
|
**2. Cada backend fornece:**
|
|
Serializador: `dataclass → bytes/wire format`
|
|
Deserializador: `bytes/wire format → dataclass`
|
|
Registro de esquema (se necessário, como Pulsar/Kafka)
|
|
|
|
**3. Abstração de consumidor/produtor:**
|
|
Já existe (consumer.py, producer.py)
|
|
Atualizar para usar a serialização do backend
|
|
Remover importações diretas do Pulsar
|
|
|
|
**4. Mapeamentos 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`
|
|
|
|
### Caminho de Migração
|
|
|
|
1. **Criar versões de dataclass** de todos os esquemas em `trustgraph/schema/`
|
|
2. **Atualizar classes de backend** (Consumer, Producer, Publisher, Subscriber) para usar a serialização fornecida pelo backend
|
|
3. **Implementar PulsarBackend** com esquema JSON ou geração dinâmica de Record
|
|
4. **Testar com Pulsar** para garantir a compatibilidade com versões anteriores com implantações existentes
|
|
5. **Adicionar novos backends** (MQTT, Kafka, Redis, etc.) conforme necessário
|
|
6. **Remover importações do Pulsar** de arquivos de esquema
|
|
|
|
### Benefícios
|
|
|
|
✅ **Nenhuma dependência de pub/sub** nas definições de esquema
|
|
✅ **Python padrão** - fácil de entender, tipar, documentar
|
|
✅ **Ferramentas modernas** - funciona com mypy, preenchimento automático de IDE, linters
|
|
✅ **Otimizado para backend** - cada backend usa a serialização nativa
|
|
✅ **Sem sobrecarga de tradução** - serialização direta, sem adaptadores
|
|
✅ **Segurança de tipo** - objetos reais com tipos adequados
|
|
✅ **Validação fácil** - pode usar Pydantic, se necessário
|
|
|
|
### Desafios e Soluções
|
|
|
|
**Desafio:** O `Record` do Pulsar tem validação de campo em tempo de execução
|
|
**Solução:** Use dataclasses Pydantic para validação, se necessário, ou recursos de dataclass Python 3.10+ com `__post_init__`
|
|
|
|
**Desafio:** Alguns recursos específicos do Pulsar (como o tipo `Bytes`)
|
|
**Solução:** Mapear para o tipo `bytes` na dataclass, o backend lida com a codificação apropriadamente
|
|
|
|
**Desafio:** Nomenclatura de tópicos (`persistent://tenant/namespace/topic`)
|
|
**Solução:** Abstrair nomes de tópicos em definições de esquema, o backend converte para o formato adequado
|
|
|
|
**Desafio:** Evolução e versionamento de esquema
|
|
**Solução:** Cada backend lida com isso de acordo com suas capacidades (versões de esquema do Pulsar, registro de esquema do Kafka, etc.)
|
|
|
|
**Desafio:** Tipos complexos aninhados
|
|
**Solução:** Use dataclasses aninhadas, os backends serializam/desserializam recursivamente
|
|
|
|
### Decisões de Design
|
|
|
|
1. **Dataclasses simples ou Pydantic?**
|
|
✅ **Decisão: Usar dataclasses Python simples**
|
|
Mais simples, sem dependências adicionais
|
|
Validação não é necessária na prática
|
|
Mais fácil de entender e manter
|
|
|
|
2. **Evolução de esquema:**
|
|
✅ **Decisão: Nenhum mecanismo de versionamento necessário**
|
|
Os esquemas são estáveis e duradouros
|
|
As atualizações normalmente adicionam novos campos (compatíveis com versões anteriores)
|
|
Os backends lidam com a evolução do esquema de acordo com suas capacidades
|
|
|
|
3. **Compatibilidade com versões anteriores:**
|
|
✅ **Decisão: Alteração de versão principal, compatibilidade com versões anteriores não é necessária**
|
|
Será uma alteração disruptiva com instruções de migração
|
|
A separação limpa permite um melhor design
|
|
Um guia de migração será fornecido para implantações existentes
|
|
|
|
4. **Tipos aninhados e estruturas complexas:**
|
|
✅ **Decisão: Usar dataclasses aninhadas naturalmente**
|
|
Dataclasses Python lidam com o aninhamento perfeitamente
|
|
`list[T]` para arrays, `dict[K, V]` para mapas
|
|
Backends serializam/desserializam recursivamente
|
|
Exemplo:
|
|
```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 padrão e campos opcionais:**
|
|
✅ **Decisão: Mistura de campos obrigatórios, valores padrão e campos opcionais**
|
|
Campos obrigatórios: Sem valor padrão
|
|
Campos com valores padrão: Sempre presentes, possuem um valor padrão razoável
|
|
Campos verdadeiramente opcionais: `T | None = None`, omitidos da serialização quando `None`
|
|
Exemplo:
|
|
```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 serialização importante:**
|
|
|
|
Quando `metadata = None`:
|
|
```json
|
|
{
|
|
"system": "...",
|
|
"prompt": "...",
|
|
"streaming": false
|
|
// metadata field NOT PRESENT
|
|
}
|
|
```
|
|
|
|
Quando `metadata = {}` (explicitamente vazio):
|
|
```json
|
|
{
|
|
"system": "...",
|
|
"prompt": "...",
|
|
"streaming": false,
|
|
"metadata": {} // Field PRESENT but empty
|
|
}
|
|
```
|
|
|
|
**Diferença chave:**
|
|
`None` → campo ausente do JSON (não serializado)
|
|
Valor vazio (`{}`, `[]`, `""`) → campo presente com valor vazio
|
|
Isso importa semanticamente: "não fornecido" vs "explicitamente vazio"
|
|
Os backends de serialização devem ignorar os campos `None`, não codificá-los como `null`
|
|
|
|
## Abordagem Rascunho 3: Detalhes de Implementação
|
|
|
|
### Formato Genérico de Nomes de Filas
|
|
|
|
Substitua os nomes de filas específicos do backend por um formato genérico que os backends possam mapear adequadamente.
|
|
|
|
**Formato:** `{qos}/{tenant}/{namespace}/{queue-name}`
|
|
|
|
Onde:
|
|
`qos`: Nível de Qualidade de Serviço
|
|
`q0` = melhor esforço (enviar e esquecer, sem confirmação)
|
|
`q1` = pelo menos uma vez (requer confirmação)
|
|
`q2` = exatamente uma vez (confirmação de duas fases)
|
|
`tenant`: Agrupamento lógico para multi-inquilinato
|
|
`namespace`: Sub-agrupamento dentro do inquilino
|
|
`queue-name`: Nome real da fila/tópico
|
|
|
|
**Exemplos:**
|
|
```
|
|
q1/tg/flow/text-completion-requests
|
|
q2/tg/config/config-push
|
|
q0/tg/metrics/stats
|
|
```
|
|
|
|
### Mapeamento de Tópicos do Backend
|
|
|
|
Cada backend mapeia o formato genérico para o seu formato nativo:
|
|
|
|
**Backend do 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 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
|
|
```
|
|
|
|
### Função de Auxílio de Tópico Atualizada
|
|
|
|
```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}"
|
|
```
|
|
|
|
### Configuração e Inicialização
|
|
|
|
**Argumentos de Linha de Comando + Variáveis de Ambiente:**
|
|
|
|
```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)'
|
|
)
|
|
```
|
|
|
|
**Função 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 em 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...
|
|
```
|
|
|
|
### Interface 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."""
|
|
...
|
|
```
|
|
|
|
### Refatoração das Classes Existentes
|
|
|
|
As classes existentes `Consumer`, `Producer`, `Publisher`, `Subscriber` permanecem em grande parte inalteradas:
|
|
|
|
**Responsabilidades atuais (manter):**
|
|
Modelo de threading assíncrono e grupos de tarefas
|
|
Lógica de reconexão e tratamento de repetições
|
|
Coleta de métricas
|
|
Limitação de taxa
|
|
Gerenciamento de concorrência
|
|
|
|
**Alterações necessárias:**
|
|
Remover importações diretas do Pulsar (`pulsar.schema`, `pulsar.InitialPosition`, etc.)
|
|
Aceitar `BackendProducer`/`BackendConsumer` em vez do cliente Pulsar
|
|
Delegar as operações reais de publicação/assinatura para instâncias de backend
|
|
Mapear conceitos genéricos para chamadas de backend
|
|
|
|
**Exemplo de refatoração:**
|
|
|
|
```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)
|
|
```
|
|
|
|
### Comportamentos Específicos do Backend
|
|
|
|
**Backend Pulsar:**
|
|
Mapeia `q0` → `non-persistent://`, `q1`/`q2` → `persistent://`
|
|
Suporta todos os tipos de consumidores (compartilhado, exclusivo, failover)
|
|
Suporta posição inicial (earliest/latest)
|
|
Reconhecimento nativo de mensagens
|
|
Suporte para registro de esquema
|
|
|
|
**Backend MQTT:**
|
|
Mapeia `q0`/`q1`/`q2` → Níveis de QoS MQTT 0/1/2
|
|
Inclui tenant/namespace no caminho do tópico para fins de namespace
|
|
Gera automaticamente IDs de cliente a partir de nomes de inscrição
|
|
Ignora a posição inicial (sem histórico de mensagens no MQTT básico)
|
|
Ignora o tipo de consumidor (o MQTT usa IDs de cliente, não grupos de consumidores)
|
|
Modelo de publicação/assinatura simples
|
|
|
|
### Resumo das Decisões de Design
|
|
|
|
1. ✅ **Nomeação genérica de filas**: Formato `qos/tenant/namespace/queue-name`
|
|
2. ✅ **QoS no ID da fila**: Determinado pela definição da fila, não pela configuração
|
|
3. ✅ **Reconexão**: Tratada pelas classes Consumer/Producer, não pelos backends
|
|
4. ✅ **Tópicos MQTT**: Incluem tenant/namespace para namespace adequado
|
|
5. ✅ **Histórico de mensagens**: O MQTT ignora o parâmetro `initial_position` (melhoria futura)
|
|
6. ✅ **IDs de cliente**: O backend MQTT gera automaticamente a partir do nome da inscrição
|
|
|
|
### Melhorias Futuras
|
|
|
|
**Histórico de mensagens MQTT:**
|
|
Poderia adicionar uma camada de persistência opcional (por exemplo, mensagens retidas, armazenamento externo)
|
|
Permitiria suportar `initial_position='earliest'`
|
|
Não é necessário para a implementação inicial
|