简体中文
运行时架构
面向修改后端服务的贡献者。录制流程和部署边界见系统概览。
高层拓扑
运行时服务之间的箭头表示逻辑事件路径。ServiceContainer 通过广播订阅、有界队列和处理 任务完成这些接线,而不是让服务彼此直接耦合。
该拓扑中有三个重要的职责边界:
ServiceContainer是组合根与事件接线层,并不持有领域状态的所有权。StreamMonitor负责探测与过滤平台状态;SessionLifecycle独占内存中的会话状态机以及 持久化启动/结束决策。- 直播事件会先进入下载启动流程;只有下载管理器的
start_with_slot返回真实下载 ID 后, 才会开始弹幕采集。
运行时根:ServiceContainer
ServiceContainer(位于 rust-srec/src/services/container.rs)负责把所有组件串起来:
- 初始化仓储与服务(数据库、配置缓存、各类 manager/service)
- 启动后台任务(scheduler actors、pipeline workers、outbox flushers)
- 订阅事件流,并在服务之间转发/协调事件
- 持有用于优雅退出的
CancellationToken
这让系统的生命周期与依赖关系有一个统一的“入口点”,方便定位与演进。
服务容器职责
容器组装、有序关闭、输出根目录辅助逻辑和事件决策位于独立的私有模块中。初始化仍只发现一次输出根目录,并将同一份快照用于健康检查注册和启动写入探测。容器公开 API 和关闭期限保持不变。
启动日志保留数据库与配置 I/O、引擎发现、需要等待的初始化阶段及整体耗时。不再单独记录同步包装对象构造和后台任务启动的耗时;这些记录并不代表任务随后执行工作的耗时。
核心组件(按实际实现)
RuntimeCoordinator(录制启动与取消)
协调器将下载启动和弹幕采集绑定到触发它们的会话。指定旧会话的 Offline 事件只停止该 会话的工作,不会按主播 ID 误选后续会话。禁用和离开录制时间窗口的事件,也会取消已 注册会话取消令牌、但尚未进入下载队列的启动任务。
启动流程在配置、预检、排队、时效检查和最终接纳期间响应取消。取消后释放预留资源, 并且不启动弹幕采集。弹幕初始化和前序采集任务交接也响应会话取消;采集任务注册后, 取消会等待其受管理的清理完成,不会丢弃就绪等待并遗留任务。 从滞后窗口恢复的 Started 转换必须同时携带下载参数且会话仍活跃。 只有排队等待时间严格超过时效阈值时,才一起刷新 URL、请求头和附加信息。等于阈值时 保留缓存媒体并执行短等待状态校验;主播缺失或已离线时出队,检查器错误则回退到缓存媒体。
ConfigService(配置合并 + 热更新)
ConfigService 是配置控制面,负责加载并合并四层配置:
- 全局默认(Global)
- 平台配置(Platform)
- 模板配置(Template)
- 主播覆盖(Streamer overrides)
它会缓存合并后的结果,并广播 ConfigUpdateEvent,让运行时服务可以无重启响应配置变更。
参见:配置
StreamerManager(已提交的元数据快照)
SQLite 行是权威数据。StreamerManager 为 actor 检查提供不可变的共享元数据快照, 为管理操作保留克隆值接口。运行时写入共享 CommittedStreamerState:经过校验的 单写连接会持续持有到借用的立即事务提交,以及同步元数据/URL 索引发布完成。 等待运行时副作用时不占用该数据库连接。
提交开始前取消会回滚已准备的变更;进入提交后,即使请求任务消失,受监督的完成 任务也会发布已提交快照。状态、会话和 outbox 仍原子更新;RETURNING 提供主播行, 不再在提交后重新查询。发布时保留当前仅存在于运行时的下播检查配置。管理补丁读取 数据库当前行,保留无关凭据和计数器;失败的 URL 修改不会改变任何缓存索引。合并配置 的失效与在途解析完成共享短临界区,较早的加载不会在新提交使缓存失效后重新放回旧凭据。
导入在提交时静默发布行快照并使过滤器快照失效,然后释放写连接,由同一任务所有者 按顺序完成凭据、退役、元数据、通道和全局通知。一个导入准入许可限制这些运行时 工作数量;提交后的重载失败仍作为已提交变更的警告返回。
一个重要的正确性细节:启动时会执行 重启恢复,把数据库中遗留的 Live 状态重置为 NotLive,确保 NotLive → Live 这条边能够再次触发下载启动。 即使前后状态均为 NotLive,actor 也会处理首次确认的下播结果,结束数据库中遗留的未完成 会话并发布最终完成事件。处理失败或被抑制时会等待后续检查重试。开播结果会复用未完成 会话,保留重启前后的录制连续性。
Scheduler(Actor 模型编排/调度)
Scheduler 采用 supervisor + actor 的结构:
StreamerActor:单个主播的自调度状态循环(自己管理定时)PlatformActor:对支持批量探测的平台进行批量协调Supervisor:负责 actor 生命周期、崩溃恢复、退出汇总
Actor 会调用 StreamMonitor 做真实状态探测;Scheduler 同时订阅配置事件,动态创建/移除 actor。
调度器在创建 actor 或发送时序更新前,通过共享配置服务解析全局 → 平台 → 模板 → 主播四层配置。它直接读取当前配置层,不依赖容器另一个订阅者刷新元数据的先后顺序, 因此离线确认次数和延迟不会因事件竞争停留在默认值。发送给 actor 与用于重启恢复的 配置来自同一次解析。解析失败时,现有 actor 保留上一次有效时序;新 actor 不会使用 未解析的默认值启动。更新解析最多并发八个查询,并响应关闭取消信号。 排队等待的崩溃重启在到期时重新解析配置,包括退避期间发生的变更。查询失败时 会将该次重启延后五秒,而不会使用此前保存的旧配置启动。
周期检查在配置间隔的 ±10% 范围内随机分散。有效间隔未变时保留原定检查时间; 实际间隔变化可以提前检查,但不会推迟已经安排的检查。智能唤醒、准入/冷却期限、 明确的立即检查以及暂停/直播状态保留各自原有的时序约束。Rust 调用方现在需要等待 Scheduler::add_streamer 完成配置解析后再创建 actor。
不可恢复的 actor 错误表示终止决定,例如主播已被移除。定时器与消息路径都会 正常停止,并执行已配置的状态持久化;supervisor 不会为它们安排重启。可恢复的 任务失败与 panic 仍可重启。连续十次崩溃后停止自动重启,即使较早的崩溃已离开 六十秒退避窗口。该窗口仍决定重启延迟;显式清除失败记录或移除 actor 会重置 崩溃额度。
下载终止反馈保留会话生命周期的权责边界。应用关闭和主播禁用导致的停止,只会 暂停 actor 本地轮询,不会上报下播:关闭时保留会话供恢复,禁用清理流程负责 关闭相应会话。未知的内部停止原因会恢复状态检查,以验证平台实际状态。 权威的主播下播反馈仍会通过 monitor 上报 Offline。
StreamMonitor(探测 + 过滤 + Outbox)
StreamMonitor 是数据面的探测器,负责:
- 根据 URL/平台解析直播状态(含过滤:时间/关键词/分类等)
- 将会话变更委托给
SessionLifecycle - 通过 DB-backed Outbox 机制发出
MonitorEvent
Outbox 模式:将“状态/会话变更”与“事件写入 outbox”放在同一 DB 事务里,然后由后台 任务定期/通知触发,把 outbox flush 到 Tokio broadcast 事件流,从而降低 “状态已变更但事件丢失”的风险。
SessionLifecycle(会话状态的唯一所有者)
SessionLifecycle 负责录制状态机,包括滞后窗口(hysteresis)和终止原因分类。新的会话 启动和持久化结束会先提交各自所需的数据库变更,再广播 Started 或 Ended。滞后阶段的 Ending 与 Resumed 属于内存状态转换,其审计写入是 best-effort;在生命周期真正进入 Ended 前,session 的 end_time 保持为空。下载终止事件会回流至此服务。Ended 会驱动 会话完成管道、弹幕清理与下载状态清理;恢复后的 Started 会重启同一会话。 同一主播的操作通过异步锁串行执行数据库提交、内存更新和事件发布;定时器到期遵循相同 顺序,并在取得句柄前重新检查取消状态。数据库结束操作只更新活跃行,因此延迟事件不会 改写结束时间或重复发布完成事件。明确指向旧会话的下播信号不会改变较新活跃会话的状态。
已获准的操作持有准入许可与主播锁直到完成。在 COMMIT 或首个不可逆的内存转换之前, 调用方取消可以回滚操作。一旦跨过该边界,即使调用方消失,受监督的操作也会完成缓存发布、 会话映射、best-effort 审计写入,以及必达与观察者事件发布。结束操作遇到 SQL 错误时保留 原有滞后句柄;主播锁使恢复或定时器等待该次结束尝试完成。定时器结束失败时,由同一个 定时器任务重试,每次尝试之间释放准入许可与主播锁。正常关闭会等待这些操作完成。 强制关闭遵守硬截止时间,同时不终止正在进行的 COMMIT;未完成操作继续受监督,数据库连接池保持打开。
DownloadManager(下载调度 + 引擎抽象)
DownloadManager 负责:
- 并发控制(含高优先级额外并发槽位)
- 失败分类与熔断器(按引擎类型、配置以及可选的主播范围隔离)
- 失败/拒绝事件与 retry-after 提示;由 Scheduler Actor 决定何时重新探测并再次进入下载 启动流程
- 引擎抽象:
- 外部进程:
ffmpeg、streamlink - 内置 Rust 引擎:
mesio
- 外部进程:
并通过 DownloadManagerEvent 广播下载生命周期与分段事件。
对于落库后的 session 分段,后端会保留三种不同含义的时间戳:
created_at:该分段开始录制的时间completed_at:该分段结束录制的时间persisted_at:该分段元数据写入 SQLite 的时间
DanmuService(弹幕/聊天采集)
弹幕采集以 session 为单位维持连接,以 segment 为单位落盘:
- session 期间维持 websocket 连接与统计(可选)
- 由下载分段边界驱动,开启/结束对应的弹幕文件(如 XML)
- Danmu 事件会转发到 pipeline,用于“视频+弹幕配对”等协调逻辑
PipelineManager(队列 + DAG + WorkerPool)
PipelineManager 是后处理引擎:
- DB-backed job queue(支持重启恢复)
- DAG 执行(fan-in / fan-out、fail-fast)
- CPU/IO 分离的 worker pool
- 多阶段触发协调:
- Segment pipeline(单个文件)
- Paired-segment pipeline(同一分段的 视频 + 弹幕)
- Session-complete pipeline(会话结束后、所有分段完成后触发)
参见:DAG 管道
NotificationService(事件分发)
NotificationService 订阅监控/下载/会话/管道事件,并分发到 Discord / Email / Gotify / Telegram / Webhook 通道,包含重试、熔断与 dead-letter 持久化。可选的浏览器 Web Push 由 WebPushService 处理。
参见:通知
仓库行写入
完整作业、DAG、步骤、会话、媒体输出和分段写入,在已有 SQLite 连接上复用仓库负责的 列绑定。重试、时钟和事务仍由调用方拥有。DAG 发布先插入未关联作业的步骤,再在同一 事务中关联根作业;后续物化仍使用独立的活跃父 DAG 和 PENDING/BLOCKED 条件。
原始会话创建保留传入模型,生命周期创建则构建带初始标题的活跃会话。原始结束操作仍 无条件更新,生命周期结束只修改活跃行。媒体插入与会话大小记账共同提交,组合分段创建 也加入该事务;单独插入分段不增加会话总大小。保留存储的毫秒值、可空生命周期时间戳和 未显式指定的数据库默认值。
下载器 Rust 接口
下载管理器将事件契约放在 downloader::manager::events,确认式事件传递放在 coordination,引擎配置放在 configuration,下载任务生命周期放在 attempt。这些实现模块保持私有;通过 downloader 导入现有事件的公开路径以及通过 downloader::manager 导入的内部路径保持不变。运行时关闭使用 DownloadManager::shutdown_until,活动下载条目持有队列槽位,直到条目被移除。
Rust 集成应使用受支持的管理器关闭与快照方法、DownloadConfig::build_pipeline_config / build_hls_pipeline_config / build_flv_pipeline_config,以及公开的 CircuitBreaker 方法。内部熔断器管理通过 CircuitBreakerManager::get 获取实例。Mesio 引擎诊断现在报告所链接库的 mesio::VERSION。
FFmpeg 录制事件
直接 FFmpeg 录制与 Streamlink 重封装共享分片标识、时长与字节累计、进度采样、 输出错误分类及最终事件发布。文件系统字节缓存只属于当前分片:切换分片后, 在新分片的元数据采样成功前使用解析出的进度字节数。每个活动分片的文件系统 采样仍以 500 毫秒为间隔节流。
标准错误 EOF 不代表最终文件已经关闭。跟踪器等待进程所有者报告退出结果后, 才检查并发布最终分片;无法确认清理完成时不发布该分片完成事件。输出 I/O 错误 仍先于终止失败事件发布;仅当标准错误尚未识别输出错误时,退出码 228 才提供 磁盘已满的回退分类。
辅助任务收尾会跨超时保留已完成的等待结果,在无法确认清理时中止并等待未完成 任务,确认清理成功后则允许最终事件正常发布。FFmpeg 的标准输入停止命令与 Streamlink 的生产者、管道和受控子进程关闭策略仍分别保留。Streamlink 经审计的 协作组件会在本次录制剩余的进程预算内停止采集并排空已接收的工作,也支持隐藏的 Windows 子进程。不支持的读取器和强制终止会明确报告未完整排空;详见 停止 Streamlink 录制。
关键流程
录制生命周期(端到端)
API 请求流(控制面)
配置 JWT 后,大多数受保护路由使用 JWT 中间件。完整健康检查与就绪检查会在处理器内部 校验 bearer token;未配置 JWT 鉴权时也会返回 401。liveness 路由保持公开。WebSocket、 媒体与流代理路由使用各自文档中说明的查询参数鉴权路径。
调度器状态与退避
主播 Actor 使用内存中的调度状态以及基于数据库的元数据缓存。运行时恢复仍使用数据库与会话生命周期,无需 Actor 状态文件。
监控保留事务式错误写入与 disabled_until_for_error_count 计算。退避保留配置的阈值,从 60 秒开始翻倍,最多一小时;即使存储的错误计数很大,也会安全达到上限,不会溢出。数据库与缓存发布现共享上述已提交写入边界。
可靠的生命周期反馈
录制启动与结束后的调度反馈使用独立且受管理的通道,与进度广播及持久化确认分离。容器在构造时接入该通道;Rust 嵌入方应在启动服务前调用 Scheduler::connect_download_manager。每个被接纳的录制任务在启动前为 Started 和 Terminal 预留容量;接纳不会等待 Actor 应用消息,结束反馈在录制槽位释放后发布。Actor 世代与录制标识隔离延迟消息,新建的替代 Actor 会从录制所有者获取当前标识。
每个主播最多保留 32 个生命周期消息。全局预算为 1,024 个积压消息,加上运行期间最高录制总并发数的两倍(包括高优先级额外槽位)。提高并发时扩容,降低时保留已持有的预留容量。容量不足会返回可重试的 SchedulerFeedbackBusy 错误,并保留本地重新检查请求,不会将主播标记为离线或禁用。受管理的恢复任务在等待两个容量池时合并请求,不阻塞生命周期应用及预留容量释放;只有容量恢复后才重试。Actor 退出和关闭流程会取消此等待。旧 Actor 世代仍持有应用消息时,共享消息内容和预留容量继续保留。
配置解析最多使用八个受管理的工作任务。配置修订号和 Actor 世代会拒绝过期结果;邮箱压力下仍保留最新的目标配置,配置广播发生丢失时会重新对齐当前状态。进度仍为节流且允许丢失的广播。关闭流程先停止监控,再排空录制;剩余反馈明确归类为已停止、已退出或不可用,录制排空流程也会等待反馈任务结束。
事件驱动通信
跨服务协调主要依赖 Tokio broadcast:
| 事件流 | 发布者 | 典型消费者 | 备注 |
|---|---|---|---|
ConfigUpdateEvent | ConfigService、StreamerManager | Scheduler、ServiceContainer | 驱动 Actor 变更、运行时重配置与资源清理 |
MonitorEvent | StreamMonitor | ServiceContainer、NotificationService | 通过 DB outbox 发出,提高一致性 |
DownloadManagerEvent | DownloadManager | Scheduler、NotificationService、ServiceContainer 处理器 | 处理器将分段交给 PipelineManager,将终止结果交给 SessionLifecycle |
SessionTransition | SessionLifecycle | ServiceContainer 处理器、NotificationService | Ended 驱动清理与会话管道;恢复后的 Started 重启同一会话 |
DanmuEvent | DanmuService | ServiceContainer 处理器 | 处理器将分段配对交给 PipelineManager,将终止信号交给下载/会话处理 |
PipelineEvent | PipelineManager | NotificationService | 作业生命周期与可观测性 |
关于限流/节流
PipelineManager 内置可选的节流系统(ThrottleController)。若注入 DownloadLimitAdjuster,可以根据队列压力动态调节下载并发。
输出根写入门
下载管理器内置了一个输出根写入门(downloader::output_root_gate),它工作在文件系统边界上,作为运行在网络/进程边界上的引擎熔断器(circuit breaker)的互补机制。设计目标是:当文件系统出现单点故障(磁盘写满、绑定挂载失效、权限丢失)时,不让这次故障级联成数十次每主播的重试,淹没日志和数据库 outbox。
Healthy ──(record_failure:启动前 ENOENT / 运行时 ENOSPC / 启动探测)──► Degraded
│
(mark_healthy:下一次真实 ensure_output_dir 成功)│
Healthy ◄────────────────────────────────────────────────────────────────┘关键特性:
- 无锁热路径。在 Healthy 状态下,
check()只做一次原子加载加一次DashMap::get,没有互斥锁,也没有空跑成本。 - 基于 CAS 的单飞冷却。当根处于
Degraded时,每个冷却窗口(默认 30 秒)只允许一个调用方通过,去尝试真实的create_dir_all;其他并发调用方以缓存的错误快速拒绝。这借鉴了CircuitBreaker的 half-open 模式。 - 没有后台探测任务。真实的
ensure_output_dir调用本身就是探测——写入门复用实际的下载尝试作为探测信号。容器启动时会运行一次有界的一次性探测,以便在录制开始前发现失效的挂载点。 - 恢复钩子。在
Degraded → Healthy的切换时,写入门会清除所有因它而退避的主播的consecutive_error_count、disabled_until和last_error(通过"output-root blocked:"前缀过滤)。所有受影响的主播会在这次状态切换时解除退避。 - 每次状态切换只发出一条通知。
Healthy → Degraded的 CAS 同时也是决定"哪个调用方负责发出 critical 级output_path_inaccessible通知"的位置——无论有多少并发主播受影响,用户只会看到一条告警。 - 只有
ENOSPC会在录制途中进入写入门。写入器写满磁盘时,会在录制仍在进行的过程中把故障上报给写入门,使该根降级,从而对下一次启动进行限流。其他写入失败——文件系统只读、权限丢失、路径消失——会被归类为文件级故障,仍由引擎的CircuitBreaker处理;它们通过启动探测抵达写入门,只有在同时导致启动前钩子的create_dir_all失败时才会经由该钩子抵达。目录已存在但不可写时ensure_output_dir返回Ok,因此启动前钩子发现不了这种情况。之所以这样划分,是因为OutputRootUnavailable不计入熔断器,对这一类故障来说写入门是唯一的限流手段。
写入门在 /api/health 中以一个聚合的 output-root 组件暴露,列出所有 Degraded 根及其分类后的 io::ErrorKind、被拒绝次数和上次尝试的时间。参见通知系统文档了解事件形态,以及 Docker 故障排查了解挂载失效的失败模式。
服务所有权
已结束会话的延迟清理由会话生命周期服务持有。关闭时会取消并等待这些任务结束, 不必等待保留时间到期;清理不会移除后续新会话的当前会话索引。API 状态复用容器中的 仓库实例与配置导入服务。导入服务在运行时协调器创建后构造,而日志归档授权及归档 容量控制仍属于各自的 API 状态。
可观测性、健康检查与优雅退出
- 日志:使用
tracing,支持动态调整过滤器并带日志保留清理 - 健康检查:
GET /api/health/live(无鉴权,适合作为容器 liveness)GET /api/health与GET /api/health/ready需要有效 bearer token;未配置 JWT 鉴权时 返回401
- 退出:
- 独立后端把 SQLite、套接字和录制文件都限制在操作系统隔离的工作进程中。父进程的专用 线程会观测终止信号,即使启动或异步运行时工作阻塞,也会立即启用绝对关闭期限。看门狗会 持续运行,直到持久化标记更新、终止诊断和父进程退出全部完成。
ServiceContainer在工作进程内执行分阶段优雅关闭;在最终分段事实持久化之前, 必需的事件消费者会保持运行。- 所有支持的平台都通过
SIGINT触发优雅退出;Unix 还会处理SIGTERM。父进程和工作 进程都注册了信号处理,因此像KillMode=control-group或pkill那样直接打到工作进程 的信号,与经由控制管道转发的信号走同一条优雅收尾流程。工作进程内部的致命故障仍会让它 立即失败退出;父进程随后终止其后代并保留恢复状态。 - 被强制终止或崩溃的工作进程会在 SQLite 旁保留未清理运行世代标记,提醒下次启动可能 需要恢复。更早的恢复事项不会被之后正常退出的运行世代清除;该标记只负责检测, 并不负责重放文件。
- 标记只保留最早和最新的未清理世代,中间的世代记为数量,因此反复重启也不会让它无限 变大。启动和退出时的提示会写明还有多少个世代等待恢复。
后端 Rust 接口
主播状态的统一类型为 rust_srec::domain::StreamerState,包含 ERROR 和 DISABLED。数据库模型构造函数与 API 状态转换检查均使用此类型。StreamerState::can_transition_to 保留为状态转换校验入口;录制状态和错误退避由运行时服务持久化,不通过修改配置层的 domain::Streamer 实体来驱动。
会话和媒体数据使用 database::models 中的持久化模型;磁盘状态分类使用 HealthChecker::check_disk_space_with_thresholds。主播仓库保留 list_streamers(排除已标记删除的记录)和 list_all_streamers(包含这些记录,以便启动时完成退出与清理)。
工作流状态转换统一由 pipeline::DagScheduler 决定:Worker 通过 on_job_attempt_completed 或 on_job_attempt_failed 上报任务结果,由调度器决定该次尝试是重试、失败还是忽略;管理器通过 retry_dag 重试工作流,校验、重置、重启和回放都在这一个入口内完成。DagRepository 继续持有让每次转换持久化的受保护的事务,并提供 list_ready_steps 作为与完成事务共用的唯一就绪规则。
Actor 终止与唤醒策略
Actor 的邮箱执行、单次检查、可靠反馈、终止决定及在线唤醒决定分别位于独立模块。 批量检测委托保留原有控制方式,此次拆分不会启用它。只有明确的主播下播终止策略 才向 monitor 提交 Offline。正常完成、用户停止和未知停止原因恢复状态核验;应用 关闭和禁用则暂停轮询。基础设施阻断保留现有持久化及重试规则,反馈确认仍在副作用 应用完成后发送。
显式检查期限优先于在线 watchdog。在线且未安排检查的 actor,先选择 watchdog、 心跳停滞及时间窗口边界中的最早唤醒时间,再应用失败 watchdog 的最小重试间隔。 周期轮询保留 ±10% 抖动;智能唤醒、准入与立即检查期限继续保有各自约束。
Rust 调用方应使用 DownloadEndPolicy::Stopped(DownloadStopCause::User)。 线上的 DownloadStopCause::User 保持不变。StreamerActor 和 Supervisor 的元数据存储 构造参数现在使用 Arc<StreamerMetadata> 值。应通过 StreamerManager 获取共享 存储;actor 检查克隆不可变的 Arc 快照,不再复制配置 JSON。原有 actor 公共导出及 StatusChecker 的借用元数据输入保持不变。
独立后端关闭期限
父进程观测到 SIGINT 或 SIGTERM 时立即开始计时,即使此时启动准入或标记 I/O 仍在进行;期限覆盖工作进程清理和父进程退出。服务器会先请求隔离运行时进行优雅关闭;运行时自身的收尾预算也由这两个值推导(超时减去强制预留,再留出少量调度余量),因此调大超时会实际延长录制收尾阶段。进入强制预留阶段时,如果运行时仍在活动,服务器将终止整个受控进程树并以失败状态退出。退出状态 124 表示达到硬期限;125 表示最终的进程树终止请求本身失败。工作进程内部发生致命故障时会直接失败退出,不会进入无期限的优雅关闭。保留下来的标记表示启动时可能需要恢复;之后的正常关闭不会清除这笔更早的恢复事项。该标记本身不会重建在写入 SQLite 之前被中断的文件。只有在后端已停止且中断的文件已完成核对后,才能删除该标记。录制引擎会把自身的优雅停止等待时间限制在该预算的剩余部分内,因此单个引擎的停止超时即使长于关闭超时,也不会再导致引擎子进程在收尾过程中被强制结束。若关闭超出了宽限期但仍完成了全部收尾,进程仍按正常退出处理;只有无法收束的工作才会被记为崩溃。