feat: configure task center archive gc deployments

This commit is contained in:
zhx 2026-07-18 18:56:15 +08:00
parent 900a524f9b
commit 16eb283908
10 changed files with 147 additions and 1 deletions

View File

@ -91,6 +91,11 @@ CHATAPP_CRON_COMPOSE_FILE=/opt/likei/cron-sg-1/docker-compose.yml
CHATAPP_CRON_COMPOSE_SERVICE=week-star-job
CHATAPP_CRON_BINARY_PATH=/opt/likei/cron-sg-1/week-star-job/service
CHATAPP_CRON_RELEASE_META_PATH=/opt/likei/cron-sg-1/.hy-app-monitor-chatapp-cron-release.json
# Task Center 大表 GC 默认关闭。维护窗口必须同时在 Go service.env 和这里显式打开;
# cron 发布会把以下三项写入目标机 service.env 并验证容器真实环境变量。
CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED=false
CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS=60
CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS=180
MONGO_URI=
MONGO_DB_NAME=tarab_all
MONGO_INSTANCE_ID=ins-o9mmob66

View File

@ -140,8 +140,16 @@ CHATAPP_CRON_COMPOSE_FILE=/opt/likei/cron-sg-1/docker-compose.yml
CHATAPP_CRON_COMPOSE_SERVICE=week-star-job
CHATAPP_CRON_BINARY_PATH=/opt/likei/cron-sg-1/week-star-job/service
CHATAPP_CRON_RELEASE_META_PATH=/opt/likei/cron-sg-1/.hy-app-monitor-chatapp-cron-release.json
CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED=false
CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS=60
CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS=180
```
归档 GC 默认关闭。Go 节点的总开关通过“配置 / Go Env”写入各节点
`service.env`cron 的三个调度变量由部署平台根目录 `.env` 提供。Cron 发布会
先备份并更新目标机 `service.env`,重建后再核对容器实际环境变量,任何一项未透传
都会让发布失败,避免页面显示已开启但调度进程仍使用旧配置。
可以直接复制模板:
```bash

View File

