Etherpad Auto-Update Tier 4:基于维护窗口(Maintenance Window)的全自主升级实现解析

📅 发布时间:2026/9/21 18:51:48
Etherpad Auto-Update Tier 4:基于维护窗口(Maintenance Window)的全自主升级实现解析
后端协同办公WebSocket前端富文本【免费下载链接】etherpadEtherpad: A modern really-real-time collaborative document editor.项目地址https://gitcode.com/gh_mirrors/et/etherpad点击查看免费下载Etherpad 的自更新子系统Auto-Update为实例管理员提供了一套分层的升级策略从notify仅通知到manual手动点击、auto宽限期后自动最终到autonomous在维护窗口内全自主升级。本文以仓库中的实施计划 docs/superpowers/plans/2026-05-15-auto-update-pr4-tier4-autonomous.md 为核心骨架结合已经落地的 MaintenanceWindow.ts、UpdatePolicy.ts、Scheduler.ts 等源码与测试系统讲解 Tier 4 的窗口模型、配置方法、调度逻辑、失效降级路径与运行验证方式。读完本文你将能够为 git 安装的 Etherpad 配置updates.maintenanceWindow并开启 Tier 4理解跨午夜、DST、UTC/local 等边界情况下窗口计算的确切语义掌握canAutonomous策略门控与“错过窗口即顺延”的调度行为并通过状态接口与冒烟手册独立验证全自主升级链路。一、Tier 4 在整个自更新分层中的位置Etherpad 的更新策略在 src/node/updater/types.ts 中定义了五个层级updates.tier含义写入型能力off完全关闭更新子系统无notify仅检测新版本并提示无manual管理员在/admin/update手动点击 ApplycanManualauto检测到新版本后在宽限期grace结束自动应用canAutoautonomous在auto之上叠加维护窗口仅窗口内自动应用canAutonomous从源码结构看所有写入型层级manual/auto/autonomous都要求安装方式为git——UpdatePolicy.ts 中WRITABLE_METHODS集合当前仅包含git其他安装方式docker、npm、managed会被静默降级为notify行为。Tier 4 不是一套独立的更新管线而是对 Tier 3 调度器的窗口化约束宽限期逻辑、邮件去重、排水drain与回滚全部复用唯一新增的是“什么时刻允许触发”这一层门控。实施计划的目标非常明确当检测到新版本且updates.tier autonomous时只有在now()位于配置的维护窗口内才启动排水窗口外发现的更新顺延到下一个窗口开启时刻管理端 UI 增加窗口选择器start/end 的 HH:MM、tz 选择、校验提示与“下次窗口开启时间”预览。二、维护窗口的数据模型与解析校验2.1 配置结构MaintenanceWindow类型定义在 src/node/updater/types.tsexport interface MaintenanceWindow { start: string; // 墙钟 HH:MM24 小时制 end: string; // 墙钟 HH:MM24 小时制end 为排他exclusive tz: local | utc; // 按宿主本地时钟还是 UTC 解释 start/end }配置入口位于settings.json的updates段默认值为null即关闭 Tier 4{ updates: { tier: autonomous, preApplyGraceMinutes: 15, maintenanceWindow: { start: 03:00, end: 05:00, tz: local } } }仓库根目录的 settings.json.template以及settings.json.docker在maintenanceWindow键上方的注释中给出了完整语义end排他end start表示跨午夜窗口。默认null意味着tier: autonomous但未配置窗口时策略会降级详见第五节。2.2 纯函数校验parseWindow实施计划要求校验逻辑做成可在策略、UI、状态接口中复用的纯函数落点即 src/node/updater/MaintenanceWindow.ts 导出的parseWindowconst HHMM /^([01]\d|2[0-3]):([0-5]\d)$/; export const parseWindow (raw: unknown): MaintenanceWindow | null { if (!raw || typeof raw ! object) return null; const r raw as Recordstring, unknown; if (typeof r.start ! string || typeof r.end ! string) return null; if (r.tz ! local r.tz ! utc) return null; const s toMinutes(r.start); const e toMinutes(r.end); if (s null || e null) return null; if (s e) return null; return {start: r.start, end: r.end, tz: r.tz}; };校验规则汇总start/end必须是形如HH:MM的字符串正则/^([01]\d|2[0-3]):([0-5]\d)$/限定了 00:00–23:59 的合法范围tz只能是local或utcstart end视为零长度窗口拒绝任何结构或格式失败统一返回null调用方将其解释为“Tier 4 关闭回退到 Tier 3”。单元测试 src/tests/backend-new/specs/updater/MaintenanceWindow.test.ts 覆盖了parseWindow的接受/拒绝矩阵合法同日窗口、合法跨午夜窗口、畸形 start/end、start end、未知 tz、非对象/缺字段等。2.3 启动期校验与日志在 src/node/utils/Settings.ts 的updates类型中maintenanceWindow默认值为null。实施计划明确启动时对窗口形状做校验非法时通过 log4js 的updater分类记录警告并视同null绝不因配置错误导致启动崩溃。冒烟手册 docs/superpowers/specs/2026-04-25-auto-update-runbook.md 第 12 节给出了可复现的验证方式配置{start:oops,end:05:00,tz:local}后重启日志应出现updater: ignoring malformed updates.maintenanceWindow (...)同时policy.reason变为maintenance-window-invalid。三、窗口计算的核心语义inWindow与nextWindowStartMaintenanceWindow.ts 文件头部的注释是理解整个模块的关键它明确了两条时间语义约定窗口是“墙钟”对而不是 UTC 偏移区间。tz: utc用getUTCHours/Minutes与Date.UTC(...)计算tz: local用宿主的本地墙钟。DST 迁移被 JSDate构造器的归一化吸收——例如春季拨快日一个不存在的02:30会静默落在本地03:30这是文档化的行为而非缺陷。end分钟在同日与跨午夜两种情况下都是排他的22:00-02:00窗口实际匹配[22:00, 24:00) ∪ [00:00, 02:00)。3.1inWindow(now, window)把当前时间折算为所选时区的“分钟数”0–1439再按同日/跨午夜两种区间判断export const inWindow (now: Date, window: MaintenanceWindow): boolean { const s toMinutes(window.start); const e toMinutes(window.end); if (s null || e null || s e) return false; const m wallMinutes(now, window.tz); return s e ? (m s m e) : (m s || m e); };要点同日窗口03:00-05:0003:30 在内02:59 在外05:00 整点在外end排他跨午夜窗口22:00-02:0023:00 与 01:00 都在内02:00 与 21:59 在外测试还专门以TZAmerica/Los_Angeles环境运行tzutc用例证明 UTC 窗口不随宿主时区漂移。3.2nextWindowStart(now, window)返回“大于等于now、且在所选时区中墙钟等于window.start的最早时刻”export const nextWindowStart (now: Date, window: MaintenanceWindow): Date { const s toMinutes(window.start); if (s null) return now; const isUtc window.tz utc; const year isUtc ? now.getUTCFullYear() : now.getFullYear(); const month isUtc ? now.getUTCMonth() : now.getMonth(); const day isUtc ? now.getUTCDate() : now.getDate(); const todayStart buildAt(year, month, day, s, window.tz); if (todayStart.getTime() now.getTime()) return todayStart; return buildAt(year, month, day 1, s, window.tz); };两个容易误用的边界测试与源码注释都做了显式约定如果now已经在窗口内nextWindowStart返回明天的开启时刻而不是折叠到now——因为“立刻触发”由调用方用inWindow单独把关两个函数职责分离DST 春令时America/New_York2026-03-08窗口 02:30-03:30 localnow 2026-03-08T06:00:00Z时nextWindowStart解析到下一个墙钟 02:30而该时刻在 DST 当天实际就是本地 03:30冬令时2026-11-01窗口 01:30-02:30 local则取墙钟 01:30 的第一次出现。测试文件中均以注释记录了这些预期。四、策略门控canAutonomous与失败原因策略计算是“本环境允许做什么”的唯一权威实现在 src/node/updater/UpdatePolicy.ts 的evaluatePolicy纯函数中。它接收PolicyInput含installMethod、tier、current、latest、executionStatus、maintenanceWindow返回PolicyResultlet canAutonomous false; let windowReason: string | null null; if (!terminal tier autonomous) { if (maintenanceWindow null) { windowReason maintenance-window-missing; } else if (parseWindow(maintenanceWindow) null) { windowReason maintenance-window-invalid; } else { canAutonomous true; } }canAutonomous为true需要同时满足全部条件安装方式为git可写tier autonomous没有终端态executionStatus rollback-failed此时canAuto也被拒绝必须由管理员先 Acknowledge 干预updates.maintenanceWindow非空且parseWindow校验通过。两个关键降级行为源码注释与 doc/admin/updates.md 的“Tier 4”一节相互印证窗口缺失时reason maintenance-window-missing窗口畸形时reason maintenance-window-invalid但两种情况下canAuto与canManual仍保持开启——即行为等价于 Tier 3auto自治更新被静默禁用但绝不整体瘫痪管理端 UI 会通过横幅把这种“配置缺失导致降级”显式暴露出来避免管理员误以为 autonomous 生效。PolicyResult的reason枚举完整集合见源码注释tier-off | up-to-date | install-method-not-writable | rollback-failed-terminal | maintenance-window-missing | maintenance-window-invalid | ok。单元测试 src/tests/backend-new/specs/updater/UpdatePolicy.test.ts 覆盖了三种窗口相关的策略结果。五、调度器窗口门控的两次检查点窗口约束在 src/node/updater/Scheduler.ts 中落为两个分离的检查点——排程时一次、触发时一次中间隔着可能长达数小时的宽限期。5.1 排程时把scheduledFor吸附到下一个窗口开启时刻decideSchedule在既有 Tier 3 逻辑scheduledFor now clamp(preApplyGraceMinutes)其中 grace 被钳制在[0, 7*24*60]分钟之后叠加 Tier 4 逻辑const graceMs clampGrace(preApplyGraceMinutes) * 60 * 1000; let scheduledForDate new Date(now.getTime() graceMs); // Tier 4: snap forward to the next opening if grace lands outside the window. if (policy.canAutonomous maintenanceWindow) { if (!inWindow(scheduledForDate, maintenanceWindow)) { scheduledForDate nextWindowStart(scheduledForDate, maintenanceWindow); } }测试覆盖的典型场景canAutonomous 窗口 03:00-05:00 now 10:00→scheduledFor吸附到次日 03:00而不是now gracecanAutonomous 窗口 03:00-05:00 now 03:30grace 0→scheduledFor now窗口内不吸附。邮件机制完全不受影响grace-start邮件仍然每个 tag 只发一次defer 不会触发新的 grace-start 邮件。5.2 触发时窗口关闭则deferdecideTriggerApply在计时器到点后重新检查当前时间。由于从 arm 到 fire 之间可能发生管理员取消、点击 Apply now、切换 tier、宿主挂起等事件触发时的复查是必须的if (policy.canAutonomous maintenanceWindow now !inWindow(now, maintenanceWindow)) { return { action: defer, nextStart: nextWindowStart(now, maintenanceWindow).toISOString(), reason: outside-maintenance-window, }; } return {action: fire};TriggerApplyDecision判别联合因此扩展为fire/abort/clear-schedule/defer。defer 语义窗口已关闭长宽限期、时钟偏移、宿主休眠恢复等本次不触发返回下一个开启时刻。5.3 运行器defer 后重新武装re-armSchedulerRunner的arm()是幂等的——重新武装会先清除旧计时器。在 src/node/updater/index.ts 的schedulerTriggerApply回调中defer 分支的处理是if (decision.action defer) { logger.info(scheduler deferred ${targetTag} to next maintenance window at ${decision.nextStart}); const sched state.execution.status scheduled ? state.execution : null; if (sched) { await saveState(stateFilePath(), { ...state, execution: {...sched, scheduledFor: decision.nextStart}, }); scheduler?.arm({targetTag, scheduledFor: decision.nextStart}); } return; }这里的关键设计是以持久化的var/update-state.json为唯一事实源defer 时把新的scheduledFor写盘再重新 arm这样即使进程在间隙中重启启动时也能通过rehydrating Tier 3 schedule ... at scheduledFor逻辑把计时器恢复见expressCreateServer中state.execution.status scheduled的重建分支。5.4 排程循环中的数据流performCheck周期检查在每次 tick 中先evaluatePolicy得出PolicyResult再把maintenanceWindow与策略一起传给decideSchedule只有canAutonomous为真时才传入解析后的窗口对象const decision decideSchedule({ state, now, policy, latest: state.latest, current, preApplyGraceMinutes: Number(settings.updates.preApplyGraceMinutes) || 0, adminEmail: settings.adminEmail, maintenanceWindow: policy.canAutonomous ? parseWindow(settings.updates.maintenanceWindow) : null, });另外注意performCheck的两个保护tier off直接短路checkInFlight防止并发 tick 竞态写盘与重复发信。六、状态接口把窗口状态暴露给管理端src/node/hooks/express/updateStatus.ts 的GET /admin/update/status响应在 Tier 4 中新增了两个字段const parsedWindow parseWindow(settings.updates.maintenanceWindow); const maintenanceWindow isAdmin ? parsedWindow : null; const nextWindowOpensAt isAdmin parsedWindow settings.updates.tier autonomous ? nextWindowStart(new Date(), parsedWindow).toISOString() : null;设计细节maintenanceWindow与nextWindowOpensAt仅在已认证管理员会话中返回未认证请求二者均为null——窗口配置属于运维敏感信息nextWindowOpensAt在请求时刻实时计算tier 为autonomous且窗口可解析时供管理端 UI 渲染“下次窗口开启于……”该接口默认开放requireAdminForStatus: false但execution/lastResult等诊断字段同样做了非管理员脱敏只保留状态枚举。七、管理端 UI窗口选择器与“顺延中”面板实施计划定义了完整的前端改动对应 admin/src/pages/UpdatePage.tsx、admin/src/components/UpdateBanner.tsx、admin/src/store/store.ts 与新增 admin/src/components/MaintenanceWindowPicker.tsxtier autonomous时渲染MaintenanceWindowPicker受控组件值为{start, end, tz} | null内联校验提示使用 i18n 键update.window.validation.format/update.window.validation.equal下方通过 prop 传入的nextWindowOpensAt渲染“下次窗口开启于……”execution.status scheduled且scheduledFor now时scheduled 面板追加顺延副标题update.page.scheduled.deferred_until即“窗口外。更新将在窗口开启时开始……”policy.reason为maintenance-window-missing/maintenance-window-invalid且 tier 为autonomous时顶部横幅渲染“Autonomous updates are disabled until a maintenance window is configured.”并链接回/admin/update所有文案必须走 i18nsrc/locales/en.json 新增update.window.*、update.page.policy.autonomous_*等键严禁硬编码。Playwright 测试 src/tests/frontend-new/admin-spec/update-autonomous.spec.ts 验证四条路径picker 保存后刷新恢复非法输入展示校验消息且不保存窗口外排程面板展示“Next window opens at HH:MM (local)”窗口缺失时横幅渲染/admin/update链接。八、边界与降级行为全景将计划、文档与源码三方对照Tier 4 的边界语义可以归纳为一张行为表场景行为tierautonomous窗口未配置canAutonomousfalsereasonmaintenance-window-missing降级为 Tier 3canAutotrueUI 横幅提示tierautonomous窗口畸形启动期日志警告并视同nullreasonmaintenance-window-invalid同样降级窗口外检测到新版本scheduledFor吸附到下一个窗口开启时刻不立即排水窗口内触发grace0直接 fire走 Tier 2/3 既有管线drain → executor → exit 75 → 监督进程重启计时器到点但窗口已关闭decideTriggerApply返回defer写盘新scheduledFor并重新 arm无排水、无退出窗口内但处于rollback-failed终端态canAutonomous与canAuto均被拒绝必须管理员 Acknowledge安装方式非 git写入型层级整体不可用降级为 notify关于跨午夜与 DST 的实践建议出自 doc/admin/updates.md跨 DST 边界的宿主优先使用tz: utc——窗口每天都锚定同一墙钟不随本地时区偏移变化tz: local的春令时缺失时刻由 JSDate构造器归一化到下一个有效时刻冬令时取第一次墙钟出现跨午夜窗口最多覆盖 24 小时end排他更长的“窗口”应拆成多个配置或直接改用 Tier 3。九、验证与运行手册9.1 自动化验证命令实施计划给出了四层验证矩阵全部可在仓库内复跑# 类型检查根 admin pnpm exec tsc --noEmit # 后端新测试vitest含 MaintenanceWindow / UpdatePolicy / Scheduler pnpm exec vitest run src/tests/backend-new/specs/updater/ # 后端集成测试mocha含窗口边界集成 pnpm exec mocha src/tests/backend/specs/updater-*.ts # 管理端 UIPlaywright端口 9003 pnpm --filter ep_etherpad-lite exec playwright test src/tests/frontend-new/admin-spec/update-autonomous.spec.ts # 前端构建 pnpm run build:ui窗口边界集成测试 src/tests/backend/specs/updater-window-integration.ts 覆盖四个场景窗口外发现新版本仅排程不排水进入窗口后 fire 并启动排水deferred-grace 期间取消返回idle窗口在宽限期内关闭则 defer 且不丢失排程。9.2 冒烟手册Tier 4 一节docs/superpowers/specs/2026-04-25-auto-update-runbook.md 第 12 节给出了针对一次性 VM 的完整冒烟流程核心步骤缺失窗口横幅tier: autonomousmaintenanceWindow: null访问/admin/update应见“Autonomous updates are disabled until a maintenance window is configured.”且policy.reason maintenance-window-missing畸形窗口{start:oops,end:05:00,tz:local}日志出现忽略警告policy.reason maintenance-window-invalid窗口外顺延把窗口设在 5 分钟后如 14:00 时配置{start:14:05,end:14:10,tz:local}强制检出旧 tag 触发新版本检测确认execution.status变为scheduled且scheduledFor位于窗口开启时刻而非now gracescheduled 面板同时显示倒计时与“Outside maintenance window…”说明窗口开启即触发等到窗口打开调度器 fire走 drain → executor → exit 75 → systemd 重启状态最终落在verified窗口中途关闭配置一个在now grace前关闭的窗口如{start:14:01,end:14:02,tz:local}配preApplyGraceMinutes: 5计时器到点时窗口已关闭日志出现updater: scheduler deferred ... to next maintenance window at ...var/update-state.json中出现约 24 小时后新的scheduledFor且无排水、无退出、无应用。实施计划要求冒烟在第 5 步基础上额外验证“窗口内回滚路径仍然可用”并将这些条目并入总签核清单。十、变更与演进轨迹实施计划把 Tier 4 拆为 8 个任务设置 schemaTask 1→ 窗口模块与单测Task 2→ 策略扩展Task 3→ 调度门控Task 4→ 运行器与状态接口接线Task 5→ 管理端 UITask 6→ 窗口边界集成测试Task 7→ 文档/运行手册/CHANGELOGTask 8并有跨任务总检查。PR 命名为feat(updater): tier 4 — autonomous update in maintenance window (#7607)并在文档 doc/admin/updates.md 中将 Tier 4 从“designed, not yet implemented”翻转为已实现状态。截至当前仓库状态MaintenanceWindow.ts、UpdatePolicy.ts、Scheduler.ts、updateStatus.ts 及对应的三个 vitest 测试文件均已存在settings.json.template已包含maintenanceWindow键——说明计划主体已经落地本文描述的即为仓库内真实可用的行为。若需从零体验可在一次性 VM 上按运行手册第 0–1 节搭建 systemd 托管的 git 安装git clonecorepack enablepnpm installpnpm run build:ui再按第九节配置与验证。赞分享后端协同办公WebSocket前端富文本【免费下载链接】etherpadEtherpad: A modern really-real-time collaborative document editor.项目地址https://gitcode.com/gh_mirrors/et/etherpad点击查看免费下载相关推荐Wails v3 Window API 实战基于 window 示例掌握 WebviewWindow 窗口控制全能力Wails v3 Window API 实战基于 window 示例掌握 WebviewWindow 窗口控制全能力 本文以 Wails v3 仓库中的 v3桌面应用跨平台CLI前端Fleet 维护窗口Maintenance Windows完全指南让修复操作自己走进用户的日历Fleet 维护窗口Maintenance Windows完全指南让修复操作自己走进用户的日历 Fleet 的维护窗口Maintenance Win后端前端企业应用运维网络安全ECC Auto Update 自动更新机制全解析基于 install-state 的安全重装流程ECC Auto Update 自动更新机制全解析基于 install state 的安全重装流程 ECCEverything Claude Code /人工智能AI 技能AI 插件AI 评测Agent 评测MCP Clients开发工具上一篇OpenAI GPT 1 vs GPT 2一文读懂两代语言模型的核心差异与演进下一篇GitHub Audio高级技巧利用事件过滤功能专注特定项目活动 创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考