test: verify go native release layout

This commit is contained in:
lingniu
2026-07-02 08:47:05 +08:00
parent 8def635bf5
commit 409f55b5ae
3 changed files with 49 additions and 7 deletions

View File

@@ -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 当前无待回放文件。

View File

@@ -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

View File

@@ -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()