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.
47 KiB
| layout | title | parent |
|---|---|---|
| default | Инфраструктура Pub/Sub | Russian (Beta) |
Инфраструктура 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.
Обзор
Этот документ содержит описание всех соединений между кодовой базой TrustGraph и инфраструктурой pub/sub. В настоящее время система жестко запрограммирована на использование Apache Pulsar. Этот анализ определяет все точки интеграции для будущей рефакторизации в сторону конфигурируемой абстракции pub/sub.
Текущее состояние: точки интеграции Pulsar
1. Прямое использование клиента Pulsar
Местоположение: trustgraph-flow/trustgraph/gateway/service.py
API-шлюз напрямую импортирует и создает экземпляр клиента Pulsar:
Строка 20: import pulsar
Строки 54-61: Прямая инициализация pulsar.Client() с необязательными параметрами pulsar.AuthenticationToken()
Строки 33-35: Конфигурация хоста Pulsar по умолчанию из переменных окружения
Строки 178-192: Аргументы командной строки для --pulsar-host, --pulsar-api-key и --pulsar-listener
Строки 78, 124: Передача pulsar_client в ConfigReceiver и DispatcherManager
Это единственное место, где напрямую создается экземпляр клиента Pulsar за пределами слоя абстракции.
2. Базовая платформа процессоров
Местоположение: trustgraph-base/trustgraph/base/async_processor.py
Базовый класс для всех процессоров обеспечивает подключение к Pulsar:
Строка 9: import _pulsar (для обработки исключений)
Строка 18: from . pubsub import PulsarClient
Строка 38: Создание pulsar_client_object = PulsarClient(**params)
Строки 104-108: Свойства, предоставляющие доступ к pulsar_host и pulsar_client
Строка 250: Статический метод add_args() вызывает PulsarClient.add_args(parser) для аргументов командной строки
Строки 223-225: Обработка исключений для _pulsar.Interrupted
Все процессоры наследуются от AsyncProcessor, что делает это центральной точкой интеграции.
3. Абстракция потребителя
Местоположение: trustgraph-base/trustgraph/base/consumer.py
Потребляет сообщения из очередей и вызывает функции обработчиков:
Импорты Pulsar:
Строка 12: from pulsar.schema import JsonSchema
Строка 13: import pulsar
Строка 14: import _pulsar
Использование, специфичное для Pulsar:
Строки 100, 102: pulsar.InitialPosition.Earliest / pulsar.InitialPosition.Latest
Строка 108: Обертка JsonSchema(self.schema)
Строка 110: pulsar.ConsumerType.Shared
Строки 104-111: self.client.subscribe() с параметрами, специфичными для Pulsar
Строки 143, 150, 65: Методы consumer.unsubscribe() и consumer.close()
Строка 162: Исключение _pulsar.Timeout
Строки 182, 205, 232: consumer.acknowledge() / consumer.negative_acknowledge()
Спецификационный файл: trustgraph-base/trustgraph/base/consumer_spec.py
Строка 22: Ссылка на processor.pulsar_client
4. Абстракция производителя
Местоположение: trustgraph-base/trustgraph/base/producer.py
Отправляет сообщения в очереди:
Импорты Pulsar:
Строка 2: from pulsar.schema import JsonSchema
Использование, специфичное для Pulsar:
Строка 49: Обертка JsonSchema(self.schema)
Строки 47-51: self.client.create_producer() с параметрами, специфичными для Pulsar (тема, схема, включение фрагментации)
Строки 31, 76: Метод producer.close()
Строки 64-65: producer.send() с сообщением и свойствами
Спецификационный файл: trustgraph-base/trustgraph/base/producer_spec.py
Строка 18: Ссылка на processor.pulsar_client
5. Абстракция издателя
Местоположение: trustgraph-base/trustgraph/base/publisher.py
Асинхронная публикация сообщений с буферизацией очереди:
Импорты Pulsar:
Строка 2: from pulsar.schema import JsonSchema
Строка 6: import pulsar
Использование, специфичное для Pulsar:
Строка 52: Обертка JsonSchema(self.schema)
Строки 50-54: self.client.create_producer() с параметрами, специфичными для Pulsar
Строки 101, 103: producer.send() с сообщением и необязательными свойствами
Строки 106-107: Методы producer.flush() и producer.close()
6. Абстракция подписчика
Местоположение: trustgraph-base/trustgraph/base/subscriber.py
Предоставляет распределение сообщений для нескольких получателей из очередей:
Импорт Pulsar:
Строка 6: from pulsar.schema import JsonSchema
Строка 8: import _pulsar
Использование, специфичное для Pulsar:
Строка 55: JsonSchema(self.schema) wrapper
Строка 57: self.client.subscribe(**subscribe_args)
Строки 101, 136, 160, 167-172: Исключения Pulsar: _pulsar.Timeout, _pulsar.InvalidConfiguration, _pulsar.AlreadyClosed
Строки 159, 166, 170: Методы потребителя: negative_acknowledge(), unsubscribe(), close()
Строки 247, 251: Подтверждение сообщений: acknowledge(), negative_acknowledge()
Файл спецификации: trustgraph-base/trustgraph/base/subscriber_spec.py
Строка 19: Ссылается на processor.pulsar_client
7. Система схем (Heart of Darkness)
Расположение: trustgraph-base/trustgraph/schema/
Каждая схема сообщения в системе определяется с использованием фреймворка схем Pulsar.
Основные примитивы: schema/core/primitives.py
Строка 2: from pulsar.schema import Record, String, Boolean, Array, Integer
Все схемы наследуются от базового класса Pulsar Record
Все типы полей являются типами Pulsar: String(), Integer(), Boolean(), Array(), Map(), Double()
Примеры схем:
schema/services/llm.py (Строка 2): from pulsar.schema import Record, String, Array, Double, Integer, Boolean
schema/services/config.py (Строка 2): from pulsar.schema import Record, Bytes, String, Boolean, Array, Map, Integer
Именование тем: schema/core/topic.py
Строки 2-3: Формат темы: {kind}://{tenant}/{namespace}/{topic}
Эта структура URI специфична для Pulsar (например, persistent://tg/flow/config)
Влияние: Все определения сообщений запроса/ответа во всем коде используют схемы Pulsar Это включает в себя сервисы для: config, flow, llm, prompt, query, storage, agent, collection, diagnosis, library, lookup, nlp_query, objects_query, retrieval, structured_query Определения схем импортируются и используются во всех процессорах и сервисах.
Краткое описание
Зависимости Pulsar по категориям
-
Инициализация клиента: Прямая:
gateway/service.pyАбстрактная:async_processor.py→pubsub.py(PulsarClient) -
Транспортировка сообщений: Потребитель:
consumer.py,consumer_spec.pyИздатель:producer.py,producer_spec.pyПубликатор:publisher.pyПодписчик:subscriber.py,subscriber_spec.py -
Система схем: Базовые типы:
schema/core/primitives.pyВсе схемы сервисов:schema/services/*.pyИменование тем:schema/core/topic.py -
Требуются концепции, специфичные для Pulsar: Сообщения на основе тем Система схем (Record, типы полей) Общие подписки Подтверждение сообщений (положительное/отрицательное) Позиционирование потребителя (самое начало/последнее) Свойства сообщений Начальные позиции и типы потребителей Поддержка разбиения на части Постоянные и непостоянные темы
Проблемы рефакторинга
Хорошая новость: слой абстракции (Consumer, Producer, Publisher, Subscriber) обеспечивает четкую инкапсуляцию большинства взаимодействий с Pulsar.
Проблемы:
- Всепроникающая система схем: Каждое определение сообщения использует
pulsar.schema.Recordи типы Pulsar. - Перечисления, специфичные для Pulsar:
InitialPosition,ConsumerType - Исключения Pulsar:
_pulsar.Timeout,_pulsar.Interrupted,_pulsar.InvalidConfiguration,_pulsar.AlreadyClosed - Сигнатуры методов:
acknowledge(),negative_acknowledge(),subscribe(),create_producer()и т.д. - Формат URI темы: Структура Pulsar
kind://tenant/namespace/topic
Следующие шаги
Чтобы сделать инфраструктуру публикации/подписки настраиваемой, нам нужно:
- Создать интерфейс абстракции для системы клиента/схем.
- Абстрагировать перечисления и исключения, специфичные для Pulsar.
- Создать обертки для схем или альтернативные определения схем.
- Реализовать интерфейс как для Pulsar, так и для альтернативных систем (Kafka, RabbitMQ, Redis Streams и т.д.).
- Обновить
pubsub.py, чтобы он был настраиваемым и поддерживал несколько бэкендов. - Предоставить путь миграции для существующих развертываний.
Предварительный черновик подхода 1: Шаблон адаптера со слоем перевода схем
Ключевое понимание
Система схем является наиболее глубокой точкой интеграции - от нее зависит все остальное. Мы должны решить эту проблему в первую очередь, иначе нам придется переписывать весь код.
Стратегия: Минимальное нарушение с помощью адаптеров
1. Сохраняйте схемы Pulsar в качестве внутреннего представления
Не переписывайте все определения схем.
Схемы остаются pulsar.schema.Record во внутреннем представлении.
Используйте адаптеры для преобразования данных на границе между нашим кодом и бэкендом pub/sub.
2. Создайте абстрактный слой pub/sub:
┌─────────────────────────────────────┐
│ Existing Code (unchanged) │
│ - Uses Pulsar schemas internally │
│ - Consumer/Producer/Publisher │
└──────────────┬──────────────────────┘
│
┌──────────────┴──────────────────────┐
│ PubSubFactory (configurable) │
│ - Creates backend-specific client │
└──────────────┬──────────────────────┘
│
┌──────┴──────┐
│ │
┌───────▼─────┐ ┌────▼─────────┐
│ PulsarAdapter│ │ KafkaAdapter │ etc...
│ (passthrough)│ │ (translates) │
└──────────────┘ └──────────────┘
3. Определение абстрактных интерфейсов:
PubSubClient - клиентское подключение
PubSubProducer - отправка сообщений
PubSubConsumer - получение сообщений
SchemaAdapter - преобразование схем Pulsar в/из JSON или форматы, специфичные для бэкенда
4. Детали реализации:
Для адаптера Pulsar: Практически без изменений, минимальное преобразование.
Для других бэкендов (Kafka, RabbitMQ и т.д.):
Сериализация объектов Pulsar Record в JSON/байты.
Отображение понятий, таких как:
InitialPosition.Earliest/Latest → auto.offset.reset в Kafka
acknowledge() → commit в Kafka
negative_acknowledge() → Паттерн повторной отправки или DLQ (Dead Letter Queue)
URI тем → Имена тем, специфичные для бэкенда.
Анализ
Преимущества: ✅ Минимальные изменения существующего кода. ✅ Схемы остаются без изменений (без необходимости масштабной переработки). ✅ Постепенный путь миграции. ✅ Пользователи Pulsar не заметят разницы. ✅ Новые бэкенды добавляются через адаптеры.
Недостатки: ⚠️ Все еще присутствует зависимость от Pulsar (для определений схем). ⚠️ Некоторые несовместимости при преобразовании понятий.
Альтернативное решение
Создать систему схем TrustGraph, которая является агностической к pub/sub (используя dataclasses или Pydantic), а затем генерировать схемы Pulsar/Kafka/и т.д. из нее. Это требует переписывания каждого файла схемы и может привести к несовместимым изменениям.
Рекомендации для версии 1
Начните с подхода с использованием адаптеров, потому что:
- Это практичный подход, который работает с существующим кодом.
- Подтверждает концепцию с минимальным риском.
- Может быть усовершенствован до нативной системы схем в будущем, если это необходимо.
- Управление конфигурацией: одна переменная окружения переключает бэкенды.
Подход, версия 2: Система схем, независимая от бэкенда, с использованием dataclasses
Основная концепция
Используйте Python dataclasses в качестве нейтрального формата определения схемы. Каждый бэкенд pub/sub предоставляет свой собственный механизм сериализации/десериализации для dataclasses, что устраняет необходимость сохранения схем Pulsar в кодовой базе.
Полиморфизм схем на уровне фабрики
Вместо преобразования схем Pulsar, каждый бэкенд предоставляет собственную обработку схем, которая работает со стандартными Python dataclasses.
Поток публикации
# 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
Поток пользователей
# 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
Что происходит за кулисами
Для бэкенда Pulsar:
create_producer() → создает производителя Pulsar с JSON-схемой или динамически генерируемой записью
send(request) → сериализует dataclass в формат JSON/Pulsar, отправляет в Pulsar
receive() → получает сообщение Pulsar, десериализует обратно в dataclass
Для бэкенда MQTT:
create_producer() → подключается к MQTT-брокеру, регистрация схемы не требуется
send(request) → преобразует dataclass в JSON, публикует в MQTT-топик
receive() → подписывается на MQTT-топик, десериализует JSON в dataclass
Для бэкенда Kafka:
create_producer() → создает производителя Kafka, регистрирует схему Avro, если необходимо
send(request) → сериализует dataclass в формат Avro, отправляет в Kafka
receive() → получает сообщение Kafka, десериализует Avro обратно в dataclass
Ключевые моменты проектирования
- Создание объекта схемы: Экземпляр dataclass (
TextCompletionRequest(...)) идентичен независимо от бэкенда - Бэкенд обрабатывает кодирование: Каждый бэкенд знает, как сериализовать свой dataclass в формат для передачи данных
- Определение схемы при создании: При создании производителя/потребителя вы указываете тип схемы
- Сохранение типобезопасности: Вы получаете правильный объект
TextCompletionRequest, а не словарь - Отсутствие утечек бэкенда: Код приложения никогда не импортирует библиотеки, специфичные для бэкенда
Пример преобразования
Текущий (специфичный для Pulsar):
# schema/services/llm.py
from pulsar.schema import Record, String, Boolean, Integer
class TextCompletionRequest(Record):
system = String()
prompt = String()
streaming = Boolean()
Новое (независимое от бэкенда):
# schema/services/llm.py
from dataclasses import dataclass
@dataclass
class TextCompletionRequest:
system: str
prompt: str
streaming: bool = False
Интеграция с бэкендом
Каждый бэкенд отвечает за сериализацию/десериализацию dataclasses:
Бэкенд Pulsar:
Динамически генерирует классы pulsar.schema.Record из dataclasses
Или сериализует dataclasses в JSON и использует JSON-схему Pulsar
Обеспечивает совместимость с существующими развертываниями Pulsar
Бэкенд MQTT/Redis:
Прямая сериализация в JSON экземпляров dataclass
Использует dataclasses.asdict() / from_dict()
Легковесный, не требуется реестр схем
Бэкенд Kafka: Генерирует схемы Avro из определений dataclass Использует реестр схем Confluent Типобезопасная сериализация с поддержкой эволюции схемы
Архитектура
┌─────────────────────────────────────┐
│ 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 │ │ │
└─────────────────┘ └───────────────────┘
Детали реализации
1. Определение схем: Простые классы данных с подсказками типов
str, int, bool, float для примитивов
list[T] для массивов
dict[str, T] для словарей
Вложенные классы данных для сложных типов
2. Каждый бэкенд предоставляет:
Сериализатор: dataclass → bytes/wire format
Десериализатор: bytes/wire format → dataclass
Регистрация схемы (если необходимо, например, для Pulsar/Kafka)
3. Абстракция потребителя/производителя: Уже существует (consumer.py, producer.py) Обновление для использования сериализации бэкенда Удаление прямых импортов Pulsar
4. Отображение типов:
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
Путь миграции
- Создайте версии классов данных для всех схем в
trustgraph/schema/ - Обновите классы бэкенда (Consumer, Producer, Publisher, Subscriber) для использования сериализации, предоставляемой бэкендом
- Реализуйте PulsarBackend с использованием JSON-схемы или динамической генерации записей
- Протестируйте с Pulsar, чтобы обеспечить обратную совместимость с существующими развертываниями
- Добавьте новые бэкенды (MQTT, Kafka, Redis и т. д.) по мере необходимости
- Удалите импорты Pulsar из файлов схем
Преимущества
✅ Отсутствие зависимости от pub/sub в определениях схем ✅ Стандартный Python - легко понять, проверить типы, документировать ✅ Современные инструменты - работает с mypy, автодополнением IDE, линтерами ✅ Оптимизировано для бэкенда - каждый бэкенд использует собственную сериализацию ✅ Отсутствие накладных расходов на преобразование - прямая сериализация, без адаптеров ✅ Безопасность типов - реальные объекты с правильными типами ✅ Простая проверка - можно использовать Pydantic, если необходимо
Проблемы и решения
Проблема: У Record Pulsar есть проверка полей во время выполнения
Решение: Используйте классы данных Pydantic для проверки, если это необходимо, или функции Python 3.10+ для классов данных с __post_init__
Проблема: Некоторые специфичные для Pulsar функции (например, тип Bytes)
Решение: Отобразите на тип bytes в классе данных, бэкенд обрабатывает кодирование соответствующим образом
Проблема: Именование тем (persistent://tenant/namespace/topic)
Решение: Абстрагируйте имена тем в определениях схем, бэкенд преобразует в правильный формат
Проблема: Эволюция и версионирование схем Решение: Каждый бэкенд обрабатывает это в соответствии со своими возможностями (версии схем Pulsar, реестр схем Kafka и т. д.)
Проблема: Вложенные сложные типы Решение: Используйте вложенные классы данных, бэкенды рекурсивно сериализуют/десериализуют
Принятые решения
-
Обычные классы данных или Pydantic? ✅ Решение: Используйте обычные классы данных Python Проще, без дополнительных зависимостей Проверка не требуется на практике Легче понять и поддерживать
-
Эволюция схемы: ✅ Решение: Механизм версионирования не требуется Схемы стабильны и долговечны Обновления обычно добавляют новые поля (обратная совместимость) Бэкенды обрабатывают эволюцию схемы в соответствии со своими возможностями
-
Обратная совместимость: ✅ Решение: Изменение основной версии, обратная совместимость не требуется Это будет изменение, нарушающее обратную совместимость, с инструкциями по миграции Чистый разрыв позволяет лучше спроектировать систему Будет предоставлено руководство по миграции для существующих развертываний
-
Вложенные типы и сложные структуры: ✅ Решение: Используйте вложенные классы данных естественным образом Классы данных Python отлично справляются с вложенностью
list[T]для массивов,dict[K, V]для словарей Бэкенды рекурсивно сериализуют/десериализуют Пример:@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] -
Значения по умолчанию и необязательные поля: ✅ Решение: Комбинация обязательных, полей со значениями по умолчанию и необязательных полей Обязательные поля: Не имеют значения по умолчанию. Поля со значениями по умолчанию: Всегда присутствуют, имеют разумные значения по умолчанию. Действительно необязательные поля:
T | None = None, опускаются при сериализации, когдаNoneПример:@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Важные семантические аспекты сериализации:
Когда
metadata = None:{ "system": "...", "prompt": "...", "streaming": false // metadata field NOT PRESENT }Когда
metadata = {}(явно пусто):{ "system": "...", "prompt": "...", "streaming": false, "metadata": {} // Field PRESENT but empty }Ключевое отличие:
None→ поле отсутствует в JSON (не сериализуется) Пустое значение ({},[],"") → поле присутствует со значением "пусто" Это имеет семантическое значение: "не предоставлено" против "явно пусто" Механизмы сериализации должны пропускать поляNone, а не кодировать их какnull
Предварительный вариант 3: Детали реализации
Универсальный формат именования очередей
Замените специфичные для каждого бэкенда имена очередей на универсальный формат, который бэкенды могут сопоставить соответствующим образом.
Формат: {qos}/{tenant}/{namespace}/{queue-name}
Где:
qos: Уровень качества обслуживания (QoS)
q0 = best-effort (отправка без подтверждения)
q1 = at-least-once (требуется подтверждение)
q2 = exactly-once (двухфазное подтверждение)
tenant: Логическая группировка для многопользовательской среды
namespace: Подгруппа внутри арендатора
queue-name: Фактическое имя очереди/топика
Примеры:
q1/tg/flow/text-completion-requests
q2/tg/config/config-push
q0/tg/metrics/stats
Отображение тем для бэкенда
Каждый бэкенд преобразует общий формат в свой собственный формат:
Бэкенд 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}"
Бэкэнд 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
Обновленная вспомогательная функция для темы
# 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}"
Конфигурация и инициализация
Аргументы командной строки + переменные окружения:
# 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)'
)
Фабричная функция:
# 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}")
Использование в 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...
Интерфейс бэкенда
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."""
...
Рефакторинг существующих классов
Существующие классы Consumer, Producer, Publisher, Subscriber остаются в основном без изменений:
Текущие обязанности (сохранить): Асинхронная модель потоков и группы задач Логика повторного подключения и обработка повторных попыток Сбор метрик Ограничение скорости Управление параллелизмом
Необходимые изменения:
Удалить прямые импорты Pulsar (pulsar.schema, pulsar.InitialPosition и т.д.)
Принимать BackendProducer/BackendConsumer вместо клиента Pulsar
Передавать фактические операции публикации/подписки на экземпляры бэкенда
Отображать общие концепции на вызовы бэкенда
Пример рефакторинга:
# 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)
Специфические для бэкенда особенности
Бэкенд Pulsar:
Отображает q0 → non-persistent://, q1/q2 → persistent://
Поддерживает все типы потребителей (shared, exclusive, failover)
Поддерживает начальную позицию (earliest/latest)
Поддержка подтверждения получения сообщений
Поддержка реестра схем
Бэкенд MQTT:
Отображает q0/q1/q2 → уровни качества обслуживания (QoS) MQTT 0/1/2
Включает арендатора/пространство имен в путь темы для обеспечения разделения имен
Автоматически генерирует идентификаторы клиентов из имен подписок
Игнорирует начальную позицию (нет истории сообщений в базовом MQTT)
Игнорирует тип потребителя (MQTT использует идентификаторы клиентов, а не группы потребителей)
Простая модель публикации/подписки
Краткое описание принятых решений
- ✅ Универсальное именование очередей: формат
qos/tenant/namespace/queue-name - ✅ QoS в идентификаторе очереди: определяется определением очереди, а не конфигурацией
- ✅ Повторное подключение: обрабатывается классами Consumer/Producer, а не бэкендами
- ✅ Темы MQTT: включают арендатора/пространство имен для правильного разделения имен
- ✅ История сообщений: MQTT игнорирует параметр
initial_position(будущее улучшение) - ✅ Идентификаторы клиентов: бэкенд MQTT автоматически генерирует из имени подписки
Будущие улучшения
История сообщений MQTT:
Можно добавить необязательный слой постоянного хранения (например, сохраненные сообщения, внешний хранилище)
Это позволит поддерживать initial_position='earliest'
Не требуется для первоначальной реализации