mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-27 01:16:22 +02:00
Structure the tech specs directory (#836)
Tech spec some subdirectories for different languages
This commit is contained in:
parent
48da6c5f8b
commit
e7efb673ef
423 changed files with 0 additions and 0 deletions
965
docs/tech-specs/ru/pubsub.ru.md
Normal file
965
docs/tech-specs/ru/pubsub.ru.md
Normal file
|
|
@ -0,0 +1,965 @@
|
|||
---
|
||||
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'`
|
||||
Не требуется для первоначальной реализации
|
||||
Loading…
Add table
Add a link
Reference in a new issue