Skip to content

fix(cloud): 落盘前把路由表里在线的主机记成此刻在线,最近在线与核心版本随落盘前进 - #197

Merged
Owlbay merged 3 commits into
mainfrom
fix/cloud-source-last-seen
Nov 2, 2026
Merged

Owlbay merged 3 commits into
mainfrom
fix/cloud-source-last-seen

Conversation

@Owlbay

@Owlbay Owlbay commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

变更摘要 / Summary

中文:修 #190。

根因:sources.last_seen_at_ms / core_version 只由 relay:presence 的上线、下线事件经 presenceRecorder.Observe 累计、每 5 分钟落盘。事件只在状态变化时发一次,主机一直连着就再没有事件,「最近在线」停在连上的那一刻(vps-43 测试部署上连了近一天的主机显示约 23.7 小时前)。

修法:

  • control.RouteLister.Online(ctx):路由表可选的枚举能力,Memory、Redis(SCAN route:src:* 后成批读值,跳过期间消失与损坏的键)、PostgreSQL(relay_routes 里未过期的行)都实现,合同测试同步。
  • presenceRecorder.Flush 先读一遍路由表,把当前持有隧道的源记成「此刻在线」并带上路由里的核心版本,再按原有逻辑落盘(只往后推、多实例取较大值、失败的条目留到下一轮)。
  • 时刻取读路由表之前:读到的路由此刻存在,它的下线事件一定不早于这个时刻,所以下线后「最近在线」停在下线事件的时刻,不会被推到之后。
  • 路由表读不到只记 warn,本轮仍落盘事件累计的部分;路由表实现不带枚举能力时退回只记事件。
  • newPresenceRecorder 的依赖改成 store.SourceStore、路由表与时钟,便于单测。

范围外:个人中转的 personal/store.go 有同样的「只在事件里记」的结构,但 #190 只报了 SaaS,本 PR 不动它。

English: Fixes #190.

Root cause: sources.last_seen_at_ms / core_version were only accumulated from the online/offline events on relay:presence (presenceRecorder.Observe) and flushed every 5 minutes. An event is emitted once per state change, so a host that stays connected emits nothing and its "last seen" stays at the connect time (a host connected for almost a day on the vps-43 test deployment showed ~23.7 h ago).

Fix:

  • control.RouteLister.Online(ctx): an optional capability of the route table, implemented by Memory, Redis (SCAN route:src:* then batched reads, skipping keys that vanished or are corrupt) and PostgreSQL (unexpired rows of relay_routes), with contract tests.
  • presenceRecorder.Flush first reads the route table and stamps every source that currently holds a tunnel as seen now, with the core version in its route, then flushes as before (forward-only, larger value wins across instances, failed entries retried next round).
  • The timestamp is taken before the route table is read: a route seen now exists now, so its offline event cannot be earlier than that moment, and after going offline "last seen" stays at the offline event's time and is never pushed past it.
  • If the route table cannot be read, only a warning is logged and the event-accumulated part is still flushed; if the implementation cannot enumerate, behaviour falls back to events only.
  • newPresenceRecorder now takes store.SourceStore, the route table and a clock, which keeps it unit-testable.

Out of scope: the personal relay's personal/store.go has the same events-only structure, but #190 only reports SaaS, so this PR leaves it alone.

关联 issue / Related issues

Closes #190

改动范围 / Scope

  • Go 实现(中继与控制面)/ Go implementation (relay and control plane)(go/)
  • docs(saas-m1-implementation.md、CHANGELOG)

契约与协议变化 / Contract and protocol changes

  • 契约节号 / Sections: 无 / None
  • 协议版本 / Protocol version: 无 / None
  • 生成物 / Generated files: 无 / None

数据库迁移 / Database migrations

无 / None(只读 relay_routes,不改表结构 / reads relay_routes only, no schema change)

测试 / Testing

先写测试再修:临时去掉 stampOnline 调用后,下列用例全部失败,恢复后通过。 / Tests first: with the stampOnline call temporarily removed these cases fail, and pass once restored.

  • cloud/server(单测):TestPresenceFlushStampsConnectedSources(一直在线的主机两次落盘之间 last_seen 前进、核心版本跟随,下线后停住)、TestPresenceOfflineStopsAtOfflineTime、TestPresenceFlushMovesForwardOnly、TestPresenceFlushDegrades(路由表读不到、写库失败留到下一轮)、TestPresenceWithoutLister。

  • cloud/server(真实 Redis + PostgreSQL):TestRedisRelayPresenceStampsConnectedHost。

  • control:Memory 的 Online;controltest 合同新增「Online 列出全部未过期的路由,别的节点写的也看得到」,Redis 与 PostgreSQL 两种实现各跑。

  • 单元测试 / Unit tests: pnpm go:check(gofmt、vet、staticcheck、go test -race、生成物新鲜度)通过,带本地一次性 PostgreSQL 17 与 Redis 8 容器,cloud/server、control/redis、control/postgres 都实际跑了,没有跳过。

  • 集成测试 / Integration tests: 未跑 pnpm test:integration

  • 跨仓端到端 / Cross-repo e2e: 未跑

  • 镜像 / Images: 未跑

  • pnpm check 与 / and pnpm repo:test

检查清单 / Checklist

  • 不涉及 app / 协议包 / 迁移 / 依赖;日志 msg 为简体中文短句、变量进字段 / No apps, protocol package, migrations or dependencies touched; log messages are short Simplified Chinese sentences with variables in fields
  • personal 缺省装配不依赖 PostgreSQL / Redis(只有 SaaS 的 Redis 控制面用到 Online)/ the default personal assembly still does not depend on PostgreSQL / Redis (only the SaaS Redis control plane uses Online)
  • 用户可见的变化已记进 CHANGELOG.md 的 Unreleased / Recorded under Unreleased in CHANGELOG.md

需要维护者提供的外部条件 / External prerequisites

无 / None

已知限制 / Known limitations

@Owlbay
Owlbay force-pushed the fix/cloud-source-last-seen branch from c0e2d48 to b2963ae Compare November 2, 2026 20:58
@Owlbay
Owlbay merged commit 62b05af into main Nov 2, 2026
6 checks passed
@Owlbay
Owlbay deleted the fix/cloud-source-last-seen branch November 2, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

主机一直在线时「最近在线」停在连上的时刻 / last_seen of a continuously connected host stays at its connect time

1 participant