Skip to content

fix: 优化记忆后台任务调度,避免历史导入阻塞实时任务和向量重试 - #734

Open
jia030928 wants to merge 34 commits into
MemTensor:v1.2.6from
jia030928:test-memory-queue-v1.2.6
Open

jia030928 wants to merge 34 commits into
MemTensor:v1.2.6from
jia030928:test-memory-queue-v1.2.6

Conversation

@jia030928

@jia030928 jia030928 commented Oct 24, 2026 •

Copy link
Copy Markdown

问题描述

历史导入、启动摘要修复和全量向量重建会产生大量后台任务。原调度依赖严格优先级,向量重试仅使用普通任务领取后的剩余名额,可能导致反思、向量重试等任务长时间无法执行。批量预领取及等待整批完成的执行方式,也会延长新任务的等待时间。

主要修改

队列调度

  • 将任务分为实时、修复重试、导入、演化、维护五类,使用加权轮询,权重为 4/3/2/2/1。
  • 同类任务按可运行 FIFO 选择,资源受阻任务保留原排队位置;向量重试参与统一调度,不再只依赖剩余名额。
  • 保持实时记忆和 Onboarding 首次报告的优先执行机会,并增加 Aging 补充机制。

统一并发控制和连续补位

自动处理、手动执行、重试和扫描共用并发控制。任务取得全部许可后才领取租约;任务完成后立即补位,不等待整批慢任务。

并发限制 默认设置
全局活跃逻辑任务 4
后台活跃逻辑任务合计 3
实时任务预留容量 1
同一本地模型服务 1
单个云 Provider 2
Embedding 请求 1
Embedding 批大小上限 16,仍受逻辑槽位等限制

limit=100 表示单次执行预算,不代表同时运行或提前领取 100 个任务。总并发配置为 1 时保留后台执行能力,避免预留规则使后台永远无法执行。

租约和失败恢复

  • 使用租约身份校验保护任务状态及结果写入,防止旧执行者覆盖新租约。
  • 完善续租、过期恢复、停止和异常时的许可释放,分离租约领取次数与业务失败次数。
  • 增加失败退避与随机抖动,区分临时错误、配置错误和永久输入错误。

运行观测

记录各类任务等待时间、完成情况、并发占用、模型请求、租约恢复及准入拒绝原因,并增加后台占用和实时预留容量指标。

联调兼容修复

  • 保留迁移后的独立记忆模型连接及其认证、请求配置。
  • 修复部分限流错误被误判为配置错误的问题,并兼容上游新增的 Responses 协议,使同一服务的 Responses 与 Chat 请求共享 Provider 并发上限。
  • 完善退出时 SSE、MCP 通知流和辅助进程的收尾,保留在途写请求。

审核反馈修复

  • 按资源保存受阻任务,正常补位和扫描结束时保留各类别、各队列的入队序号游标,区分扫描进度、是否需要继续扫描和是否有资源等待者,避免反复分页读取已知受阻积压。
  • 对应资源释放后,等待任务按原入队顺序重新参与选择,重新检查全部执行条件、取得全部许可后才领取;仍受其他资源阻挡时继续等待,让后面的可运行任务获得机会。
  • 等待组检查按每批最多 64 个分片继续,单次唤醒能完成整轮检查,结束后休眠;处理删除、同 ID 重新入队和外部依赖变化。
  • 保留尚未处理的到期点,避免同步扫描跨过期限后遗漏旧任务;覆盖延迟执行、失败退避和租约过期,并在配置或分类变化时重建必要的扫描状态。
  • 分类维护使用缺失/旧分类部分索引和有界补齐;同一未完成任务保留首次报告/主动重试待遇,新周期不继承旧提升。
  • smoke 计划覆盖预算 1/20、摘要与向量成功、真实向量落库、读取召回和重复调用无副作用。

最新验证(2026-10-27)

