Repository Wiki
jason5ng32/MyIP

外部服务状态查询与后台轮询

本功能定期读取外部服务商的状态页,将服务状态、子服务组件和近期事件归一化为进程内快照,再通过概览与详情接口提供给页面。访客请求不触发上游查询,因此上游请求量与页面访问量解耦。

目的与范围

本文覆盖服务商注册、后台初始化与定时刷新、Statuspage / incident.io 数据转换、故障降级、内存快照及 HTTP 读取接口。重点面向排查“状态为何未更新”“某家服务详情为何缺失”和新增服务商的开发者。

网络连通性探测、DNS 诊断、通用缓存中间件、全站启动框架和 Sentry 配置不属于本文范围。运行时未提供同级目录的具体链接,因此不构造未验证的 Wiki 路径。前端组件代码、通用请求超时实现和测试文件本次未读取;下文仅说明已验证的后端行为。

概述

实现分为两条彼此独立的路径:

  • 写入路径:启动时调用 bootstrapServiceStatus(),随后 startServiceStatusPolling() 每 5 分钟发起刷新;每家服务同时请求 summary.json 和 incidents.json,处理失败与旧值回退后整体发布快照。
  • 读取路径:概览只提取 id、name、page、indicator;详情返回单家服务的 components 与 incidents。处理器不执行网络请求。

当前注册 16 家服务:Claude、ChatGPT、Cursor、GitHub、ElevenLabs、Cloudflare、Groq、Notion、Vercel、Netlify、Render、Supabase、Discord、Figma、Linear 和 Stripe。它们使用兼容的 /api/v2/summary.json 与 /api/v2/incidents.json 数据接口。这里呈现的是服务商状态页报告,不是从用户浏览器到服务商的实时连通性测试。

依据:service-status-providers.js、service-status.js。

架构

Loading diagram...

Sources: backend-server.js、service-status-store.js、service-status-transform.js、service-status.js。

这种分层将网络失败与数据形状转换分开:轮询器决定是否使用旧数据,纯函数负责字段裁剪和默认值,接口只负责输出已缓存数据。概览与详情拆开可以避免首次读取携带全部组件和事件;详情将组件与事件合并返回,减少展开单家服务时的请求数。

初始化、轮询与快照发布

1. 初始状态与就绪判断

模块初始化时,snapshot 为 { updatedAt: null, providers: [] },schedulerStarted 为 false。isServiceStatusPrimed() 仅判断 updatedAt !== null。

这意味着 primed 表示至少完成过一轮快照发布,不表示所有上游均查询成功。首次刷新即使所有服务都被归一化为 unknown,只要刷新流程完成,就会写入时间戳并视为就绪。

bootstrapServiceStatus() 等待一次刷新,成功记录 Service status cache primed;发生异常则记录警告而不向外重新抛出。首次失败时快照仍为空,后续定时任务可以继续填充。若手工再次调用初始化函数,失败也不会清空已有快照。

依据:service-status-store.js、service-status-store.js。

2. 单家服务的双端点请求

fetchJson(url) 调用 fetchUpstream(url),对非成功 HTTP 响应抛出消息为 upstream ${res.status} 的 Error,否则调用 res.json()。网络错误和 JSON 解析错误也会让此 Promise 拒绝。

refreshProvider(provider, previous) 使用 Promise.allSettled 同时等待摘要与事件接口。两个请求的失败分别转换为 null,因此摘要失败不会直接取消事件请求,事件失败也不会阻止读取摘要。

javascript
1 const [summary, incidents] = await Promise.allSettled([ 2 fetchJson(`${provider.api}/api/v2/summary.json`), 3 fetchJson(`${provider.api}/api/v2/incidents.json`), 4 ]); 5 const summaryJson = summary.status === 'fulfilled' ? summary.value : null; 6 const incidentsJson = incidents.status === 'fulfilled' ? incidents.value : null;

Source: service-status-store.js。

3. 整批并发与整体替换

每轮刷新开始时,从当前快照建立 prevById,再并发刷新所有配置中的服务商。只有所有服务的刷新 Promise 均完成,才给 snapshot 赋新值。

javascript
1export async function refreshServiceStatus() { 2 const prevById = new Map(snapshot.providers.map((p) => [p.id, p])); 3 const providers = await Promise.all( 4 STATUS_PROVIDERS.map((p) => refreshProvider(p, prevById.get(p.id))), 5 ); 6 snapshot = { updatedAt: new Date().toISOString(), providers }; 7 return snapshot; 8}

Source: service-status-store.js。

由此产生几个重要语义:

  • 刷新期间,读请求继续获得上一份完整快照,而不是半更新的服务列表。
  • 返回的服务顺序遵循 STATUS_PROVIDERS,不是网络响应完成顺序。
  • updatedAt 是整轮发布的时间,而不是每家服务最后一次成功获取数据的时间。
  • 最慢的服务请求影响整轮发布时间;某家返回的旧值也可能随本轮新时间戳发布。
  • 如转换阶段意外抛出异常,Promise.all 拒绝,本轮不执行快照赋值;已发出的其他请求并不会因该拒绝而自动取消。

4. 后台调度生命周期

startServiceStatusPolling() 用模块级布尔值防止重复创建定时器。它本身不立即刷新,而是注册 setInterval;首次填充由初始化函数承担。定时器调用 .unref?.(),避免它单独维持 Node.js 进程存活。

每次定时执行通过 withCronMonitor('service-status-refresh', ...) 包装,传入 5 分钟的 interval 调度以及 checkinMargin: 5、maxRuntime: 5。包装调用拒绝时,记录 service-status refresh tick failed。这些监控参数不能被当作网络请求超时或强制取消机制;工具内部实现本次未读取。代码注释说明未配置后端 DSN 时监控为 no-op。

依据:service-status-store.js。

故障降级的实际控制流

Loading diagram...

Source: service-status-store.js。

情况发布结果需要注意的语义
摘要获取失败,旧 indicator 不是 unknown返回整个旧服务条目即使本轮事件成功,也不会合并新事件,因为已经提前返回
摘要获取失败,没有旧条目或旧值为 unknown继续归一化,摘要成为 unknown、组件为空新事件若获取成功仍可使用;记录警告
摘要成功,事件获取失败,旧事件非空使用新摘要和旧事件组件状态和事件可能来自不同刷新轮次
事件接口成功返回空数组使用空事件列表不恢复旧事件,因为 incidentsJson 对象仍为真值
摘要响应对象存在,但缺少 status.indicator转换为 unknown不触发“摘要获取失败”的旧值回退

所谓 last-known-good 的判断仅是 previous.indicator !== 'unknown',并非仅保留正常的 none。旧值为 major 或 critical 也会保留。这可以减少瞬时网络抖动导致的状态闪烁,但没有最大陈旧时间;持续失败时,旧状态可能跨多轮保留。

javascript
1 if (!summaryJson) { 2 const hadGood = previous && previous.indicator !== 'unknown'; 3 if (hadGood) { 4 logger.debug({ provider: provider.id }, 'service-status refresh blip; serving last-known-good'); 5 return previous; 6 } 7 logger.warn({ err: summary.reason, provider: provider.id }, 'service-status summary fetch failed (no prior data)'); 8 }

Source: service-status-store.js。