Skip to content

[BUG][P1] 独立 ReAct 应取消在运行终止后迟到的模型流订阅 #276

Closed

Description

@Aias00

已确认问题

ReAct 的模型流订阅仅用 modelSubscription.compareAndSet(null, subscription) 接入,没有检查运行是否已取消/失去执行权,也没有用已取消哨兵封闭接入。取消回调遇到尚未到达的 onSubscribe 时只能取消等待 Future;迟到的订阅随后仍被 request(Long.MAX_VALUE),已经取消的 Agent 反而启动了上游模型流。

代码:https://github.com/agentic-ai-java/argi/blob/b34835354b27f1f320ea8438edf2e9d170e69c84/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/react/ReActAgent.java#L405;https://github.com/agentic-ai-java/argi/blob/b34835354b27f1f320ea8438edf2e9d170e69c84/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/react/ReActAgent.java#L418。

可控复现

  1. 假 ChatClient.stream 返回一个 Publisher:subscribe 时保存 Subscriber,暂不调用 onSubscribe。
  2. 使用 ReActAgent.builder(client).streamModel(true) 启动 invoke,等待假 Publisher 收到订阅。
  3. 调用 Agent 返回 Future 的 cancel(true),使运行先完成取消。
  4. 再向保存的 Subscriber 发送一个记录 request/cancel 次数的 Subscription。

实测:requests=1, cancels=0;期望 requests=0, cancels=1。不需要真实模型、外部服务或依赖网络抖动。此问题不同于正常完成时误取消共享上下文,也不是流式 fallback 的切换策略。

影响

消费者认为运行已经停止,但迟到的上游仍可开始工作,可能继续占用连接与产生调用消耗;停止控制与实际执行脱节。

交付与验收

  • 将模型订阅接入与运行终止建立可靠的并发边界,取消后迟到的订阅立即取消,不再请求数据。
  • 不以清空引用重新开放已终止订阅的接入;旧轮次的迟到回调也不能占据新轮次的订阅槽。
  • 覆盖先取消后 onSubscribe、先 onSubscribe 后取消、并发竞态、重复订阅及正常多轮完成。
  • 通过受控 Publisher/屏障验证调用次数和终止行为,不只断言外层 Future 已取消;检查所有权丢失等停止路径的同类行为。
  • 保持取消向实际模型订阅传播;取消后的回调不再向下游交付事件。

关联与边界

关联 #196、#195、#206。#206 负责额外结构化修复调用的停止边界;本单只修复独立 ReAct 主模型流的迟到订阅竞态,不扩展持久化调度或 fallback 能力。

验证基线与变更约束

Core b34835354b27f1f320ea8438edf2e9d170e69c84;Extensions cb2635ab2fc338e8aade28a132a15f48b4e655fc。本轮 Core 19 模块 clean install 通过,2517 个测试中 173 个跳过;此处缺陷由额外的独立 Java 探针确认,说明已有测试尚未覆盖对应行为,不代表真实模型或生产环境验收。

优先修复现有契约的实现,不强制拆模块或重命名,不让独立 Agent/Workflow 反向依赖 Graph/Loop,不向 Core 引入供应商 SDK。若必须改变公开 API、事件、配置或持久格式,说明较小替代方案、消费者影响、收益、迁移成本与目标版本;不得通过删测试、吞错误或弱化取消保证获得通过。

Activity

  1. konlue commented on Oct 29, 2026

    @konlue
    Contributor

    您好,我想认领 #276。

    方案(单 PR,不动公开 API / 事件 / 配置)

    • onSubscribe 前置校验:onSubscribe 中 CAS 之后先判 cancelled.get() || future.isCancelled();命中则 compareAndSet(subscription, null) 归还槽位并 subscription.cancel(),不 request。
    • 原子所有权:cancel() 与 onLoss 回调统一改为 getAndSet(null):谁拿到引用谁负责取消,天然实现 exactly-once,也不会被后续清理抹掉。
    • 不使用订阅哨兵(sentinel):规则会跨 turn 残留,并与一次性 CAS 和 finally 清理冲突;用“单调标志 + 原子所有权”两件事分别表达“能不能开始”和“谁负责取消”即可。
    • 已知固有竞态声明:guard 读发生在前、cancel 写标志发生在后的极窄窗口里仍可能 request 一次。这是响应式规范允许的(RS 3.6 要求 Publisher 自行处理 request-after-cancel),不为它加锁,PR 正文会如实声明。

    测试(离线,受控 Publisher,无真实模型)

    新增 ReActStreamSubscriptionCancellationTest,共 5 个用例:

    1. 取消后迟到 onSubscribe:requests=0 / cancels=1(issue 原样复现);
    2. 正常流式完成:requests=1 / cancels=0(防止把正常完成误取消);
    3. 两轮流式:每轮各 1 次 request、0 次 cancel(抓 finally 清理缺失);
    4. 第二轮取消:只关当前订阅,第一轮不受影响;
    5. context.control().cancel() 路径(deadline/所有权丢失):迟到订阅同样不得 request。
  2. added a commit that references this issue on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions