Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ jobs:
HOST_CN: ${{ vars.HOST_CN }}
SITE_ALIAS: ${{ vars.SITE_ALIAS }}
BEIAN: ${{ vars.BEIAN }}
LIVE_POLLING_INTERVAL: ${{ vars.LIVE_POLLING_INTERVAL }}
WS_BASE: ${{ vars.WS_BASE }}
GTAG: ${{ vars.GTAG }}
BUILD_COMMIT: ${{ github.event.workflow_run.head_sha }}
run: pnpm run build
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Check protobuf generated files
run: pnpm run check:proto

- name: Run database migrations
run: pnpm run db:migration:run

Expand Down
31 changes: 31 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# RankLand 比赛领域

RankLand 对实时比赛、比赛事件及消费者恢复过程使用的统一领域语言。

## 实时比赛事件

**比赛事件流(Contest Event Stream)**:
一场比赛按确定顺序形成的状态变化序列。消费者可从已处理位置继续追赶该序列。
_Avoid_:比赛时间流、SSE 消息流

**事件流版本(Stream Revision)**:
比赛事件流的一代;新版本完整取代旧版本,版本之间的事件位置不可直接比较。
_Avoid_:SSE 版本、部署版本

**事件高水位(Event High-water Mark)**:
某一事件流版本中已经提交的最远事件位置。它表示消费者需要追赶到哪里,不包含比赛事件内容。
_Avoid_:最新 SSE 消息、推送序号

**事件可用通知(Event Availability Notification)**:
提示某场比赛的事件流高水位可能已经变化的可合并信号。它用于唤醒消费者,不承担比赛事件本身的交付正确性。
_Avoid_:比赛时间推送、实时事件推送、事件投递

**事件追赶(Event Catch-up)**:
消费者从本地已处理位置读取并应用已提交比赛事件,直至达到目标高水位的过程。
_Avoid_:SSE 重放

## 比赛展示

**比赛时钟(Competition Clock)**:
根据比赛起止时间和当前时间呈现的已用或剩余时长。它不属于比赛事件可用通知。
_Avoid_:事件高水位、SSE 时钟
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ pnpm run db:migration:generate
- `/search?kw=`:榜单搜索
- `/ranklist/:id`:单榜渲染,支持 `focus=yes` 或 `聚焦=是` 进入无站点 chrome 的聚焦模式
- `/collection/:id?rankId=`:合集页和侧边导航
- `/live/:id`:实时榜单,支持轮询和滚榜 WebSocket 配置
- `/live/:id`:实时榜单,通过 API v2 protobuf 事件补拉和 SSE 水位通知更新
- `/playground`:CSR-only SRK playground,包含 Monaco 编辑器和实时预览

RankLand 外部 API 配置:
Expand All @@ -123,14 +123,12 @@ VIN_URL=https://cdn.algoux.cn/rankland/vin.txt
HOST_GLOBAL=rl.algoux.org
HOST_CN=rl.algoux.cn
SITE_ALIAS=global
LIVE_POLLING_INTERVAL=10000
WS_BASE=wss://rl-api.algoux.cn
GTAG=
BEIAN=
```

`LEGACY_API_BASE_SERVER` 在 SSR 服务运行时读取;`LEGACY_API_BASE_CLIENT` 和 `VIN_URL` 会在 Vite 构建时写入浏览器包。
这两个地址只用于 `/rank/search`、`/ranking/*` 等尚未迁移的旧接口;contest、collection、statistics 与文件元数据统一使用同源 `/api/v2`。如果切换 legacy mock 或环境地址,需要重新运行 client build。
这两个地址只用于尚未迁移的 `/rank/search`;contest、collection、statistics、文件元数据与 live 事件统一使用同源 `/api/v2`。如果切换 legacy mock 或环境地址,需要重新运行 client build。

## 测试

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
status: accepted
---

# 使用 Redis Pub/Sub 分发比赛事件可用通知

比赛事件和事件高水位继续以 MySQL 为唯一事实源,SSE 只发送可合并的事件可用通知。跨实例分发采用“本机 Hub 即时通知 + Redis Pub/Sub 快速扇出 + 各实例针对本机活跃比赛周期校准 MySQL”,消费者仍通过 HTTP catch-up 获取真实事件;Redis 故障采用 fail-open,通知允许丢失、重复和乱序,但必须在约定窗口内收敛到权威高水位。

所有应用实例永久订阅一个带部署 namespace 和协议版本的全局频道;同一部署共享 namespace,不同环境必须不同,不能依赖 Redis logical database 隔离 Pub/Sub。实例只向本机当前有连接的比赛扇出,首期不做按比赛动态订阅。

首期不在 Redis 中维护比赛高水位快照,也不引入 Lua 或 Redis Function。subscriber 恢复后立即校准 MySQL,平时继续周期校准本机活跃比赛;只有实测该权威查询成为瓶颈时,才把 Redis 快照作为可丢弃的恢复加速器重新评估。

本轮多机支持指多个应用实例共享同一 MySQL 与当前单端点 Redis 接入,不扩展应用的 Sentinel/Cluster 连接配置。分发模块保留可替换边界,Redis 自身拓扑另行演进。

通知平面整体采用 fail-open:MySQL 已提交后,本机 Hub、序列化、日志或 Redis publisher/subscriber 的任何失败都不能改变业务成功响应。Redis 不可用时应用实例仍保持 ready,现有健康检查不因通知加速层降级而失败;实例继续提供本地通知与 MySQL 校准,并通过限频结构化日志暴露状态变化和失败。

内部连接路由与 Redis envelope 使用数据库 `contestId` 作为比赛身份,不直接用请求 `uk` 字符串作为跨实例/Hub key,避免 MySQL collation 与大小写变体产生不同连接桶。公开 payload 仍使用各 SSE client 建连时的请求 `uk`,不改变既有 wire 语义。

Redis 健康时以 MySQL commit 后 1 秒内跨实例通知为目标;故障路径每 5 秒批量校准本机活跃比赛,在 MySQL 健康且查询于 1 秒内完成时以 6 秒为验收上限。周期校准无条件运行,不能只在 Redis 重连后执行,因为写实例可能在 commit 后、publish 前退出。

首次发布采用协调式整批切换,优先 blue-green;新实例组先验证订阅 ACK、跨实例通知和校准,再把切换 nginx upstream 与立即停止/drain 旧实例作为一个发布动作。旧实例活跃 SSE 归零前不宣称新保证生效;新旧版本混部不作为受支持发布形态。

不选择 Redis Streams 或 transactional outbox,因为当前通知不承载不可恢复的业务数据,Streams 不能消除 MySQL commit 与 Redis 写入之间的双写间隙,共享 consumer group 也不符合向每个实例广播的需求。若未来通知变为不可合并的业务事件,或不能接受一个校准周期的故障延迟,再重新评估持久消息日志或 MySQL outbox。
9 changes: 6 additions & 3 deletions docs/contest-event-notification-multi-instance-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,15 +318,18 @@ nginx 到应用、应用到 Redis/MySQL 的网络都必须允许多机互通。

首期不引入新的 metrics 依赖,使用稳定的结构化日志事件:

- `contest_event_notification.redis_state`
- `contest_event_notification.publish_failed`
- `contest_event_notification.invalid_message`
- `contest_event_notification.reconcile_failed`
- `contest_event_notification.backpressure_timeout`

状态变化立即记录;同类重复错误每 30 秒最多输出一次,并在下一条中带 `suppressedCount`。恢复时输出一条 recovery/state 日志。日志至少包含 channel、Redis state、active contest 数量和 error class,不输出密码或完整连接串。
故障类日志默认输出;同类重复错误每 30 秒最多输出一次,并在下一条中带 `suppressedCount`。恢复时输出一条
recovery 日志。设置 `CONTEST_EVENT_RUNTIME_DIAGNOSTICS_ENABLED=true` 后,额外输出
`contest_event_notification.redis_state` 状态变化。日志至少包含 channel、Redis state、active contest
数量和 error class,不输出密码或完整连接串。

Redis 故障不改变 readiness。生产告警应基于 Redis state/publish failure/reconcile failure 日志,而不是摘除所有实例。
Redis 故障不改变 readiness。生产告警应默认基于 publish failure/reconcile failure 等故障日志;仅在显式
启用运行时诊断时才依赖 Redis state 日志。告警不应直接摘除所有实例。

## 13. 故障矩阵

Expand Down
Loading
Loading