最终提交:9673ccf015ef8b8e30d576ff0301b4f9761e33a2。已通过 merge 同步 upstream/v1.2.6(0074a33b31a909b7964b8c56254945467eb9cfdc)。上游新增的导入状态测试已按实际租约过期和退避到期条件适配,保留“不误报处理中任务”的断言,并验证有效租约不可被重启回收。

  • 根目录 npm run lint、npm run typecheck、完整 npm test 全部通过。
  • Memory 两轮均 2332 项通过、2 项跳过;Backend 1030 项通过;Frontend 2736 项通过;Desktop 1308 项通过、126 项跳过。
  • Agent 类型检查及此次上游改动涉及的 190 项回归通过;打包守卫 129 项通过、1 项跳过;发布流程 31 项通过、5 项跳过。
  • 新增的 5 个边界回归在修复前全部失败、修复后全部通过,覆盖多依赖组单次唤醒、检查结束后休眠,以及扫描跨过延迟/退避/租约期限;打包后 181 项相关回归通过。
  • 最终 macOS ARM64 中文未签名 DMG 校验通过,包内 233 个 Memory 模块与最终源码编译结果一致。包内边界探针、资源恢复/FIFO、实时预留槽、隔离安装启动、登录页渲染、健康检查、退出重启和合成旧数据保留均通过。

包内持续入队测试:真实 SQLite 和 WorkerRunner,模型与业务执行使用受控替身;每 100 毫秒加入一条实时任务,共 49 条,每条模拟执行 1 毫秒。

稳定受阻积压 实时任务完成 最长开始等待 候选读取 / 检查次数
1 49/49 7.64 ms 50 / 50
10,000 49/49 11.64 ms 10,049 / 10,049
50,000 49/49 16.19 ms 50,049 / 50,049

三组均无未开始的实时任务;首次发现结束后的额外 10 次空闲唤醒没有重复读取或检查积压候选。这些数值衡量本机受控调度行为,不代表真实模型吞吐量。

安装包 Memory buildId:144b8912daea74babceb58b80f54eb6a6c6cf517db179f415921254e55e26b97。
DMG SHA256:11d28ab02080210bbe4431d3884c7c5689010d2e3088b41a73ca7379a396238a。
版本沿用 Desktop 1.2.4、Memory 2.1.7、schema 13;分支名与应用版本号不同。

验证范围

  • 预留槽位不突破模型并发限制,不抢占已经执行的模型请求。
  • 最新安装验证覆盖 macOS ARM64;Windows、Intel、真实云 Provider/LM Studio 负载及真实账号登录后的完整业务流程尚未验证。
  • 完整 HTTP smoke 运行时脚本本轮未执行;smoke 计划测试和包内核心链路通过不等于完整脚本通过。

李嘉 added 30 commits October 22, 2026 10:19
…-v1.2.6

