修复发布就绪等待与充值过期监听
为 API readiness 探测增加单次超时,避免端口已建立时无限挂起 修复 SpacetimeDB 枚举状态订阅导致的充值过期监听失败 在 pause-after-stdb 审批提示中强调运行时与迁移身份互斥 补齐生产发布门禁、回归测试与运维排障文档
This commit is contained in:
@@ -2105,6 +2105,13 @@
|
||||
- 验证:确认 `GENARRATIVE_PROCESS_ROLE=external-generation-worker` / `external-generation-controller` 不启动充值过期监听;创建 pending 充值单后只由 scheduled reducer 产生 `expired`,HTTP api-server listener 记录 `expiration_checked_at` 或补入账。
|
||||
- 关联:`server-rs/crates/api-server/src/profile_recharge_expiration_listener.rs`、`server-rs/crates/spacetime-module/src/runtime/profile.rs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
## 充值订单状态枚举不能用字符串 SQL 字面量订阅
|
||||
|
||||
- 现象:API 已 ready,但日志每 5 秒出现 `profile recharge expiration listener failed to subscribe`,并提示 `pending` 不能解析为 `profile_recharge_order.status` 的枚举类型;scheduled reducer 仍会把订单改成 `expired`,但微信查单补偿监听没有运行。
|
||||
- 原因:SpacetimeDB 2.6 不会把订阅 SQL 中的 `'pending'` / `'expired'` 字符串自动转换为生成绑定的 sum-type enum;两个按状态过滤的订阅都在应用阶段失败。
|
||||
- 处理:后端订阅完整 `profile_recharge_order` 表,并继续只在 SDK `on_update` 中处理 `Pending -> Expired`。完整订阅让同一行跨状态变化时稳定产生 update callback,也避免依赖不受支持的枚举字符串 SQL。
|
||||
- 验证:运行 `cargo test -p spacetime-client profile_recharge_expiration --manifest-path server-rs/Cargo.toml`,发布后确认 API 日志不再出现订阅解析错误,并用真实 pending 订单验证 scheduled reducer 过期后写入 `expiration_checked_at`。
|
||||
|
||||
## 抓大鹅历史草稿外部 Rodin GLB 链接必须转存后再试玩或发布
|
||||
|
||||
- 现象:草稿页预览模型失败并报 `GL_INVALID_ENUM: Invalid cap.`,或结果页能看到历史生成记录但试玩、发布和正式运行态仍显示默认积木。
|
||||
|
||||
@@ -401,7 +401,7 @@ Pingora current release 自审脚本 `scripts/ops/pingora-current-release-audit.
|
||||
|
||||
生产运行时不把 bootstrap secret 明文写进 `/etc/genarrative/*.env`。`api-server.env` 和 worker env 只登记固定 FILE 路径 `GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET_FILE=/var/lib/genarrative/spacetime/runtime-service-bootstrap-secret.txt`;若检测到明文 `GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET` 或其他 FILE 路径,Server-Provision / API deploy 必须失败。Full Build 先执行 Stdb publish、后执行 API deploy,因此两段必须透传同一 `API_ENV_FILE` / `WORKER_ENV_FILE`;Stdb Publish 把 Secret File 路径作为 `--migration-bootstrap-secret-file` 传给随包 `production-stdb-publish.sh`。脚本先进入维护模式、按所选模式完成发布前冷备份、校验 checksum 并发布 module;成功后拒绝符号链接目标,把 secret 安装成 `root:genarrative 0440`、目录收紧为 `root:genarrative 0750`,原子补齐 API / worker env 的固定 FILE 配置,再快照并重启发布前为 active 的 API、controller 和 worker。`systemctl is-active` 只有明确返回合法的非 active 状态时才允许跳过,查询错误或 active worker 的 `list-units` 失败都必须保留维护模式并阻断;所有原 active 服务重启后必须重新确认为 `active`。如果 API 原本 active,还必须在 `maintenance-off` 前通过本机 `http://127.0.0.1:8082/healthz` readiness;可用 `--api-health-url` / `GENARRATIVE_STDB_PUBLISH_API_HEALTH_URL` 调整本机 URL,并用 `--api-readiness-timeout-seconds` / `GENARRATIVE_STDB_PUBLISH_API_READINESS_TIMEOUT_SECONDS` 调整超时。这样旧服务器首次 rollout 也不会等到后续 API deploy 才拿到 FILE;只覆盖 secret 文件或只补 env 而不重启都不生效,因为 `AppConfig` 在进程启动时读取 secret。人工执行 `npm run build:production-release -- --component spacetime-module --name <version>` 且未显式提供 secret / SHA-256 时,原始随机 secret 只写入 gitignored 的 `server-rs/.spacetimedb/build-secrets/<version>.txt`,目录权限 `0700`、文件权限 `0600`,发布包和 `release-manifest.json` 都不收录它;必须把该受保护文件另行交给 publish 阶段。旧 `npm run deploy:rust:remote` Ubuntu 直传入口也使用同一 sidecar 目录,发布包内不含原文;上传模式通过独立 SSH 标准输入把 secret 原子安装到远端发布目录并收紧为 `0600`,`--skip-upload` 时必须单独受保护交付。 本地 dev 的原始值也只能进入 api-server,不得扩散给 Web / Vite,任何控制台、Jenkins 日志、归档或生成 README 都不得输出明文。相关变更至少运行 `bash -n scripts/deploy/production-stdb-publish.sh scripts/deploy/production-api-deploy.sh scripts/deploy-rust-remote.sh scripts/jenkins-server-provision.sh`、`node --check scripts/dev.mjs scripts/check-production-ops-guardrails.mjs`、`npm run check:production-ops`、`npm run check:encoding` 和 `git diff --check`。
|
||||
|
||||
生产 runtime writer 不能通过替换 bootstrap secret 或重启服务隐式轮换。migration operator 与 runtime writer 必须身份互斥:operator 不能成为 writer,当前 writer 不能授权为 operator;已有任一 operator 后,bootstrap secret 不得新增或接管 operator。先准备新 api-server identity,并使用当前已授权 migration operator 的 CLI 登录态执行 `node scripts/deploy/production-runtime-writer-identity-rotate.mjs --database <database> --server-url <url> --operator-identity <operatorIdentity> --operator-user-id <userId> --next-writer-identity <nextIdentity> --confirm-next-writer-identity <nextIdentity> --note <reason>`。CLI 会校验当前登录 identity、双录新 identity 和审计原因;模块 procedure 还会拒绝把 writer 设为任一已登记 migration operator。成功后必须核对 `editor_generation_runtime_identity_rotation` 的旧 writer、新 writer、operator identity、操作人、原因和服务端时间,再切换 API token;轮换只改 writer,不改模型价格。
|
||||
生产 runtime writer 不能通过替换 bootstrap secret 或重启服务隐式轮换。migration operator 与 runtime writer 必须身份互斥:operator 不能成为 writer,当前 writer 不能授权为 operator;已有任一 operator 后,bootstrap secret 不得新增或接管 operator。先准备新 api-server identity,并使用当前已授权 migration operator 的 CLI 登录态执行 `node scripts/deploy/production-runtime-writer-identity-rotate.mjs --database <database> --server-url <url> --operator-identity <operatorIdentity> --operator-user-id <userId> --next-writer-identity <nextIdentity> --confirm-next-writer-identity <nextIdentity> --note <reason>`。CLI 会校验当前登录 identity、双录新 identity 和审计原因;模块 procedure 还会拒绝把 writer 设为任一已登记 migration operator。成功后必须核对 `editor_generation_runtime_identity_rotation` 的旧 writer、新 writer、operator identity、操作人、原因和服务端时间,再切换 API token;轮换只改 writer,不改模型价格。`pause-after-stdb` 人工维护不得把 `api-server.env` 的 `GENARRATIVE_SPACETIME_TOKEN` identity 授权为 migration operator;首次初始化前数据库还没有 writer 记录,模块无法提前识别这枚 identity 的未来用途,误授权会让 API 启动持续报“数据库迁移操作员 identity 不能初始化为模型生成运行时服务 identity”。若已误授权,必须在维护完成后由该 identity 自撤 migration operator 权限,再确认 rollout gate。API deploy 的本机 readiness 探测每次请求固定 `--max-time 2`,即使端口已建立但 API 尚未响应也会回到有限重试,不得使用无超时 curl。
|
||||
|
||||
`Genarrative-Web-Build` 打包 `web.tar.gz` 前、`Genarrative-Web-Deploy` 解包后都会把 Web 静态目录规范为目录 `755`、文件 `644`。如果前端页面能打开但 public 图片、字体或音频返回 `403 Forbidden`,优先检查当前 `/srv/genarrative/web` 指向的 release 中对应文件权限是否被异常归档为 `600`,临时恢复可对该 release 的 `web` 目录执行目录 `755`、文件 `644` 的权限修正。
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ pipeline {
|
||||
}
|
||||
steps {
|
||||
timeout(time: 4, unit: 'HOURS') {
|
||||
input message: 'SpacetimeDB module 已发布,站点和外部生成服务保持维护态。请在目标部署 agent 使用本次 Stdb artifact 中的外部生成任务维护脚本完成 dry-run、分批 apply 与摘要回填;确认无 has_more 后再继续部署 API。', ok: '确认维护完成,继续部署 API', submitter: params.STDB_API_ROLLOUT_APPROVERS.trim(), submitterParameter: 'STDB_API_ROLLOUT_APPROVED_BY'
|
||||
input message: 'SpacetimeDB module 已发布,站点和外部生成服务保持维护态。请使用与 api-server GENARRATIVE_SPACETIME_TOKEN 不同的 migration operator identity,在目标部署 agent 用本次 Stdb artifact 中的维护脚本完成 dry-run、分批 apply 与摘要回填;确认无 has_more,并确认 API runtime identity 未被授权为 migration operator 后再继续部署 API。', ok: '确认维护完成,继续部署 API', submitter: params.STDB_API_ROLLOUT_APPROVERS.trim(), submitterParameter: 'STDB_API_ROLLOUT_APPROVED_BY'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,8 +333,8 @@ function assertDeployCopiesPingoraDirectReleaseDependencies() {
|
||||
);
|
||||
assertIncludes(
|
||||
commandsLog,
|
||||
'curl -fsS http://127.0.0.1:18082/readyz',
|
||||
'部署脚本必须执行 readiness curl。',
|
||||
'curl -fsS --max-time 2 http://127.0.0.1:18082/readyz',
|
||||
'部署脚本必须为 readiness curl 设置单次超时,避免端口已建立但服务未响应时无限等待。',
|
||||
);
|
||||
|
||||
if (existsSync(fixture.maintenanceFile)) {
|
||||
|
||||
@@ -167,6 +167,12 @@ const checks = [
|
||||
includes: 'readiness 通过,按参数保持维护模式',
|
||||
reason: 'API deploy 保留维护模式时必须在 readiness 通过后给出明确状态。',
|
||||
},
|
||||
{
|
||||
file: 'scripts/deploy/production-api-deploy.sh',
|
||||
includes: 'curl -fsS --max-time 2 "${HEALTH_URL}"',
|
||||
reason:
|
||||
'API readiness 单次请求必须有超时,避免端口已建立但服务尚未响应时绕过重试上限无限挂起。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-api-deploy',
|
||||
includes:
|
||||
@@ -383,6 +389,13 @@ const checks = [
|
||||
"booleanParam(name: 'EXIT_MAINTENANCE_MODE_AFTER_COMPLETION', defaultValue: true",
|
||||
reason: 'Full Build 必须显式提供完整发布成功后是否退出维护模式的选项。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-full-build-and-deploy',
|
||||
includes:
|
||||
'确认 API runtime identity 未被授权为 migration operator 后再继续部署 API',
|
||||
reason:
|
||||
'Stdb/API 人工 gate 必须提醒审批人保持 migration operator 与运行时服务 identity 互斥。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-full-build-and-deploy',
|
||||
includes:
|
||||
|
||||
@@ -1126,7 +1126,7 @@ wait_for_worker_controller_service "${WORKER_CONTROLLER_SERVICE}"
|
||||
|
||||
echo "[production-api-deploy] 等待 readiness: ${HEALTH_URL}"
|
||||
for _ in {1..30}; do
|
||||
if curl -fsS "${HEALTH_URL}" >/dev/null; then
|
||||
if curl -fsS --max-time 2 "${HEALTH_URL}" >/dev/null; then
|
||||
if [[ "${KEEP_MAINTENANCE_MODE}" -eq 1 ]]; then
|
||||
echo "[production-api-deploy] readiness 通过,按参数保持维护模式: ${MAINTENANCE_FILE}"
|
||||
else
|
||||
|
||||
@@ -2,10 +2,23 @@ use super::*;
|
||||
use spacetimedb_sdk::TableWithPrimaryKey;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
const PROFILE_RECHARGE_EXPIRATION_EVENT_SUBSCRIPTION_QUERIES: [&str; 2] = [
|
||||
"SELECT * FROM profile_recharge_order WHERE status = 'pending'",
|
||||
"SELECT * FROM profile_recharge_order WHERE status = 'expired'",
|
||||
];
|
||||
// SpacetimeDB 2.6 cannot parse string SQL literals as generated sum-type enum values.
|
||||
// Keeping the row subscribed across the status transition also guarantees an update callback.
|
||||
const PROFILE_RECHARGE_EXPIRATION_EVENT_SUBSCRIPTION_QUERIES: [&str; 1] =
|
||||
["SELECT * FROM profile_recharge_order"];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PROFILE_RECHARGE_EXPIRATION_EVENT_SUBSCRIPTION_QUERIES;
|
||||
|
||||
#[test]
|
||||
fn expiration_listener_keeps_orders_subscribed_across_enum_status_updates() {
|
||||
assert_eq!(
|
||||
PROFILE_RECHARGE_EXPIRATION_EVENT_SUBSCRIPTION_QUERIES,
|
||||
["SELECT * FROM profile_recharge_order"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProfileRechargeExpirationSubscription {
|
||||
connection: DbConnection,
|
||||
|
||||
Reference in New Issue
Block a user