7.OpenClaw源码解析——可靠消息投递

1. 知识回顾

上节课我们主要学习了 OpenClawheartbeatcron 两种异步任务机制,有如下特点:

  • 异步任务在后台自动运行。
  • 当有用户提问时,系统会优先响应用户 ,阻塞 heartbeat等主动任务。
  • cron 任务在后台默默执行,不影响主流程。

本节课我们来解决一个OpenClaw的核心痛点,当发送消息时,可能会因为网络故障进程崩溃 等原因导致回复丢失 而影响体验,所以OpenClaw的做法就是引入可靠消息投递机制,确保消息不会丢失。


2. 总体设计思路

OpenClaw的做法就是消息必须先写到磁盘,然后再尝试发送

  • 消息必须先持久化到本地磁盘,确保掉电/崩溃后依然可恢复。
  • 随后再通过后台线程异步尝试发送,失败则自动重试

实现细节

3.1 消息分片

大模型生成的回复可能过长,需要根据不同平台的消息长度限制进行切分:

  • 如果回复较短,则不分片
  • 如果回复过长,则按平台规则切割为多个片段,依次投递。

3.2. 生成任务对象并持久化入队 💾

具体做法是:

  • 生成唯一的 delivery_id(UUID 截取前 12 位)。
  • 构造 QueuedDelivery 任务对象,包含通道、接收方、文本内容、入队时间、下次重试时间等。
  • 调用 _write_entry() 写入磁盘。

下面的代码演示了原子写入的过程,防止写入过程中断导致文件损坏

python 复制代码
def _write_entry(self, entry: QueuedDelivery) -> None:
    """通过 tmp + os.replace() 保证原子写入,避免数据损坏"""
    final_path = self.queue_dir / f"{entry.id}.json"
    tmp_path = self.queue_dir / f".tmp.{os.getpid()}.{entry.id}.json"

    data = json.dumps(entry.to_dict(), indent=2, ensure_ascii=False)
    with open(tmp_path, "w", encoding="utf-8") as f:
        f.write(data)
        f.flush()
        os.fsync(f.fileno())   # 强制落盘
    os.replace(str(tmp_path), str(final_path))  # 原子替换

通过将消息落入磁盘进行持久化,防止消息丢失。

python 复制代码
delivery_id = uuid.uuid4().hex[:12]
entry = QueuedDelivery(
    id=delivery_id,
    channel=channel,
    to=to,
    text=text,
    enqueued_at=time.time(),
    next_retry_at=0.0,        # 立即重试
)
self._write_entry(entry)      # 落盘后才返回
return delivery_id            # 返回 ID 给上层

3.3 后台线程扫描

系统启动时,后台线程会扫描 pending(待发送)和 failed(失败)队列,确保没有遗留任务。

python 复制代码
pending = self.queue.load_pending()
failed = self.queue.load_failed()

# 打印恢复状态
if pending:
    print_delivery(f"Recovery: {len(pending)} pending")
if failed:
    print_delivery(f"Recovery: {len(failed)} failed")

后台线程进行轮询,遍历pending队列。

python 复制代码
for entry in pending:
    # 1. 检查是否到达下一次重试时间
    if entry.next_retry_at > now:
        continue

    # 2. 尝试投递
    try:
        self.deliver_fn(entry.channel, entry.to, entry.text)
        self.queue.ack(entry.id)           # 投递成功,确认删除
        self.total_succeeded += 1
    except Exception as exc:
        # 3. 投递失败,记录失败并更新重试次数
        self.queue.fail(entry.id, str(exc))
        self.total_failed += 1

        # 4. 判断是否超过最大重试次数(MAX_RETRIES)
        if entry.retry_count + 1 >= MAX_RETRIES:
            print_warn(f"Delivery {entry.id} -> failed (最终失败)")
        else:
            # 进入指数退避等待
            backoff = compute_backoff_ms(entry.retry_count + 1)
            print_warn(f"next retry in {backoff / 1000:.0f}s")

为了防止某个消息一直发送不成功而抢占其他消息的时间,OpenClaw使用了带随机抖动的指数退避算法,具体代码如下:

python 复制代码
# 基础退避时间序列(毫秒)
BACKOFF_MS = [5_000, 25_000, 120_000, 600_000]  # 5s, 25s, 2min, 10min

def compute_backoff_ms(retry_count: int) -> int:
    if retry_count <= 0:
        return 0

    # 根据重试次数选择对应的基础退避档位(超出则取最大)
    idx = min(retry_count - 1, len(BACKOFF_MS) - 1)
    base = BACKOFF_MS[idx]

    # 加入随机抖动(±20%),避免大量消息同时重试造成网络风暴
    jitter = random.randint(-base // 5, base // 5)
    return max(0, base + jitter)

4. 知识总结

模块 作用
消息分片 适配不同平台的长度限制,避免发送失败
原子写入 利用 tmp+ os.replace,防止崩溃导致文件损坏
持久化队列 内存队列 + 磁盘存储,确保重启后任务不丢失
指数退避重试 失败后等待时间递增(含随机抖动),避免服务端过载
后台线程扫描 服务启动时自动恢复未完成的任务,实现高可用
相关推荐
sali-tec3 分钟前
C# 基于OpenCv的视觉工作流-章107-光流追踪
图像处理·人工智能·opencv·算法·计算机视觉
格林威3 分钟前
C# 图像使用AVX2指令集:使用OpenCvSharp实现字节图像解压缩速度和map_image算子速度提升
开发语言·图像处理·人工智能·计算机视觉·c#·视觉检测·工业相机
蓝速科技4 分钟前
蓝速科技丨多网点涉外窗口翻译机批量部署实战指南
服务器·数据库·人工智能·缓存·语音识别
Csvn7 分钟前
改坏了,在合并前就被拦住——AI 回归门禁实战(E04)
人工智能
来让爷抱一个10 分钟前
2026 KV Cache实战:把缓存契约写进SPEC,MonkeyCode 云端跑通
人工智能·机器学习
Geek-Chow11 分钟前
MCP 模型上下文协议:八、深入传输层 · stdio 与 Streamable HTTP
人工智能·mcp
QYR-分析13 分钟前
超分辨显微镜行业市场现状、竞争格局及发展前景分析
大数据·人工智能
古少侠14 分钟前
用 DS随心转 把 AI 给的表格导出成能筛选、能统计的 Excel
人工智能·excel
寻道码路17 分钟前
大模型工程化实战(八):OTel + LangFuse 全链路 Trace——把网关、护栏、SchemaGate 全部打点串起来,出问题一眼定位到环节
大模型·agent·mlops·rag·ai工程化·langfuse·llm可观测
Francek Chen18 分钟前
【HarmonyOS 7】新品发布:华为Mate XT 2携麒麟9050 Pro与鸿蒙7重磅登场
人工智能·华为·harmonyos·鸿蒙·小艺