trustgraph/docs/tech-specs/pubsub.pt.md

966 lines
35 KiB
Markdown
Raw Normal View History

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