mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 08:26:21 +02:00
966 lines
47 KiB
Markdown
966 lines
47 KiB
Markdown
|
|
---
|
|||
|
|
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'`
|
|||
|
|
Не требуется для первоначальной реализации
|