@ -367,6 +367,19 @@ CHATAPP_CRON_RELEASE_META_PATH = (
).strip()
or f"{CHATAPP_CRON_REMOTE_DIR.rstrip('/')}/.hy-app-monitor-chatapp-cron-release.json"
)
# 归档 GC 的 Go 总开关和 cron 调度开关使用同名配置;部署平台默认强制关闭,
# 只有在维护窗口显式修改 .env 后cron 发布才会把启用态传给目标容器。
CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED = "true" if truthy(
os.environ.get("CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED"), False
) else "false"
# cron 只负责触发单轮 tick实际批次、cutoff 和状态机由 Go 服务持久化控制。
CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS = max(
1, env_int("CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS", 60)
)
# 超时必须覆盖 Go 单轮的小批删除和 COS 恢复包上传,避免 cron 提前取消请求。
CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS = max(
1, env_int("CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS", 180)
)
# chatapp-cron 是否启用;未显式指定时,只要目标机和仓库信息齐备就自动开启。
CHATAPP_CRON_ENABLED = truthy(
os.environ.get("CHATAPP_CRON_ENABLED"),

View File

@ -33,6 +33,9 @@ from core.config import (
CHATAPP_CRON_SSH_PORT,
CHATAPP_CRON_SSH_USE_SUDO,
CHATAPP_CRON_SSH_USER,
CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED,
CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS,
CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS,
ROOT,
SERVICE_BUILD_TIMEOUT_SECONDS,
SERVICE_PULL_TIMEOUT_SECONDS,
@ -148,6 +151,9 @@ def build_chatapp_cron_overview_payload() -> dict[str, Any]:
"composeService": CHATAPP_CRON_COMPOSE_SERVICE,
"binaryPath": CHATAPP_CRON_BINARY_PATH,
"dockerPlatform": BUILD_DOCKER_PLATFORM,
"archiveGCEnabled": CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED == "true",
"archiveGCIntervalSeconds": CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS,
"archiveGCTimeoutSeconds": CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS,
}
return {
"ok": True,
@ -262,8 +268,20 @@ def activate_chatapp_cron_binary(build_result: dict[str, Any], *, progress: Prog
"binaryPath": CHATAPP_CRON_BINARY_PATH,
"composeFile": CHATAPP_CRON_COMPOSE_FILE,
"remoteDir": CHATAPP_CRON_REMOTE_DIR,
"archiveGCEnabled": CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED == "true",
"archiveGCIntervalSeconds": CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS,
"archiveGCTimeoutSeconds": CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS,
}
metadata_blob = base64.b64encode(json.dumps(metadata, ensure_ascii=False, indent=2).encode("utf-8")).decode("ascii")
# cron 目标机的 service.env 属于运行时配置,不跟二进制一起进仓库。每次发布只
# 覆盖这三个白名单项,并在 compose 重建后核对容器真实 Env防止文件写了但
# compose 没有引用 service.env 的“假成功”。
runtime_env_updates = {
"CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED": CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED,
"CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS": str(CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS),
"CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS": str(CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS),
}
runtime_env_blob = base64.b64encode(json.dumps(runtime_env_updates, sort_keys=True).encode("utf-8")).decode("ascii")
script = f"""set -euo pipefail
REMOTE_DIR={shlex.quote(CHATAPP_CRON_REMOTE_DIR)}
@ -273,6 +291,8 @@ BINARY_PATH={shlex.quote(CHATAPP_CRON_BINARY_PATH)}
REMOTE_TMP={shlex.quote(remote_tmp)}
META_PATH={shlex.quote(CHATAPP_CRON_RELEASE_META_PATH)}
META_BLOB={shlex.quote(metadata_blob)}
SERVICE_ENV_PATH={shlex.quote(f"{CHATAPP_CRON_REMOTE_DIR.rstrip('/')}/service.env")}
RUNTIME_ENV_BLOB={shlex.quote(runtime_env_blob)}
compose() {{
if docker compose version >/dev/null 2>&1; then
@ -289,6 +309,45 @@ fi
install -m 0755 "$REMOTE_TMP" "$BINARY_PATH"
rm -f "$REMOTE_TMP"
if [ -f "$SERVICE_ENV_PATH" ]; then
cp "$SERVICE_ENV_PATH" "$REMOTE_DIR/.backups/service.env.$(date +%Y%m%d-%H%M%S)"
fi
python3 - "$SERVICE_ENV_PATH" "$RUNTIME_ENV_BLOB" <<'PY'
from pathlib import Path
import base64
import json
import os
import sys
import tempfile
target = Path(sys.argv[1])
updates = json.loads(base64.b64decode(sys.argv[2]).decode("utf-8"))
original_mode = (target.stat().st_mode & 0o777) if target.exists() else 0o600
lines = target.read_text(encoding="utf-8").splitlines() if target.exists() else []
seen = set()
output = []
for line in lines:
stripped = line.strip()
assignment = stripped[7:].strip() if stripped.startswith("export ") else stripped
key = assignment.split("=", 1)[0].strip() if "=" in assignment else ""
if key not in updates:
output.append(line)
continue
if key in seen:
continue
output.append(f"{{key}}={{json.dumps(str(updates[key]))}}")
seen.add(key)
for key, value in updates.items():
if key not in seen:
output.append(f"{{key}}={{json.dumps(str(value))}}")
target.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=target.parent, delete=False) as handle:
handle.write("\n".join(output).rstrip() + "\n")
temp_name = handle.name
os.chmod(temp_name, original_mode)
os.replace(temp_name, target)
PY
cd "$REMOTE_DIR"
compose -f "$COMPOSE_FILE" up -d --no-deps --force-recreate "$COMPOSE_SERVICE"
@ -305,6 +364,19 @@ if ! printf '%s\\n' "$STATUS_OUTPUT" | grep -Eiq 'running|up'; then
exit 1
fi
CONTAINER_ID="$(compose -f "$COMPOSE_FILE" ps -q "$COMPOSE_SERVICE")"
CONTAINER_ENV="$(docker inspect --format='{{{{range .Config.Env}}}}{{{{println .}}}}{{{{end}}}}' "$CONTAINER_ID")"
for EXPECTED_ENV in \
"CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED={CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED}" \
"CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS={CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS}" \
"CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS={CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS}"
do
if ! printf '%s\\n' "$CONTAINER_ENV" | grep -Fqx -- "$EXPECTED_ENV"; then
printf 'missing cron runtime env: %s\\n' "$EXPECTED_ENV" >&2
exit 1
fi
done
mkdir -p "$(dirname "$META_PATH")"
printf '%s' "$META_BLOB" | base64 -d > "$META_PATH"
"""

View File

@ -265,6 +265,15 @@ GO_ENV_SPECS: list[dict[str, Any]] = [
"aliases": ("TASK_CENTER_ARCHIVE_WORKERS",),
"kind": "int",
},
{
"section": "TaskCenter Archive GC",
"primary": "CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED",
"aliases": ("TASK_CENTER_ARCHIVE_GC_ENABLED",),
"kind": "bool",
# 大表清理必须由运营预检后显式开放;旧 service.env 没有该项时,
# 配置页也要展示关闭态,保存后才会把开关写到所有 Go 节点。
"default": "false",
},
{
"section": "WeekStar",
"primary": "CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN",
@ -606,7 +615,13 @@ def extract_go_values(values: dict[str, str]) -> dict[str, str]:
extracted: dict[str, str] = {}
for spec in GO_ENV_SPECS:
keys = (spec["primary"], *spec["aliases"])
extracted[spec["primary"]] = first_present_value(values, keys)
present = any(key in values for key in keys)
current_value = first_present_value(values, keys) if present else ""
extracted[spec["primary"]] = (
current_value
if current_value != ""
else str(spec.get("default", ""))
)
return extracted

View File

@ -426,6 +426,9 @@ function createChatappCronDeployState() {
composeService: "week-star-job",
binaryPath: "",
dockerPlatform: "linux/amd64",
archiveGCEnabled: false,
archiveGCIntervalSeconds: 60,
archiveGCTimeoutSeconds: 180,
},
metadata: {},
},

View File

@ -2939,6 +2939,11 @@
<span class="mono">{{ truncateMiddle(chatappCronSettings.repoRoot, 16, 12) || '-' }}</span>
<span class="mono">{{ truncateMiddle(chatappCronSettings.composeFile, 16, 12) || '-' }}</span>
<span>{{ chatappCronSettings.composeService || '-' }}</span>
<span :class="['badge', chatappCronSettings.archiveGCEnabled ? 'warn' : 'neutral']">
归档 GC {{ chatappCronSettings.archiveGCEnabled ? '已开启' : '已关闭' }}
</span>
<span>间隔 {{ chatappCronSettings.archiveGCIntervalSeconds || 60 }}s</span>
<span>超时 {{ chatappCronSettings.archiveGCTimeoutSeconds || 180 }}s</span>
</div>
<div class="action-group">

View File

@ -234,6 +234,10 @@ CHATAPP_ROOM_TURNOVER_REWARD_SETTLE_INTERVAL_SECONDS=60
CHATAPP_GAME_KING_SETTLE_INTERVAL_SECONDS=60
# Game King 每轮最多派送 4 组(单组硬超时 20 秒120 秒为 DB 冻结和状态修复保留余量。
CHATAPP_GAME_KING_SETTLE_TIMEOUT_SECONDS=120
# 大表归档清理默认关闭;测试清理流程时先从管理端 dry-run再显式打开 Go/cron 共用开关。
CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED=false
CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS=60
CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS=180
CHATAPP_WEEK_STAR_ROCKETMQ_ENABLED=false
CHATAPP_WEEK_STAR_ROCKETMQ_ENDPOINT=rocketmq-proxy:8081
CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_KEY=test

View File

@ -12,6 +12,9 @@ class ChatappCronDeployTests(unittest.TestCase):
def test_overview_does_not_read_remote_metadata_when_disabled(self) -> None:
with (
patch.object(cron, "CHATAPP_CRON_ENABLED", False),
patch.object(cron, "CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED", "false"),
patch.object(cron, "CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS", 60),
patch.object(cron, "CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS", 180),
patch.object(cron, "read_chatapp_cron_metadata") as read_metadata_mock,
):
payload = cron.build_chatapp_cron_overview_payload()
@ -19,6 +22,9 @@ class ChatappCronDeployTests(unittest.TestCase):
self.assertTrue(payload["ok"])
self.assertFalse(payload["enabled"])
self.assertEqual(payload["metadata"], {})
self.assertFalse(payload["settings"]["archiveGCEnabled"])
self.assertEqual(payload["settings"]["archiveGCIntervalSeconds"], 60)
self.assertEqual(payload["settings"]["archiveGCTimeoutSeconds"], 180)
read_metadata_mock.assert_not_called()
def test_activate_uploads_binary_and_recreates_compose_service(self) -> None:
@ -44,6 +50,9 @@ class ChatappCronDeployTests(unittest.TestCase):
patch.object(cron, "CHATAPP_CRON_COMPOSE_FILE", "/opt/deploy/runtime/current/docker-compose.yml"),
patch.object(cron, "CHATAPP_CRON_COMPOSE_SERVICE", "week-star-job"),
patch.object(cron, "CHATAPP_CRON_BINARY_PATH", "/opt/deploy/runtime/current/week-star-job/service"),
patch.object(cron, "CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED", "false"),
patch.object(cron, "CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS", 60),
patch.object(cron, "CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS", 180),
patch.object(
cron,
"CHATAPP_CRON_RELEASE_META_PATH",
@ -75,9 +84,13 @@ class ChatappCronDeployTests(unittest.TestCase):
self.assertIn("BINARY_PATH=/opt/deploy/runtime/current/week-star-job/service", remote_script)
self.assertIn('up -d --no-deps --force-recreate "$COMPOSE_SERVICE"', remote_script)
self.assertIn("META_BLOB=", remote_script)
self.assertIn("CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED=false", remote_script)
self.assertIn("CHATAPP_TASK_CENTER_ARCHIVE_GC_INTERVAL_SECONDS=60", remote_script)
self.assertIn("CHATAPP_TASK_CENTER_ARCHIVE_GC_TIMEOUT_SECONDS=180", remote_script)
self.assertEqual(result["metadata"]["commit"], "f0f6205abc1234567890")
self.assertEqual(result["metadata"]["component"], "week-star-job")
self.assertFalse(result["metadata"]["archiveGCEnabled"])
if __name__ == "__main__":

View File

@ -7,6 +7,7 @@ from core.config import HARBOR_PROJECT, HARBOR_REGISTRY, load_hosts
from monitors.yumi.runtime_config import (
GO_ENV_PRIMARY_BY_KEY,
GO_ENV_SPECS_BY_PRIMARY,
extract_go_values,
expand_host_updates,
validate_go_editor_content,
)
@ -29,6 +30,7 @@ class ServiceReleaseTargetsTests(unittest.TestCase):
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_CONSUMER_GROUP",
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_TOPIC",
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_TAG",
"CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED",
}
self.assertTrue(expected_keys.issubset(GO_ENV_SPECS_BY_PRIMARY))
@ -49,11 +51,17 @@ class ServiceReleaseTargetsTests(unittest.TestCase):
'CHATAPP_YUMI_GIFT_CHALLENGE_DELIVERY_LEASE_SECONDS="300"',
'CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ENABLED="true"',
'CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ENDPOINT="rocketmq.example:8081"',
'CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED="false"',
]
)
)
self.assertTrue(valid["ok"])
self.assertEqual(valid["values"]["CHATAPP_JAVA_ORDER_BASE_URL"], "http://10.2.1.15:2600")
self.assertEqual(valid["values"]["CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED"], "false")
self.assertEqual(
extract_go_values({})["CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED"],
"false",
)
invalid_lease = validate_go_editor_content(
'CHATAPP_YUMI_GIFT_CHALLENGE_DELIVERY_LEASE_SECONDS="0"'