trustgraph/docs/tech-specs/pubsub.zh-cn.md
Alex Jenkins 8954fa3ad7 Feat: TrustGraph i18n & Documentation Translation Updates (#781)
Native CLI i18n: The TrustGraph CLI has built-in translation support
that dynamically loads language strings. You can test and use
different languages by simply passing the --lang flag (e.g., --lang
es for Spanish, --lang ru for Russian) or by configuring your
environment's LANG variable.

Automated Docs Translations: This PR introduces autonomously
translated Markdown documentation into several target languages,
including Spanish, Swahili, Portuguese, Turkish, Hindi, Hebrew,
Arabic, Simplified Chinese, and Russian.
2026-04-14 12:08:32 +01:00

965 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
layout: default
title: "Pub/Sub 基础设施"
parent: "Chinese (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 行:** CLI 参数,用于 `--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)` 以获取 CLI 参数
**第 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()`
**Spec 文件:** `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 特定的参数 (topic, schema, chunking_enabled)
**第 31、76 行:** `producer.close()` 方法
**第 64-65 行:** `producer.send()`,带有消息和属性
**Spec 文件:** `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)` 包装器
**第 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()`
**Spec 文件:** `trustgraph-base/trustgraph/base/subscriber_spec.py`
**第 19 行:** 引用 `processor.pulsar_client`
### 7. Schema 系统 (黑暗之心)
**位置:** `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` 结构
### 下一步
为了使 pub/sub 基础设施可配置,我们需要:
1. 为客户端/模式系统创建抽象接口
2. 抽象 Pulsar 特定的枚举和异常
3. 创建模式包装器或替代模式定义
4. 为 Pulsar 和其他系统Kafka、RabbitMQ、Redis Streams 等)实现该接口
5. 更新 `pubsub.py` 以使其可配置并支持多个后端
6. 提供现有部署的迁移路径
## 方法草案 1带模式转换层的适配器模式
### 关键洞察
**模式系统** 是最深入的集成点 - 其他一切都由此而产生。 我们需要首先解决这个问题,否则我们将重写整个代码库。
### 策略:通过适配器实现最小的干扰
**1. 保持 Pulsar 模式作为内部表示**
不要重写所有的模式定义
模式在内部仍然是 `pulsar.schema.Record`
使用适配器在我们的代码和发布/订阅后端之间的边界进行转换
**2. 创建一个发布/订阅抽象层:**
```
┌─────────────────────────────────────┐
│ 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` → Kafka 的 auto.offset.reset
`acknowledge()` → Kafka 的 commit
`negative_acknowledge()` → 重试或 DLQ 模式
Topic 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()` → 创建具有 JSON 模式或动态生成的记录的 Pulsar 生产者。
`send(request)` → 将数据类序列化为 JSON/Pulsar 格式,并发送到 Pulsar。
`receive()` → 获取 Pulsar 消息,将其反序列化回数据类。
**对于 MQTT 后端:**
`create_producer()` → 连接到 MQTT 代理,无需注册模式。
`send(request)` → 将数据类转换为 JSON并发布到 MQTT 主题。
`receive()` → 订阅 MQTT 主题,并将 JSON 反序列化为数据类。
**对于 Kafka 后端:**
`create_producer()` → 创建 Kafka 生产者,如果需要,注册 Avro 模式。
`send(request)` → 将数据类序列化为 Avro 格式,并发送到 Kafka。
`receive()` → 获取 Kafka 消息,并将 Avro 反序列化回数据类。
### 关键设计点
1. **模式对象创建:** 数据类实例 (`TextCompletionRequest(...)`) 无论后端如何都是相同的。
2. **后端处理编码:** 每个后端都知道如何将数据类序列化为线格式。
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
```
### 后端集成
每个后端负责数据类的序列化/反序列化:
**Pulsar 后端:**
从数据类动态生成 `pulsar.schema.Record`
或者将数据类序列化为 JSON并使用 Pulsar 的 JSON 模式
保持与现有 Pulsar 部署的兼容性
**MQTT/Redis 后端:**
直接对数据类实例进行 JSON 序列化
使用 `dataclasses.asdict()` / `from_dict()`
轻量级,无需模式注册表
**Kafka 后端:**
从数据类定义生成 Avro 模式
使用 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 模式或动态 Record 生成
4. **使用 Pulsar 进行测试**,以确保与现有部署的向后兼容性
5. **添加新的后端**MQTT, Kafka, Redis 等),如果需要
6. **从模式文件中移除 Pulsar 导入**
### 优点
**模式定义中没有 pub/sub 依赖**
**标准 Python** - 易于理解、类型检查和记录
**现代工具** - 与 mypy、IDE 自动完成、linter 兼容
**后端优化** - 每个后端使用本机序列化
**没有翻译开销** - 直接序列化,没有适配器
**类型安全** - 具有正确类型的真实对象
**易于验证** - 如果需要,可以使用 Pydantic
### 挑战与解决方案
**挑战:** Pulsar 的 `Record` 具有运行时字段验证
**解决方案:** 如果需要,可以使用 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`:服务质量级别
`q0` = 尽力而为(发送并忘记,无需确认)
`q1` = 至少一次(需要确认)
`q2` = 恰好一次(两阶段确认)
`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://`
支持所有类型的消费者(共享、独占、故障转移)
支持初始位置(最早/最新)
原生消息确认
模式注册支持
**MQTT 后端:**
将 `q0`/`q1`/`q2` 映射到 MQTT QoS 等级 0/1/2
在主题路径中包含租户/命名空间,以进行命名空间划分
根据订阅名称自动生成客户端 ID
忽略初始位置(基本的 MQTT 中没有消息历史)
忽略消费者类型MQTT 使用客户端 ID而不是消费者组
简单的发布/订阅模型
### 设计决策摘要
1. ✅ **通用队列命名:** `qos/tenant/namespace/queue-name` 格式
2. ✅ **队列 ID 中的 QoS** 由队列定义确定,而不是配置
3. ✅ **重连:** 由 Consumer/Producer 类处理,而不是后端
4. ✅ **MQTT 主题:** 包含租户/命名空间以进行正确的命名空间划分
5. ✅ **消息历史:** MQTT 忽略 `initial_position` 参数(未来增强)
6. ✅ **客户端 ID** MQTT 后端根据订阅名称自动生成
### 未来增强
**MQTT 消息历史:**
可以添加可选的持久层(例如,保留的消息、外部存储)
将允许支持 `initial_position='earliest'`
不适用于初始实现