数据流
状态:Current — 当前实现/规范,可作为实现依据。
本文档只描述当前目标架构下的主要调用流,不再使用历史 gRPC/旧传输 回拨 模型。
1. 页面发布与受控执行(vNext 主路径)
运营页面的执行主路径经过发布快照,而不是直接调用函数目录:
要点:
- 浏览器只提交
bindingId与 selector context(form/row/selection/page state),不传 functionId、target、gameId、env。 - 发布快照冻结函数版本、schema digest、risk、permission、approval 与 renderer 版本;函数重注册只产生新 Proposal 与 stale 诊断,绝不静默改写已发布页面。
- 合同变化后的恢复路径:
GET /api/v1/versioning/pages/:pageKey/diff→ 三方合并(展示字段自动合并、执行字段人工决策)→ 重新发布。 - 函数直调
POST /api/v1/functions/:id/invoke仍存在,但定位是调试/管理面路径,不是运营页面主路径。
2. Server 到业务函数(dispatch)
要点:
Server -> Agent不再新开一条回拨连接Agent -> App由本地 session 边界处理- 两段链路都遵循相同的 session runtime 思维
invoke 路由:Provider/Instance 双索引
当一个 Agent 后挂多个游戏服务(service)时,Agent 内部按 Nacos 风格的双层索引选择目标实例:
- 注册期:SDK 的
ProviderConnectRequest携带service_id与Metadata(proto 字段sdk_language/sdk_version/game_id/env等)。Agent 以functionId → serviceId → []Instance双层索引登记,实例带LastSeen用于健康判断(internal/platform/agentlocal/store.go)。 - 调用期:
pickInstance(internal/agent/local_handler.go)先按functionId取 service 索引;若 invoke metadata 带serviceId则精确落到该 service 的实例集合,否则合并该函数下所有 service 的实例;再按LastSeen过滤健康实例并做负载均衡。 Instance.Metadata负责透传 SDK 元信息(键为sdkLanguage/sdkVersion/gameId等小驼峰),最终经AgentProcess → ProviderSession暴露到 opsNodes。
3. SDK 注册到 Agent
4. Agent 注册到 Server
SDK 滑动版本门槛(函数注册)
RegisterRequest.processes 各进程自报 sdk_language/sdk_version。Server 按 (game_id, env, sdk_language) 记录历史最高 SDK 版本(高水位,落各 game 库 sdk_version_highwatermarks 表,编号迁移 0028),注册时三档判定(internal/platform/registry/sdkversion):
| 判定 | 条件 | 行为 |
|---|---|---|
| 放行 | 版本 ≥ 高水位 | 注册成功后抬升高水位 |
| 放行 + 警告 | 同 major 且落后 1 个 minor | 函数照常注册,写 sdk_version_behind 注册警告 |
| 拒绝 | 落后 ≥2 个 minor 或 ≥1 个 major | 该进程独占声明的函数不进本次注册(会话函数表/契约均不物化),写 sdk_version_floor_rejected 警告 |
细节语义:
- 拒绝粒度是函数不是连接:被拒进程的 provider 记录仍进会话(进程存在是事实;调度候选第一道门是
agent.Functions,不会误路由)。多进程交叉提供同一函数时,只要任一放行进程也声明了该函数就不剔除,拒绝不制造可用性缺口。 - 版本解析失败(
"unknown"/空/非数字段)一律放行,且不抬升高水位——门槛只在两端都是可解析的语义化版本时生效。 - 无
sdk_language/sdk_version自报的进程(自定义游戏服直连)不参与门槛。 - 拒绝与落后警告同时随
RegisterResponse.warnings返回,agent 侧日志可见;连接保持不断开。
配置最低版本(绝对下限)
除自动高水位外,还可按语言配置最低可注册版本(registry.sdkVersionMinimums):
registry:
sdkVersionMinimums:
go: "0.3.0"
python: "0.2.0"自报版本低于配置值的 provider 其独占声明函数不注册(写 sdk_version_below_minimum 注册警告并随 RegisterResponse.warnings 返回,连接保持)。语义要点:
- 配置门槛是绝对下限,优先于滑动高水位判定——即使尚无高水位观测(首次注册)也生效;
- 未配置的语言仅走高水位门槛;语言键大小写不敏感;
- 与高水位门槛同样保守:自报版本或配置值解析失败时不触发;
- 交叉提供保护一致:任一放行进程也声明的函数不被剔除。
函数级最低版本(UI 按函数配置)
除语言级 yaml 外,还可对单个函数设置最低可注册函数版本(比函数描述符自身的 version,与 SDK 版本无关):函数详情页「变更历史」tab 顶部的「版本门槛」卡片,或函数目录勾选后「批量设置门槛」(GET/PUT/DELETE /api/v1/functions/:id/version-floor + 批量端点,写需 functions:manage),落 game 库 function_version_floors 表(编号迁移 0030)。
- 防契约回退:滚动升级窗口里旧 game server 的重注册会把物化契约刷回旧版(契约振荡 → 页面 stale 的主要触发源);低于配置值的旧版函数声明不物化,写
function_version_below_minimum注册警告(随RegisterResponse.warnings返回),契约永不回退。 - 判定粒度是函数:逐描述符判定(
req.Functions的version),同请求里其他达标函数照常注册;不依赖进程自报(无 processes 的纯游戏服直连也生效)。 - 与 SDK 版本门槛正交:SDK 门槛(高水位/语言级 yaml)管车队升级,函数版本门槛管契约不回退;语义化版本比较复用同一
sdkversion包。 - 与注册物化解耦存独立表:函数行随重注册 upsert,平台设置不会被描述符回写冲掉;清空即物理删行(唯一索引不被软删残留占住)。
- 平台上函数版本恒为可解析 semver:上游注册校验已按
invalid_version拒绝空/非法版本(根本到不了门槛);配置值入库前服务端同样校验可解析(sdkversion.Parseable)。 - 已知边界:唯一 provider 的版本低于门槛时函数不可用(警告可见),这是「低于配置值不注册」语义的直接结果。
批量入口(函数目录页):典型场景是 SDK 全量升级后把一批函数的门槛统一收口,逐个进详情页不可接受。函数目录页表格支持勾选多函数后浮出批量操作条(「批量设置门槛」/「批量清除」),并新增「最低函数版本」列回显当前门槛:
- 批量读:
GET /api/v1/functions/version-floors→{"floors": {functionId: minVersion}}(当前 game/env 全量门槛)。 - 批量写:
POST /api/v1/functions/version-floor/batch,body{functionIds, minVersion};minVersion 非空时统一 upsert(服务端先整包校验可解析,非法 400 零写入),空串等价对每个函数执行 DELETE(物理删行)。写权限同单函数(functions:manage)。 - 部分成功语义:逐函数循环 Set/Delete(均幂等,重试安全),响应
{updated, failed, minVersion};failed 列出失败 functionId,前端 warning 提示明细。无事务包裹。 - 目录列拉取失败降级为不显示门槛(
.catch(() => ({}))),不阻塞函数列表。
已知边界:
- 高水位是单调观测值:高版本 SDK 永久下线后,低版本会持续被拒,重置需手动
DELETE FROM sdk_version_highwatermarks WHERE ...。 - 内存 registry(无 DB)退化为进程内 map,重启丢失,重新从首次注册版本开始累积。
- 落后警告(
sdk_version_behind)在 SDK 追赶后不自动清除(多语言 provider 共用 agent+code 维度无法精确按语言清理),人工删除兜底。 - 抬升高水位发生在
UpsertAgent成功之后:注册链中途失败不会留下「未注册但已抬升」的状态;反之注册成功后进程崩溃在抬升之前,高水位少记一次,下次注册补记,无正确性影响。
函数摘除宽限(注册面 Removed)
此前注册面函数消失(game server 下线/重启窗口/闪断/门槛误配)会立即真删契约并级联清理提案,重注册后再 created+物化回来——瞬态摘除制造 removed+created 版本噪音与页面 stale 抖动。现在改为摘除宽限两段式(列 function_contracts.removal_pending_at,编号迁移 0031):
- 打标(注册事务内):
UpsertAgent的 diff 发现函数 Removed 且无幸存 provider 时,只把契约行标removal_pending_at = now(重复摘除刷新时间戳),不真删。运行时可用性立即生效——内存 registry 的agent.Functions已不含该函数,调用路由即刻 503,目录实例数归零。 - 清除(宽限内重注册):函数重新出现在注册面时,契约物化后自动清
removal_pending_at。全程零版本噪音、页面不抖。 - 清扫(宽限过期):后台循环(
StartBackgroundTasks,与指标修剪/会话清理同 5 分钟间隔)调SweepExpiredContractRemovals,对removal_pending_at早于宽限(默认 10 分钟,包级常量)的行做条件删除原子认领(DELETE ... WHERE removal_pending_at IS NOT NULL,HA 多实例只有一个实例命中,removed 历史不双写;宽限内已重注册的行条件删除落空,不误删)。真删后补 removed 版本历史(B2 变更历史可见终态)、清理 standalone 提案、资源无存活契约时摘除资源能力/语义/资源页提案(与即时移除路径同净效果)。 - 跨 scope:database-per-game 模式下 pending 行散布在各 game 库,清扫按
game_envs绑定逐 scope 收口(单 scope 失败记日志继续,下轮重试);单库模式一次清扫覆盖全部 scope。
已知边界:
- 宽限期内(默认 10 分钟)契约行与提案仍在:运行时不可用(调用 503)以内存 registry 为准,但契约目录/函数详情在宽限期内仍能看到该函数的契约内容(实例 0);这是宽限语义的直接结果,不是目录 bug。
- 宽限时长是包级常量(
contractRemovalGracePeriod),暂不暴露配置项;清扫间隔复用后台循环 5 分钟,实际最迟删除时点 ≈ 摘除时刻 + 宽限 + 清扫间隔。 RemoveFunctionContract即时路径仅剩 OpenAPI 服务内部使用(被取代的 unbound 契约清理、SDK 回退 reconcile),注册面不再触发。
5. 作业流
运营侧的 TaskPage 也走第 1 节的 binding execute:task_status/task_events/task_result/task_cancel 各自对应一个 binding,页面模型见 Dashboard Resource/Page 模型。
6. 核心原则
当前数据流遵循以下原则:
- 控制面和调用面统一收敛到 session 复用,不再拆成“注册链路 + 回拨链路”。
Agent本地监听只面向本地接入方。Server通过 session ownership 路由到对应Agent,而不是直接拨Agent。- 业务 payload 保持 JSON,平台控制字段保持 protobuf。
