mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 08:26:21 +02:00
966 lines
35 KiB
Markdown
966 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
|