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

966 lines
47 KiB
Markdown
Raw Normal View History

---
layout: default
title: "Инфраструктура Pub/Sub"
parent: "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 по категориям
1. **Инициализация клиента:**
Прямая: `gateway/service.py`
Абстрактная: `async_processor.py``pubsub.py` (PulsarClient)
2. **Транспортировка сообщений:**
Потребитель: `consumer.py`, `consumer_spec.py`
Издатель: `producer.py`, `producer_spec.py`
Публикатор: `publisher.py`
Подписчик: `subscriber.py`, `subscriber_spec.py`
3. **Система схем:**
Базовые типы: `schema/core/primitives.py`
Все схемы сервисов: `schema/services/*.py`
Именование тем: `schema/core/topic.py`
4. **Требуются концепции, специфичные для Pulsar:**
Сообщения на основе тем
Система схем (Record, типы полей)
Общие подписки
Подтверждение сообщений (положительное/отрицательное)
Позиционирование потребителя (самое начало/последнее)
Свойства сообщений
Начальные позиции и типы потребителей
Поддержка разбиения на части
Постоянные и непостоянные темы
### Проблемы рефакторинга
Хорошая новость: слой абстракции (Consumer, Producer, Publisher, Subscriber) обеспечивает четкую инкапсуляцию большинства взаимодействий с Pulsar.
Проблемы:
1. **Всепроникающая система схем:** Каждое определение сообщения использует `pulsar.schema.Record` и типы Pulsar.
2. **Перечисления, специфичные для Pulsar:** `InitialPosition`, `ConsumerType`
3. **Исключения Pulsar:** `_pulsar.Timeout`, `_pulsar.Interrupted`, `_pulsar.InvalidConfiguration`, `_pulsar.AlreadyClosed`
4. **Сигнатуры методов:** `acknowledge()`, `negative_acknowledge()`, `subscribe()`, `create_producer()` и т.д.
5. **Формат URI темы:** Структура Pulsar `kind://tenant/namespace/topic`
### Следующие шаги
Чтобы сделать инфраструктуру публикации/подписки настраиваемой, нам нужно:
1. Создать интерфейс абстракции для системы клиента/схем.
2. Абстрагировать перечисления и исключения, специфичные для Pulsar.
3. Создать обертки для схем или альтернативные определения схем.
4. Реализовать интерфейс как для Pulsar, так и для альтернативных систем (Kafka, RabbitMQ, Redis Streams и т.д.).
5. Обновить `pubsub.py`, чтобы он был настраиваемым и поддерживал несколько бэкендов.
6. Предоставить путь миграции для существующих развертываний.
## Предварительный черновик подхода 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
Начните с **подхода с использованием адаптеров**, потому что:
1. Это практичный подход, который работает с существующим кодом.
2. Подтверждает концепцию с минимальным риском.
3. Может быть усовершенствован до нативной системы схем в будущем, если это необходимо.
4. Управление конфигурацией: одна переменная окружения переключает бэкенды.
## Подход, версия 2: Система схем, независимая от бэкенда, с использованием dataclasses
### Основная концепция
Используйте Python **dataclasses** в качестве нейтрального формата определения схемы. Каждый бэкенд pub/sub предоставляет свой собственный механизм сериализации/десериализации для dataclasses, что устраняет необходимость сохранения схем Pulsar в кодовой базе.
### Полиморфизм схем на уровне фабрики
Вместо преобразования схем Pulsar, **каждый бэкенд предоставляет собственную обработку схем**, которая работает со стандартными Python dataclasses.
### Поток публикации
```python
# 1. Get the configured backend from factory
pubsub = get_pubsub() # Returns PulsarBackend, MQTTBackend, etc.
# 2. Get schema class from the backend
# (Can be imported directly - backend-agnostic)
from trustgraph.schema.services.llm import TextCompletionRequest
# 3. Create a producer/publisher for a specific topic
producer = pubsub.create_producer(
topic="text-completion-requests",
schema=TextCompletionRequest # Tells backend what schema to use
)
# 4. Create message instances (same API regardless of backend)
request = TextCompletionRequest(
system="You are helpful",
prompt="Hello world",
streaming=False
)
# 5. Send the message
producer.send(request) # Backend serializes appropriately
```
### Поток пользователей
```python
# 1. Get the configured backend
pubsub = get_pubsub()
# 2. Create a consumer
consumer = pubsub.subscribe(
topic="text-completion-requests",
schema=TextCompletionRequest # Tells backend how to deserialize
)
# 3. Receive and deserialize
msg = consumer.receive()
request = msg.value() # Returns TextCompletionRequest dataclass instance
# 4. Use the data (type-safe access)
print(request.system) # "You are helpful"
print(request.prompt) # "Hello world"
print(request.streaming) # False
```
### Что происходит за кулисами
**Для бэкенда 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
### Ключевые моменты проектирования
1. **Создание объекта схемы**: Экземпляр dataclass (`TextCompletionRequest(...)`) идентичен независимо от бэкенда
2. **Бэкенд обрабатывает кодирование**: Каждый бэкенд знает, как сериализовать свой dataclass в формат для передачи данных
3. **Определение схемы при создании**: При создании производителя/потребителя вы указываете тип схемы
4. **Сохранение типобезопасности**: Вы получаете правильный объект `TextCompletionRequest`, а не словарь
5. **Отсутствие утечек бэкенда**: Код приложения никогда не импортирует библиотеки, специфичные для бэкенда
### Пример преобразования
**Текущий (специфичный для Pulsar):**
```python
# schema/services/llm.py
from pulsar.schema import Record, String, Boolean, Integer
class TextCompletionRequest(Record):
system = String()
prompt = String()
streaming = Boolean()
```
**Новое (независимое от бэкенда):**
```python
# 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`
### Путь миграции
1. **Создайте версии классов данных** для всех схем в `trustgraph/schema/`
2. **Обновите классы бэкенда** (Consumer, Producer, Publisher, Subscriber) для использования сериализации, предоставляемой бэкендом
3. **Реализуйте PulsarBackend** с использованием JSON-схемы или динамической генерации записей
4. **Протестируйте с Pulsar**, чтобы обеспечить обратную совместимость с существующими развертываниями
5. **Добавьте новые бэкенды** (MQTT, Kafka, Redis и т. д.) по мере необходимости
6. **Удалите импорты Pulsar** из файлов схем
### Преимущества
**Отсутствие зависимости от pub/sub** в определениях схем
**Стандартный Python** - легко понять, проверить типы, документировать
**Современные инструменты** - работает с mypy, автодополнением IDE, линтерами
**Оптимизировано для бэкенда** - каждый бэкенд использует собственную сериализацию
**Отсутствие накладных расходов на преобразование** - прямая сериализация, без адаптеров
**Безопасность типов** - реальные объекты с правильными типами
**Простая проверка** - можно использовать Pydantic, если необходимо
### Проблемы и решения
**Проблема:** У `Record` Pulsar есть проверка полей во время выполнения
**Решение:** Используйте классы данных Pydantic для проверки, если это необходимо, или функции Python 3.10+ для классов данных с `__post_init__`
**Проблема:** Некоторые специфичные для Pulsar функции (например, тип `Bytes`)
**Решение:** Отобразите на тип `bytes` в классе данных, бэкенд обрабатывает кодирование соответствующим образом
**Проблема:** Именование тем (`persistent://tenant/namespace/topic`)
**Решение:** Абстрагируйте имена тем в определениях схем, бэкенд преобразует в правильный формат
**Проблема:** Эволюция и версионирование схем
**Решение:** Каждый бэкенд обрабатывает это в соответствии со своими возможностями (версии схем Pulsar, реестр схем Kafka и т. д.)
**Проблема:** Вложенные сложные типы
**Решение:** Используйте вложенные классы данных, бэкенды рекурсивно сериализуют/десериализуют
### Принятые решения
1. **Обычные классы данных или Pydantic?**
**Решение: Используйте обычные классы данных Python**
Проще, без дополнительных зависимостей
Проверка не требуется на практике
Легче понять и поддерживать
2. **Эволюция схемы:**
**Решение: Механизм версионирования не требуется**
Схемы стабильны и долговечны
Обновления обычно добавляют новые поля (обратная совместимость)
Бэкенды обрабатывают эволюцию схемы в соответствии со своими возможностями
3. **Обратная совместимость:**
**Решение: Изменение основной версии, обратная совместимость не требуется**
Это будет изменение, нарушающее обратную совместимость, с инструкциями по миграции
Чистый разрыв позволяет лучше спроектировать систему
Будет предоставлено руководство по миграции для существующих развертываний
4. **Вложенные типы и сложные структуры:**
**Решение: Используйте вложенные классы данных естественным образом**
Классы данных Python отлично справляются с вложенностью
`list[T]` для массивов, `dict[K, V]` для словарей
Бэкенды рекурсивно сериализуют/десериализуют
Пример:
```python
@dataclass
class Value:
value: str
is_uri: bool
@dataclass
class Triple:
s: Value # Nested dataclass
p: Value
o: Value
@dataclass
class GraphQuery:
triples: list[Triple] # Array of nested dataclasses
metadata: dict[str, str]
```
5. **Значения по умолчанию и необязательные поля:**
**Решение: Комбинация обязательных, полей со значениями по умолчанию и необязательных полей**
Обязательные поля: Не имеют значения по умолчанию.
Поля со значениями по умолчанию: Всегда присутствуют, имеют разумные значения по умолчанию.
Действительно необязательные поля: `T | None = None`, опускаются при сериализации, когда `None`
Пример:
```python
@dataclass
class TextCompletionRequest:
system: str # Required, no default
prompt: str # Required, no default
streaming: bool = False # Optional with default value
metadata: dict | None = None # Truly optional, can be absent
```
**Важные семантические аспекты сериализации:**
Когда `metadata = None`:
```json
{
"system": "...",
"prompt": "...",
"streaming": false
// metadata field NOT PRESENT
}
```
Когда `metadata = {}` (явно пусто):
```json
{
"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:**
```python
def map_topic(self, generic_topic: str) -> str:
# Parse: q1/tg/flow/text-completion-requests
qos, tenant, namespace, queue = generic_topic.split('/', 3)
# Map QoS to persistence
persistence = 'persistent' if qos in ['q1', 'q2'] else 'non-persistent'
# Return Pulsar URI: persistent://tg/flow/text-completion-requests
return f"{persistence}://{tenant}/{namespace}/{queue}"
```
**Бэкэнд MQTT:**
```python
def map_topic(self, generic_topic: str) -> tuple[str, int]:
# Parse: q1/tg/flow/text-completion-requests
qos, tenant, namespace, queue = generic_topic.split('/', 3)
# Map QoS level
qos_level = {'q0': 0, 'q1': 1, 'q2': 2}[qos]
# Build MQTT topic including tenant/namespace for proper namespacing
mqtt_topic = f"{tenant}/{namespace}/{queue}"
return mqtt_topic, qos_level
```
### Обновленная вспомогательная функция для темы
```python
# schema/core/topic.py
def topic(queue_name, qos='q1', tenant='tg', namespace='flow'):
"""
Create a generic topic identifier that can be mapped by backends.
Args:
queue_name: The queue/topic name
qos: Quality of service
- 'q0' = best-effort (no ack)
- 'q1' = at-least-once (ack required)
- 'q2' = exactly-once (two-phase ack)
tenant: Tenant identifier for multi-tenancy
namespace: Namespace within tenant
Returns:
Generic topic string: qos/tenant/namespace/queue_name
Examples:
topic('my-queue') # q1/tg/flow/my-queue
topic('config', qos='q2', namespace='config') # q2/tg/config/config
"""
return f"{qos}/{tenant}/{namespace}/{queue_name}"
```
### Конфигурация и инициализация
**Аргументы командной строки + переменные окружения:**
```python
# In base/async_processor.py - add_args() method
@staticmethod
def add_args(parser):
# Pub/sub backend selection
parser.add_argument(
'--pubsub-backend',
default=os.getenv('PUBSUB_BACKEND', 'pulsar'),
choices=['pulsar', 'mqtt'],
help='Pub/sub backend (default: pulsar, env: PUBSUB_BACKEND)'
)
# Pulsar-specific configuration
parser.add_argument(
'--pulsar-host',
default=os.getenv('PULSAR_HOST', 'pulsar://localhost:6650'),
help='Pulsar host (default: pulsar://localhost:6650, env: PULSAR_HOST)'
)
parser.add_argument(
'--pulsar-api-key',
default=os.getenv('PULSAR_API_KEY', None),
help='Pulsar API key (env: PULSAR_API_KEY)'
)
parser.add_argument(
'--pulsar-listener',
default=os.getenv('PULSAR_LISTENER', None),
help='Pulsar listener name (env: PULSAR_LISTENER)'
)
# MQTT-specific configuration
parser.add_argument(
'--mqtt-host',
default=os.getenv('MQTT_HOST', 'localhost'),
help='MQTT broker host (default: localhost, env: MQTT_HOST)'
)
parser.add_argument(
'--mqtt-port',
type=int,
default=int(os.getenv('MQTT_PORT', '1883')),
help='MQTT broker port (default: 1883, env: MQTT_PORT)'
)
parser.add_argument(
'--mqtt-username',
default=os.getenv('MQTT_USERNAME', None),
help='MQTT username (env: MQTT_USERNAME)'
)
parser.add_argument(
'--mqtt-password',
default=os.getenv('MQTT_PASSWORD', None),
help='MQTT password (env: MQTT_PASSWORD)'
)
```
**Фабричная функция:**
```python
# In base/pubsub.py or base/pubsub_factory.py
def get_pubsub(**config) -> PubSubBackend:
"""
Create and return a pub/sub backend based on configuration.
Args:
config: Configuration dict from command-line args
Must include 'pubsub_backend' key
Returns:
Backend instance (PulsarBackend, MQTTBackend, etc.)
"""
backend_type = config.get('pubsub_backend', 'pulsar')
if backend_type == 'pulsar':
return PulsarBackend(
host=config.get('pulsar_host'),
api_key=config.get('pulsar_api_key'),
listener=config.get('pulsar_listener'),
)
elif backend_type == 'mqtt':
return MQTTBackend(
host=config.get('mqtt_host'),
port=config.get('mqtt_port'),
username=config.get('mqtt_username'),
password=config.get('mqtt_password'),
)
else:
raise ValueError(f"Unknown pub/sub backend: {backend_type}")
```
**Использование в AsyncProcessor:**
```python
# In async_processor.py
class AsyncProcessor:
def __init__(self, **params):
self.id = params.get("id")
# Create backend from config (replaces PulsarClient)
self.pubsub = get_pubsub(**params)
# Rest of initialization...
```
### Интерфейс бэкенда
```python
class PubSubBackend(Protocol):
"""Protocol defining the interface all pub/sub backends must implement."""
def create_producer(self, topic: str, schema: type, **options) -> BackendProducer:
"""
Create a producer for a topic.
Args:
topic: Generic topic format (qos/tenant/namespace/queue)
schema: Dataclass type for messages
options: Backend-specific options (e.g., chunking_enabled)
Returns:
Backend-specific producer instance
"""
...
def create_consumer(
self,
topic: str,
subscription: str,
schema: type,
initial_position: str = 'latest',
consumer_type: str = 'shared',
**options
) -> BackendConsumer:
"""
Create a consumer for a topic.
Args:
topic: Generic topic format (qos/tenant/namespace/queue)
subscription: Subscription/consumer group name
schema: Dataclass type for messages
initial_position: 'earliest' or 'latest' (MQTT may ignore)
consumer_type: 'shared', 'exclusive', 'failover' (MQTT may ignore)
options: Backend-specific options
Returns:
Backend-specific consumer instance
"""
...
def close(self) -> None:
"""Close the backend connection."""
...
```
```python
class BackendProducer(Protocol):
"""Protocol for backend-specific producer."""
def send(self, message: Any, properties: dict = {}) -> None:
"""Send a message (dataclass instance) with optional properties."""
...
def flush(self) -> None:
"""Flush any buffered messages."""
...
def close(self) -> None:
"""Close the producer."""
...
```
```python
class BackendConsumer(Protocol):
"""Protocol for backend-specific consumer."""
def receive(self, timeout_millis: int = 2000) -> Message:
"""
Receive a message from the topic.
Raises:
TimeoutError: If no message received within timeout
"""
...
def acknowledge(self, message: Message) -> None:
"""Acknowledge successful processing of a message."""
...
def negative_acknowledge(self, message: Message) -> None:
"""Negative acknowledge - triggers redelivery."""
...
def unsubscribe(self) -> None:
"""Unsubscribe from the topic."""
...
def close(self) -> None:
"""Close the consumer."""
...
```
```python
class Message(Protocol):
"""Protocol for a received message."""
def value(self) -> Any:
"""Get the deserialized message (dataclass instance)."""
...
def properties(self) -> dict:
"""Get message properties/metadata."""
...
```
### Рефакторинг существующих классов
Существующие классы `Consumer`, `Producer`, `Publisher`, `Subscriber` остаются в основном без изменений:
**Текущие обязанности (сохранить):**
Асинхронная модель потоков и группы задач
Логика повторного подключения и обработка повторных попыток
Сбор метрик
Ограничение скорости
Управление параллелизмом
**Необходимые изменения:**
Удалить прямые импорты Pulsar (`pulsar.schema`, `pulsar.InitialPosition` и т.д.)
Принимать `BackendProducer`/`BackendConsumer` вместо клиента Pulsar
Передавать фактические операции публикации/подписки на экземпляры бэкенда
Отображать общие концепции на вызовы бэкенда
**Пример рефакторинга:**
```python
# OLD - consumer.py
class Consumer:
def __init__(self, client, topic, subscriber, schema, ...):
self.client = client # Direct Pulsar client
# ...
async def consumer_run(self):
# Uses pulsar.InitialPosition, pulsar.ConsumerType
self.consumer = self.client.subscribe(
topic=self.topic,
schema=JsonSchema(self.schema),
initial_position=pulsar.InitialPosition.Earliest,
consumer_type=pulsar.ConsumerType.Shared,
)
# NEW - consumer.py
class Consumer:
def __init__(self, backend_consumer, schema, ...):
self.backend_consumer = backend_consumer # Backend-specific consumer
self.schema = schema
# ...
async def consumer_run(self):
# Backend consumer already created with right settings
# Just use it directly
while self.running:
msg = await asyncio.to_thread(
self.backend_consumer.receive,
timeout_millis=2000
)
await self.handle_message(msg)
```
### Специфические для бэкенда особенности
**Бэкенд Pulsar:**
Отображает `q0``non-persistent://`, `q1`/`q2``persistent://`
Поддерживает все типы потребителей (shared, exclusive, failover)
Поддерживает начальную позицию (earliest/latest)
Поддержка подтверждения получения сообщений
Поддержка реестра схем
**Бэкенд MQTT:**
Отображает `q0`/`q1`/`q2` → уровни качества обслуживания (QoS) MQTT 0/1/2
Включает арендатора/пространство имен в путь темы для обеспечения разделения имен
Автоматически генерирует идентификаторы клиентов из имен подписок
Игнорирует начальную позицию (нет истории сообщений в базовом MQTT)
Игнорирует тип потребителя (MQTT использует идентификаторы клиентов, а не группы потребителей)
Простая модель публикации/подписки
### Краткое описание принятых решений
1.**Универсальное именование очередей**: формат `qos/tenant/namespace/queue-name`
2.**QoS в идентификаторе очереди**: определяется определением очереди, а не конфигурацией
3.**Повторное подключение**: обрабатывается классами Consumer/Producer, а не бэкендами
4.**Темы MQTT**: включают арендатора/пространство имен для правильного разделения имен
5.**История сообщений**: MQTT игнорирует параметр `initial_position` (будущее улучшение)
6.**Идентификаторы клиентов**: бэкенд MQTT автоматически генерирует из имени подписки
### Будущие улучшения
**История сообщений MQTT:**
Можно добавить необязательный слой постоянного хранения (например, сохраненные сообщения, внешний хранилище)
Это позволит поддерживать `initial_position='earliest'`
Не требуется для первоначальной реализации