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.
35 KiB
| layout | title | parent |
|---|---|---|
| default | Infraestrutura Pub/Sub | 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
-
Instanciação do cliente: Direto:
gateway/service.pyAbstrato:async_processor.py→pubsub.py(PulsarClient) -
Transporte de mensagens: Consumidor:
consumer.py,consumer_spec.pyProdutor:producer.py,producer_spec.pyPublicador:publisher.pyAssinante:subscriber.py,subscriber_spec.py -
Sistema de schema: Tipos base:
schema/core/primitives.pyTodos os schemas de serviço:schema/services/*.pyNomeação de tópicos:schema/core/topic.py -
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:
- Ubiquidade do sistema de schema: Cada definição de mensagem usa
pulsar.schema.Recorde tipos de campo do Pulsar - Enums específicos do Pulsar:
InitialPosition,ConsumerType - Exceções do Pulsar:
_pulsar.Timeout,_pulsar.Interrupted,_pulsar.InvalidConfiguration,_pulsar.AlreadyClosed - Assinaturas de método:
acknowledge(),negative_acknowledge(),subscribe(),create_producer(), etc. - 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:
- Criar uma interface de abstração para o sistema de cliente/schema
- Abstrair enums e exceções específicos do Pulsar
- Criar wrappers de schema ou definições de schema alternativas
- Implementar a interface para sistemas Pulsar e sistemas alternativos (Kafka, RabbitMQ, Redis Streams, etc.)
- Atualizar
pubsub.pypara ser configurável e suportar vários backends - 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:
- É pragmática - funciona com o código existente
- Demonstra o conceito com risco mínimo
- Pode evoluir para um sistema de esquemas nativo posteriormente, se necessário
- 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
# 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
# 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
- Criação do objeto de esquema: A instância da classe de dados (
TextCompletionRequest(...)) é idêntica, independentemente do backend. - O backend lida com a codificação: Cada backend sabe como serializar sua classe de dados para o formato de transmissão.
- Definição do esquema na criação: Ao criar o produtor/consumidor, você especifica o tipo de esquema.
- Segurança de tipo preservada: Você recebe um objeto
TextCompletionRequestadequado, não um dicionário. - 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):
# 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):
# 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
- Criar versões de dataclass de todos os esquemas em
trustgraph/schema/ - Atualizar classes de backend (Consumer, Producer, Publisher, Subscriber) para usar a serialização fornecida pelo backend
- Implementar PulsarBackend com esquema JSON ou geração dinâmica de Record
- Testar com Pulsar para garantir a compatibilidade com versões anteriores com implantações existentes
- Adicionar novos backends (MQTT, Kafka, Redis, etc.) conforme necessário
- 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
-
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
-
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
-
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
-
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:@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] -
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 quandoNoneExemplo:@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 absentSemântica de serialização importante:
Quando
metadata = None:{ "system": "...", "prompt": "...", "streaming": false // metadata field NOT PRESENT }Quando
metadata = {}(explicitamente vazio):{ "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 camposNone, não codificá-los comonull
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:
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:
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
# 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:
# 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:
# 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:
# 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
class PubSubBackend(Protocol):
"""Protocol defining the interface all pub/sub backends must implement."""
def create_producer(self, topic: str, schema: type, **options) -> BackendProducer:
"""
Create a producer for a topic.
Args:
topic: Generic topic format (qos/tenant/namespace/queue)
schema: Dataclass type for messages
options: Backend-specific options (e.g., chunking_enabled)
Returns:
Backend-specific producer instance
"""
...
def create_consumer(
self,
topic: str,
subscription: str,
schema: type,
initial_position: str = 'latest',
consumer_type: str = 'shared',
**options
) -> BackendConsumer:
"""
Create a consumer for a topic.
Args:
topic: Generic topic format (qos/tenant/namespace/queue)
subscription: Subscription/consumer group name
schema: Dataclass type for messages
initial_position: 'earliest' or 'latest' (MQTT may ignore)
consumer_type: 'shared', 'exclusive', 'failover' (MQTT may ignore)
options: Backend-specific options
Returns:
Backend-specific consumer instance
"""
...
def close(self) -> None:
"""Close the backend connection."""
...
class BackendProducer(Protocol):
"""Protocol for backend-specific producer."""
def send(self, message: Any, properties: dict = {}) -> None:
"""Send a message (dataclass instance) with optional properties."""
...
def flush(self) -> None:
"""Flush any buffered messages."""
...
def close(self) -> None:
"""Close the producer."""
...
class BackendConsumer(Protocol):
"""Protocol for backend-specific consumer."""
def receive(self, timeout_millis: int = 2000) -> Message:
"""
Receive a message from the topic.
Raises:
TimeoutError: If no message received within timeout
"""
...
def acknowledge(self, message: Message) -> None:
"""Acknowledge successful processing of a message."""
...
def negative_acknowledge(self, message: Message) -> None:
"""Negative acknowledge - triggers redelivery."""
...
def unsubscribe(self) -> None:
"""Unsubscribe from the topic."""
...
def close(self) -> None:
"""Close the consumer."""
...
class Message(Protocol):
"""Protocol for a received message."""
def value(self) -> Any:
"""Get the deserialized message (dataclass instance)."""
...
def properties(self) -> dict:
"""Get message properties/metadata."""
...
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:
# 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
- ✅ Nomeação genérica de filas: Formato
qos/tenant/namespace/queue-name - ✅ QoS no ID da fila: Determinado pela definição da fila, não pela configuração
- ✅ Reconexão: Tratada pelas classes Consumer/Producer, não pelos backends
- ✅ Tópicos MQTT: Incluem tenant/namespace para namespace adequado
- ✅ Histórico de mensagens: O MQTT ignora o parâmetro
initial_position(melhoria futura) - ✅ 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