trustgraph/docs/tech-specs/pubsub.pt.md
Alex Jenkins 8954fa3ad7 Feat: TrustGraph i18n & Documentation Translation Updates (#781)
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.
2026-04-14 12:08:32 +01:00

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

  1. Instanciação do cliente: Direto: gateway/service.py Abstrato: async_processor.pypubsub.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

# 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

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

# 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

  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:

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

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

    {
        "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 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:

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 q0non-persistent://, q1/q2persistent:// 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