diff --git a/.env.example b/.env.example index 93c2b59..3e10cb8 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/README.md b/README.md index 223b70a..150f873 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/core/config.py b/core/config.py index b91459a..6b8e7b1 100644 --- a/core/config.py +++ b/core/config.py @@ -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"), diff --git a/monitors/yumi/chatapp_cron_deploy.py b/monitors/yumi/chatapp_cron_deploy.py index 17f3b13..419d995 100644 --- a/monitors/yumi/chatapp_cron_deploy.py +++ b/monitors/yumi/chatapp_cron_deploy.py @@ -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" """ diff --git a/monitors/yumi/runtime_config.py b/monitors/yumi/runtime_config.py index 7085cfa..ba440a5 100644 --- a/monitors/yumi/runtime_config.py +++ b/monitors/yumi/runtime_config.py @@ -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 diff --git a/static/app.js b/static/app.js index 71e2bbd..6b6099d 100644 --- a/static/app.js +++ b/static/app.js @@ -426,6 +426,9 @@ function createChatappCronDeployState() { composeService: "week-star-job", binaryPath: "", dockerPlatform: "linux/amd64", + archiveGCEnabled: false, + archiveGCIntervalSeconds: 60, + archiveGCTimeoutSeconds: 180, }, metadata: {}, }, diff --git a/static/index.html b/static/index.html index f06a524..0f50afd 100644 --- a/static/index.html +++ b/static/index.html @@ -2939,6 +2939,11 @@ {{ truncateMiddle(chatappCronSettings.repoRoot, 16, 12) || '-' }} {{ truncateMiddle(chatappCronSettings.composeFile, 16, 12) || '-' }} {{ chatappCronSettings.composeService || '-' }} + + 归档 GC {{ chatappCronSettings.archiveGCEnabled ? '已开启' : '已关闭' }} + + 间隔 {{ chatappCronSettings.archiveGCIntervalSeconds || 60 }}s + 超时 {{ chatappCronSettings.archiveGCTimeoutSeconds || 180 }}s
diff --git a/testbox/.env.example b/testbox/.env.example index be26397..0bf2bf0 100644 --- a/testbox/.env.example +++ b/testbox/.env.example @@ -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 diff --git a/tests/test_chatapp_cron_deploy.py b/tests/test_chatapp_cron_deploy.py index 0c087c7..5ce9d09 100644 --- a/tests/test_chatapp_cron_deploy.py +++ b/tests/test_chatapp_cron_deploy.py @@ -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__": diff --git a/tests/test_service_release.py b/tests/test_service_release.py index b5b9a2f..813005f 100644 --- a/tests/test_service_release.py +++ b/tests/test_service_release.py @@ -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"'