From 409f55b5ae11c27a7dfd78bc0e8be1523b2a0394 Mon Sep 17 00:00:00 2001 From: lingniu Date: Thu, 2 Jul 2026 08:47:05 +0800 Subject: [PATCH] test: verify go native release layout --- docs/operations/current-ecs-deployment.md | 3 +- tools/go_systemd_prod_smoke.py | 35 +++++++++++++++++++---- tools/test_go_systemd_prod_smoke.py | 18 ++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/docs/operations/current-ecs-deployment.md b/docs/operations/current-ecs-deployment.md index 4dcfeac0..1f35bc54 100644 --- a/docs/operations/current-ecs-deployment.md +++ b/docs/operations/current-ecs-deployment.md @@ -167,7 +167,7 @@ python3 tools/go_systemd_prod_smoke.py --host 115.29.187.205 --user root python3 tools/go_native_prod_smoke.py --date 2026-07-02 --timeout 8 ``` -`go_systemd_prod_smoke.py` 通过 SSH 检查四个 Go systemd 服务是否 `active` 且 `enabled`,并确认 `808`、`32960` 由 `gateway` 监听、`20210` 由 `realtime-api` 监听,同时要求 `/opt/lingniu-go-native/spool/gateway` 没有待回放文件,避免旧 Java/Docker 进程占用生产端口或 Kafka spool 静默堆积。运行环境需要已配置 SSH 免密或已建立可用的 SSH 认证方式。 +`go_systemd_prod_smoke.py` 通过 SSH 检查四个 Go systemd 服务是否 `active` 且 `enabled`,并确认 `808`、`32960` 由 `gateway` 监听、`20210` 由 `realtime-api` 监听,同时要求 `/opt/lingniu-go-native/current` 指向有效 release、四个生产二进制均可执行、`/opt/lingniu-go-native/spool/gateway` 没有待回放文件,避免旧 Java/Docker 进程占用生产端口、部署目录错乱或 Kafka spool 静默堆积。运行环境需要已配置 SSH 免密或已建立可用的 SSH 认证方式。 `go_native_prod_smoke.py` 通过 `20210` HTTP API 验证 GB32960、JT808、宇通 MQTT 的 RAW 查询及结构化 `parsed_json`,三类协议的位置和里程点查询及 `frame_id` RAW 回链,GB32960/JT808 的 `daily_mileage_km`、`daily_total_mileage_km` 统计公式,以及三类协议的 Redis realtime snapshot、online、protocol 查询。实时 snapshot/protocol 查询会校验 `updated_at_ms` 新鲜度;默认检查今天东八区数据时,还会要求三类 RAW 最新样本不超过 15 分钟,避免旧数据误判为接收正常;任一检查不达标时退出码为非 0。 @@ -213,6 +213,7 @@ journalctl -u lingniu-go-realtime-api --since '5 minutes ago' --no-pager 2026-07-02 01:30 CST 已验证: - 四个 Go systemd 服务均为 `active` 且 `enabled`。 +- `/opt/lingniu-go-native/current` 指向有效 release,四个生产二进制均存在且可执行。 - `gateway` 正在监听 `32960` 和 `808`。 - `realtime-api` 正在监听 `20210`。 - gateway Kafka spool 当前无待回放文件。 diff --git a/tools/go_systemd_prod_smoke.py b/tools/go_systemd_prod_smoke.py index 8f88fa20..9fe8f6cc 100755 --- a/tools/go_systemd_prod_smoke.py +++ b/tools/go_systemd_prod_smoke.py @@ -23,7 +23,9 @@ DEFAULT_PORTS = { 32960: "gateway", 20210: "realtime-api", } +DEFAULT_RELEASE_ROOT = "/opt/lingniu-go-native" DEFAULT_SPOOL_DIR = "/opt/lingniu-go-native/spool/gateway" +DEFAULT_BINARIES = ["gateway", "history-writer", "stat-writer", "realtime-api"] @dataclass(frozen=True) @@ -92,6 +94,14 @@ def spool_check(output: str) -> Check: return Check("spool.gateway", status, output.strip() or "missing") +def release_check(output: str) -> Check: + values = parse_key_values(output) + current = values.get("current", "") + binaries_present = [values.get(binary, "0") == "1" for binary in DEFAULT_BINARIES] + status = "pass" if current and all(binaries_present) else "fail" + return Check("release.current", status, output.strip() or "missing") + + def parse_key_values(output: str) -> dict[str, str]: values: dict[str, str] = {} for part in output.split(): @@ -132,29 +142,42 @@ def remote_command(services: list[str]) -> str: "bytes=$(du -sb \"$spool\" 2>/dev/null | awk '{print $1}'); " "recent=$(find \"$spool\" -type f -mmin -5 | wc -l); " "printf 'files=%s bytes=%s recent=%s\\n' \"$files\" \"${bytes:-0}\" \"$recent\"; " - "else printf 'files=-1 bytes=0 recent=-1 missing=%s\\n' \"$spool\"; fi" + "else printf 'files=-1 bytes=0 recent=-1 missing=%s\\n' \"$spool\"; fi; " + "printf '\\n--RELEASE--\\n'; " + "root='" + DEFAULT_RELEASE_ROOT + "'; " + "current=$(readlink -f \"$root/current\" 2>/dev/null || true); " + "printf 'current=%s' \"$current\"; " + "for bin in " + " ".join(DEFAULT_BINARIES) + "; do " + "if test -x \"$root/current/$bin\"; then present=1; else present=0; fi; " + "printf ' %s=%s' \"$bin\" \"$present\"; " + "done; printf '\\n'" ) -def split_remote_output(output: str) -> tuple[str, str, str]: +def split_remote_output(output: str) -> tuple[str, str, str, str]: ports_marker = "\n--PORTS--\n" spool_marker = "\n--SPOOL--\n" + release_marker = "\n--RELEASE--\n" if ports_marker not in output: - return output, "", "" + return output, "", "", "" service_output, rest = output.split(ports_marker, 1) if spool_marker not in rest: - return service_output, rest, "" + return service_output, rest, "", "" port_output, spool_output = rest.split(spool_marker, 1) - return service_output, port_output, spool_output + if release_marker not in spool_output: + return service_output, port_output, spool_output, "" + spool_output, release_output = spool_output.split(release_marker, 1) + return service_output, port_output, spool_output, release_output def run(args: argparse.Namespace) -> tuple[str, list[Check]]: output = ssh(args.host, args.user, remote_command(DEFAULT_SERVICES), args.timeout) - service_output, port_output, spool_output = split_remote_output(output) + service_output, port_output, spool_output, release_output = split_remote_output(output) checks = ( service_checks(service_output, DEFAULT_SERVICES) + port_checks(port_output, DEFAULT_PORTS) + [spool_check(spool_output)] + + [release_check(release_output)] ) status = "fail" if any(check.status == "fail" for check in checks) else "pass" return status, checks diff --git a/tools/test_go_systemd_prod_smoke.py b/tools/test_go_systemd_prod_smoke.py index 535b3bfc..30e4962e 100644 --- a/tools/test_go_systemd_prod_smoke.py +++ b/tools/test_go_systemd_prod_smoke.py @@ -78,6 +78,24 @@ LISTEN 0 4096 *:20210 *:* users:(("realtime-api",pid=11,fd=3)) self.assertEqual(check.status, "fail") + def test_release_check_passes_when_current_release_has_all_binaries(self): + check = smoke.release_check( + "current=/opt/lingniu-go-native/releases/8def635 " + "gateway=1 history-writer=1 stat-writer=1 realtime-api=1" + ) + + self.assertEqual(check.status, "pass") + self.assertIn("8def635", check.message) + + def test_release_check_fails_when_binary_is_missing(self): + check = smoke.release_check( + "current=/opt/lingniu-go-native/releases/8def635 " + "gateway=1 history-writer=0 stat-writer=1 realtime-api=1" + ) + + self.assertEqual(check.status, "fail") + self.assertIn("history-writer=0", check.message) + if __name__ == "__main__": unittest.main()