Repository navigation
fix: 优化记忆后台任务调度,避免历史导入阻塞实时任务和向量重试 - #734
Conversation
…-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 || |
There was a problem hiding this comment.
更新仍要求向量任务在第二轮执行的测试断言
问题: 当前 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 || |
There was a problem hiding this comment.
无关资源释放反复重置扫描,导致可运行任务长期得不到调度
关联位置:
- 当前文件 581 行的资源释放订阅;
- 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 |
There was a problem hiding this comment.
分类已有效仍在每次补位时扫描大量积压,显著增加调度开销
关联位置:
- fair-worker-queue.ts (line 215) 的重复调用;
- 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) => { |
There was a problem hiding this comment.
受阻任务仍被反复遍历,持续到来的实时任务等待过长(部分解决)
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、多资源再次受阻、新增/删除/到期任务、配置变化、有限预算和停止后的许可释放验证。
| scanDeadline = this.nextAdmissionDeadline(this.nowMs(), request.targetMemoryIds, retryOnly); | ||
| prepareExecution = this.deps.prepareExecutionBatch?.() ?? this.deps.prepareExecution; | ||
| } | ||
| if (deadlineWakeVersion !== this.wakeVersion) { |
There was a problem hiding this comment.
普通任务完成也重查整个队列的到期时间,积压越多,后台处理越慢
问题是什么
大量后台任务已经可以立即执行时,完成普通任务仍会反复查询“整个队列下一次何时有任务到期”,增加同步数据库开销。在真实 SQLite 和调度器中放入 50,000 条分类有效、没有延迟时间的任务,只执行其中 1,000 条:上轮 head 同条件重跑为 2.60 秒,本轮为 8.44 秒。到期查询从 1 次增至 1,001 次,累计耗时从 3.66 毫秒增至 3.34 秒,约占本轮总耗时的 40%。测试不计造数时间,使用空业务处理函数,没有真实模型请求;其他检查开销也有变化,不能把全部变慢都归因于到期查询。
已经修了什么
测试已适配连续补位;扫描位置和资源等待状态能够保留;分类维护不再遍历全部分类有效的任务。其中,5 万条持续受阻任务中新增的 49 条实时任务全部完成,平均等待 9.90 毫秒、最长 15.76 毫秒,计数复测没有重复遍历已知受阻记录。这些修复应当保留,当前 comment 是本轮新增的性能回退。
还有什么没修
调度器用同一个变化标记 wakeVersion 处理新任务、时间条件变化、普通完成和资源释放。标记一变,就通过 nextAdmissionDeadline 调用数据库的 nextWorkerRunAt 查询最近到期时间。因此,即使没有任何时间条件改变,普通完成也会触发重查。
查询本身也有优化空间:主任务的可执行时间取失败退避时间 available_at 与业务延迟时间 runAfter 中较晚的一个。当前查询计划按任务状态读取候选记录,再计算、筛选和排序,现有索引未有效支持这个计算结果的顺序查找。5 万条任务没有延迟条件时,仍会反复检查大量普通排队记录。向量重试的待执行查询已有 (status, next_attempt_at) 索引,不能笼统地认为所有到期查询都缺少索引。
应该怎么修
本轮按以下三点修复和验收:
- 减少无意义的查询。 普通任务完成或资源释放,在未改变待执行时间的情况下,继续使用已知到期时间,不再反复触发昂贵的到期查询;资源可用后仍应正常补位。
- 及时发现真正的变化。 新任务、已有任务的延迟时间调整、失败重试、租约变化,以及影响候选任务范围的状态或配置变化,仍应及时影响调度。成功完成若派生新任务,也必须处理新任务带来的变化。主任务与向量重试都应覆盖,既要考虑正在扫描,也要考虑调度器空闲等待的情况。
- 降低必要查询的成本。 优化现有 SQL 与匹配索引,使大量普通排队任务不再显著拖慢到期查询,同时保留现有时间计算、状态和目标任务筛选规则。不要求改变队列调度算法,也不通过删减功能换取性能。
复验固定执行 1,000 条、积压分别为 1,000/10,000/50,000 条的场景,记录总耗时、到期查询次数与累计耗时,并复测实时任务的平均、p90、p99 和最长等待时间。保留已有到期正确性测试,重点覆盖扫描期间及空闲等待时新增更早到期任务、修改已有任务延迟、失败退避、租约到期和查询期间跨过到期时刻;不能仅延长轮询间隔或永久缓存查询结果。
问题描述
历史导入、启动摘要修复和全量向量重建会产生大量后台任务。原调度依赖严格优先级,向量重试仅使用普通任务领取后的剩余名额,可能导致反思、向量重试等任务长时间无法执行。批量预领取及等待整批完成的执行方式,也会延长新任务的等待时间。
大积压下,普通任务完成与无关资源释放还会触发反复的到期查询。此次增加事务一致的到期变更序号及匹配索引,减少重复查询,同时保留新入队、更新时间、退避、租约及配置变化后的发现与执行。
主要修改
队列调度
统一并发控制和连续补位
自动处理、手动执行、重试和扫描共用并发控制。任务取得全部许可后才领取租约;任务完成后立即补位,不等待整批慢任务。
limit=100表示单次执行预算,不代表同时运行或提前领取 100 个任务。总并发配置为 1 时保留后台执行能力,避免预留规则使后台永远无法执行。租约和失败恢复
运行观测
记录各类任务等待时间、完成情况、并发占用、模型请求、租约恢复及准入拒绝原因,并增加后台占用和实时预留容量指标。
联调兼容修复
审核反馈修复
按资源保存受阻任务,正常补位和扫描结束时保留各类别、各队列的入队序号游标,区分扫描进度、是否需要继续扫描和是否有资源等待者,避免反复分页读取已知受阻积压。
对应资源释放后,等待任务按原入队顺序重新参与选择,重新检查全部执行条件、取得全部许可后才领取;仍受其他资源阻挡时继续等待,让后面的可运行任务获得机会。
等待组检查按每批最多 64 个分片继续,单次唤醒能完成整轮检查,结束后休眠;处理删除、同 ID 重新入队和外部依赖变化。
保留尚未处理的到期点,避免同步扫描跨过期限后遗漏旧任务;覆盖延迟执行、失败退避和租约过期,并在配置或分类变化时重建必要的扫描状态。
分类维护使用缺失/旧分类部分索引和有界补齐;同一未完成任务保留首次报告/主动重试待遇,新周期不继承旧提升。
smoke 计划覆盖预算 1/20、摘要与向量成功、真实向量落库、读取召回和重复调用无副作用。
将补位唤醒与实际到期状态变化分开,普通完成和无关资源释放复用到期缓存;同一快照读取变更序号和到期时间,自己的领取操作只确认数量匹配的租约变化。
为主任务到期表达式、主任务租约和进行中的向量重试建立匹配的部分有序索引,待执行重试继续使用原有索引。
同轮到期判断与查询共用时间基准,避免查询期间刚到期的旧任务被过滤并遗忘;12 项新增回归覆盖延迟、runAfter、失败退避及两类租约的准确边界。
同步最新上游向量维护时保留模型来源标记、Embedding 批处理和请求并发控制;兼容无主重试恢复与旧索引升级,并让升级修复在共享 worker 入口内执行及按期唤醒。
本次冲突解决与验证(2026-10-28)
合并提交:
3a49052049c71696705d5d380131d188046a547c,通过 merge 同步upstream/v1.2.6(67744eb137db74a6a0f483b4ac5192815be7e9ce)。episode_assignment、episode_finalize注册调度策略,历史扫描产生的同类任务进入导入通道,保留会话顺序依赖并在依赖完成后重新参与调度。npm run lint、npm run typecheck、smoke 类型检查通过。完整 Memory 初跑发现的 9 项旧批处理/退避/租约夹具预期差异已修正,并通过对应定向复测;未放宽业务断言。前次验证(2026-10-28,本次同步上游之前)
前次验证提交:
1cd72bdbcd05f9460d4ea67d9033aea909605186。通过 merge 同步upstream/v1.2.6(9b494d84705cf30f2325c0cd8acfb23657cc8e7d),已处理合并冲突;仅向个人 fork 的任务分支推送。npm run lint、npm run typecheck、Agent 类型检查通过。VITEST_MAX_WORKERS=2 npm test通过。测试进程上限用于避免本机高并行引发大数据诊断测试超时,未放宽断言或测试时限。固定执行 1000 条可运行任务:真实 SQLite 和 WorkerRunner,预先入队并分类,业务和模型调用使用替身;构造数据不计入耗时。
包内持续入队测试:历史任务使用的模型资源保持受阻,每 100 ms 加入一条使用空闲资源的实时任务,共 49 条,每条处理替身执行 1 ms。等待时间从入队到处理开始,单位 ms;P90/P99 使用最近秩法,49 个样本的 P99 等于最大值。
三组均无未开始的实时任务;首次扫描结束后额外 10 次空闲唤醒没有重复读取或检查积压候选。这些数值衡量本机受控调度行为,不代表真实模型吞吐量。
安装包 Memory buildId:
9398738f2fae3352a70b7138b6502e2a021c1dee8358a21258719b13171fa634。DMG SHA256:
bd306aadd64588b82e06be594bf18ea48e674a171f568242722927cfdd76f0d0。版本沿用 Desktop 1.2.4、Memory 2.1.7、schema 13;分支名与应用版本号不同。
验证范围