# Conflicts:
#	App/backend/src/services/agent-source-service.ts
#	App/backend/src/services/tests/agent-source-service.test.ts
});
active.add(task);
}
if (remaining <= 0 || !this.deps.memoryAddEnabled() || this.stopping || failedDrain || execution.signal?.aborted ||

@origami-chen origami-chen Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

更新仍要求向量任务在第二轮执行的测试断言

问题: 当前 head 的 npm run smoke:memory-layer:test 为 2 项通过、1 项失败,错误为 expected [] to include 'embedding';指定 base 同一命令为 3 项通过。这是旧测试没有适配新设计,不是已证实的向量生成或检索故障。

原因: 旧测试要求第一次调用 worker 执行摘要,第二次调用返回向量任务。新设计在预算尚有剩余时连续补位,摘要产生的向量任务已经在第一次调用中执行完,第二次返回空是正常结果。使用真实 SQLite、MemoryService 和模拟模型的补充验证确认,摘要、向量生成及最终检索均正常。

建议做法: 只调整测试及相关断言,不修改产品调度逻辑。不再要求向量任务固定出现在第二轮,保留摘要成功、向量成功、记忆可读取并可检索的完整验证,并确认后续调用不会重复处理已完成任务。更新后重跑该测试及相关 smoke 入口,后续脚本原有的基线失败单独记录。

const paused = this.deps.memoryBudgetPaused();
const filters = JSON.stringify([retryOnly, paused, this.deps.memoryBudgetModelSources(), this.summaryJobsHeld(),
this.deps.configurationFingerprint?.()]);
if (!scan.more || scanVersion !== admissionVersion || scanFilters !== filters ||

@origami-chen origami-chen Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

无关资源释放反复重置扫描,导致可运行任务长期得不到调度

关联位置:

  1. 当前文件 581 行的资源释放订阅;
  2. worker-concurrency.ts (line 327) 的释放通知。

问题: 大量历史任务需要的模型服务并发名额已满时,后面的任务即使使用其他可用资源,也可能长期轮不到执行。

使用真实 SQLite、WorkerRunner、并发控制器及受控业务替身的测试中,20,000 条受阻任务后的可运行任务,在无关请求持续完成的 15 秒内始终没有开始;停止干扰后又等待约 4.20 秒才执行。换成使用独立 Provider 的演化任务仍复现。无关请求降到每秒一次、持续 5 秒时,目标也要等到约 8.22 秒才开始。有限测试证明了干扰期间没有进展;相同干扰持续存在时,代码路径可能持续阻止任务被调度。

原因: 当前已经能解析任务需要哪些资源,也会检查 Memmy 自己维护的资源占用和并发上限。问题不在于缺少资源感知,而在于释放通知没有告诉扫描器具体哪项资源发生变化。
任何资源释放都可能增加 admissionVersion,随后清除整个扫描进度。每次最多检查 64 个候选,无关请求不断结束就会让扫描反复回到队首。15 秒内累计检查 64,512 次,却始终没有越过前 192 条。Aging 只能帮助已经读到的候选,无法解决这个问题。诊断性保留扫描进度后,干扰仍在持续,任务约 4.02 秒就开始了。

建议做法:
保留五类加权轮询、同类可运行 FIFO、并发限制和先取得许可再领取任务的设计,调整共用的扫描及资源通知逻辑:

  • 保留向后检查的进度,无关资源释放不能使整个扫描归零。
  • 记录阻止任务运行的具体资源;资源状态未变化时,避免反复检查已确认受阻的任务。
  • 相关资源释放后重新考虑这些任务,保留原 FIFO 位置,不能移到队尾或永久跳过。
  • 全局槽位、Provider、Embedding、同目标互斥等限制采用一致规则;真正的配置、分类或可运行条件变化仍需正确重新检查。

回归应验证:持续干扰期间可运行任务有进展;原资源释放后旧任务恢复;多资源任务不突破并发上限;开启 Aging 后仍正常。诊断性取消重置只证明根因,不能代替完整修复。

${queue === "main" ? "q.job_type, q.payload_json" : "'embedding_retry' AS job_type, '{}' AS payload_json"},
m.agent_id, m.memory_layer, m.tags_json,
json_extract(m.properties_json, '$.internal_info.plugin_algorithm') AS plugin_algorithm,
json_extract(m.properties_json, '$.internal_info.capture_origin') AS capture_origin

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

分类已有效仍在每次补位时扫描大量积压,显著增加调度开销

关联位置:

  1. fair-worker-queue.ts (line 215) 的重复调用;
  2. queue-scheduling-schema.ts (line 46) 的分类失效触发条件。

问题: 即便所有任务的分类已经有效,调度器仍重复进行昂贵检查。使用立即完成的业务替身,固定只处理前 1,000 条,初始队列为 1,000/10,000/50,000 条时,耗时分别为 1.53/3.07/10.68 秒。
50,000 条场景没有一条分类需要更新,分类查询仍调用 278 次、耗时 7.54 秒,占总耗时约 71%;事件循环延迟 p99 为 121.90 毫秒,影响同进程处理新任务及其他请求的及时性。

原因: 分类检查放在每次领取切片的路径中,没有变化也会反复执行。SQL 先通过子查询找出大量可运行任务,再判断分类版本是否过期;maxRows=64 只限制返回或更新条数,不能限制子查询的扫描规模。此外,普通记忆属性更新也可能使关联任务被标记为需要重新检查分类,触发范围过宽。
这不是独立的分类守护进程,而是繁忙队列反复进入同一检查路径。隔离测试加入“没有过期分类”的前置判断后,总耗时降到 3.10 秒,领取的 1,000 条任务及顺序完全相同,支持这段检查是主要额外成本。

建议做法:
将分类维护放到明确事件上:

  • 新任务创建时确定并保存分类。
  • 软件升级且分类规则版本确实变化时,对相关旧任务进行分批、可恢复的补齐。
  • 用户主动操作需要复用并提升已有任务待遇时,定向更新该任务。
  • 普通补位使用已保存的有效分类,普通记忆属性更新不应宽泛地使关联任务重新分类。

同步核对主队列、向量重试、去重合并及旧数据写入入口。确有缺失或过期分类时,只处理这些记录,不能遗漏,也不能恢复为每次全队列扫描。
同一个未完成任务被复用时,应保留已获得的较高执行待遇。例如,历史任务因用户主动重试而提升后,后续普通扫描不能将其降回历史导入类,首次报告待遇也应保留。应按明确的业务提升规则合并,不能把轮询权重直接当作优先级,也不能将待遇永久继承给以后新建的独立任务。这是修复约束,不是另一个已经完整复现的缺陷。

回归应覆盖不同积压规模下的同预算成本、升级后旧分类补齐、主队列和向量重试不遗漏,以及“后台创建→用户主动提升→后台再次复用”不降级且保留 FIFO 位置。上述性能数据衡量的是调度开销,不能直接外推为真实云模型吞吐。

mainSeen: [], retrySeen: [], mainDone: false, retryDone: false, agingSkippedLanes: [] };
let inspected = 0;
const inspectLimit = scan ? FAIR_SCAN_CANDIDATE_LIMIT : Number.POSITIVE_INFINITY;
const saveScan = (more: boolean) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

受阻任务仍被反复遍历,持续到来的实时任务等待过长(部分解决)

1. 问题是什么
大量历史任务因模型资源没有空闲名额而受阻时,调度器反复遍历这些任务,拖慢了使用其他空闲资源的实时任务。这是上轮 CR-002 尚未彻底修复的部分。
复审 head 的受控测试中,50,000 条历史任务保持受阻,每 100 毫秒加入一条执行耗时为 1 毫秒的实时任务;5 秒共加入 49 条,到第 7 秒只完成 15 条,34 条尚未开始。已开始任务平均等待 2.18 秒、最长 4.65 秒。相同输入、仅 1 条受阻历史任务时,49 条全部完成,最长等待 9.49 毫秒。测试使用真实 SQLite 和调度器,业务及模型调用为替身。
2. 已经修了什么
已经按具体阻塞资源保存等待任务,并在相关资源释放后重新纳入候选。无关资源释放不再清空正在向后扫描的进度;已知受阻的任务也不再反复解析资源、申请许可。上轮“扫描总被打断,无法找到后面的可运行任务”的路径已修复。
3. 还有什么没修
扫描位置没有跨正常补位完整保留:本次槽位填满或扫描结束时,saveScan(false) 仍会丢弃扫描位置,下一次补位从队首重新读取。虽然随后能认出“这条任务已经受阻”并跳过,但分页读取和逐条选择仍然重复发生。只增加计数器的复跑记录了 202,496 次命中已知受阻记录的重复检查;现有只检查资源解析次数的测试无法发现这部分开销。
4. 应该怎么修
沿用现有加权轮询、可运行 FIFO、并发限制和资源等待队列,完善共用扫描逻辑与 WorkerRunner 的等待状态。分别保存“扫描到哪里”“是否立即继续扫描”“是否仍有任务等待资源”,使用现有稳定入队序号保存各类别、各队列的扫描位置;正常补位或扫描结束时,不要无条件从头重扫。
资源释放后,让对应等待任务按原入队顺序重新参与调度,并重新检查全部执行条件、取得全部许可后才领取任务。如果还受另一项资源阻挡,就继续等待该资源,同时允许后面的可运行任务被考虑;资源释放不等于直接执行,也不能让一个仍受阻的队首卡住其他可运行任务。
新任务入队后,应能从已扫描范围之后发现它;已完成或删除的任务应从等待记录中清除或在重新检查时识别失效。较早任务到期、失败退避结束、租约过期,以及配置或分类变化时,应重新检查受影响任务,必要时重建扫描状态;服务重启后也应重新发现待执行任务。因此,保留扫描位置不是“永远只扫一次”,也不要求因正常入队、出队新增周期性全量扫描。不能通过永久跳过旧任务或提前结束后台处理来减少重扫。
回归应覆盖 10,000/50,000 条稳定受阻积压中持续加入使用空闲资源的实时任务,核对等待时间、未开始数量和候选分页/检查次数,并保留资源恢复后的原 FIFO、多资源再次受阻、新增/删除/到期任务、配置变化、有限预算和停止后的许可释放验证。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants