- 迁移 1043:gateway 表加 health_status/last_check_reason/last_checked_at - Store.RecordGatewayCheck 覆盖写最近一次探活结果,Create/Update/List/Get 全量返回健康字段 - api.RecordGatewayHealthChecks 并发探 /healthz:连接失败或 5xx 判离线,其余判在线(401 等异常写入原因供排查) - workers 每 30 秒一轮,app 启动接线 - 网关管理列表新增健康状态列:在线/离线/未检查 Tag,原因与检查时间收进 Tooltip
123 lines
4.6 KiB
Go
123 lines
4.6 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
hub "git.ipao.vip/rogee/creator-hub/internal/environment"
|
|
)
|
|
|
|
type recordedGatewayCheck struct {
|
|
name string
|
|
status string
|
|
reason string
|
|
}
|
|
|
|
// healthMemoryStore 在 memoryStore 之上记录 RecordGatewayCheck 调用,供健康检查断言。
|
|
type healthMemoryStore struct {
|
|
*memoryStore
|
|
mu sync.Mutex
|
|
checks []recordedGatewayCheck
|
|
}
|
|
|
|
func (s *healthMemoryStore) RecordGatewayCheck(_ context.Context, name, status, reason string) (hub.Gateway, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.checks = append(s.checks, recordedGatewayCheck{name: name, status: status, reason: reason})
|
|
return hub.Gateway{Name: name, HealthStatus: status, LastCheckReason: reason}, nil
|
|
}
|
|
|
|
func TestClassifyGatewayHealth(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
status int
|
|
callErr error
|
|
wantStatus string
|
|
wantReasonHad bool // 是否应携带非空 reason
|
|
}{
|
|
{name: "204 healthy without reason", status: http.StatusNoContent, wantStatus: "healthy", wantReasonHad: false},
|
|
{name: "200 healthy without reason", status: http.StatusOK, wantStatus: "healthy", wantReasonHad: false},
|
|
{name: "401 reachable but unauthorized", status: http.StatusUnauthorized, wantStatus: "healthy", wantReasonHad: true},
|
|
{name: "404 reachable but misrouted", status: http.StatusNotFound, wantStatus: "healthy", wantReasonHad: true},
|
|
{name: "503 unhealthy", status: http.StatusServiceUnavailable, wantStatus: "unhealthy", wantReasonHad: true},
|
|
{name: "connection error unhealthy", callErr: errors.New(`Get "http://gw:8081/healthz": dial tcp: connection refused`), wantStatus: "unhealthy", wantReasonHad: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
status, reason := classifyGatewayHealth(test.status, test.callErr)
|
|
if status != test.wantStatus {
|
|
t.Fatalf("classify status = %q, want %q (reason=%q)", status, test.wantStatus, reason)
|
|
}
|
|
if test.wantReasonHad != (reason != "") {
|
|
t.Fatalf("classify reason = %q, wantReasonHad=%v", reason, test.wantReasonHad)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyGatewayHealthTruncatesReason(t *testing.T) {
|
|
long := errors.New(string(make([]byte, 1024)))
|
|
_, reason := classifyGatewayHealth(0, long)
|
|
if len(reason) != 300 {
|
|
t.Fatalf("reason length = %d, want 300", len(reason))
|
|
}
|
|
}
|
|
|
|
func TestRecordGatewayHealthChecksPersistsLatestResult(t *testing.T) {
|
|
reachable := &fakeGateway{token: "token-a"}
|
|
reachableServer := httptest.NewServer(reachable.handler(t))
|
|
t.Cleanup(reachableServer.Close)
|
|
|
|
closedServer := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
|
closedServer.Close() // 端口已释放,保证连接失败
|
|
|
|
store := &healthMemoryStore{memoryStore: newMemoryStore()}
|
|
store.gateways["gw-live"] = hub.Gateway{Name: "gw-live", Endpoint: reachableServer.URL, Token: reachable.token}
|
|
store.gateways["gw-down"] = hub.Gateway{Name: "gw-down", Endpoint: closedServer.URL, Token: "unused-token"}
|
|
|
|
if err := recordGatewayHealthChecks(context.Background(), store); err != nil {
|
|
t.Fatalf("record gateway health checks returned error: %v", err)
|
|
}
|
|
|
|
byName := map[string]recordedGatewayCheck{}
|
|
for _, check := range store.checks {
|
|
byName[check.name] = check
|
|
}
|
|
if len(store.checks) != 2 {
|
|
t.Fatalf("expected one check per gateway, got %#v", store.checks)
|
|
}
|
|
if byName["gw-live"].status != "healthy" {
|
|
t.Fatalf("reachable gateway status = %q, want healthy", byName["gw-live"].status)
|
|
}
|
|
if byName["gw-live"].reason != "" {
|
|
t.Fatalf("reachable gateway reason = %q, want empty", byName["gw-live"].reason)
|
|
}
|
|
if byName["gw-down"].status != "unhealthy" || byName["gw-down"].reason == "" {
|
|
t.Fatalf("unreachable gateway = %#v, want unhealthy with reason", byName["gw-down"])
|
|
}
|
|
if requests := reachable.recorded(); len(requests) != 1 || requests[0].path != "/healthz" {
|
|
t.Fatalf("reachable gateway probes = %#v, want exactly one /healthz", requests)
|
|
}
|
|
}
|
|
|
|
func TestRecordGatewayHealthChecksEmptyListIsNoop(t *testing.T) {
|
|
store := &healthMemoryStore{memoryStore: newMemoryStore()}
|
|
if err := recordGatewayHealthChecks(context.Background(), store); err != nil {
|
|
t.Fatalf("empty list returned error: %v", err)
|
|
}
|
|
if len(store.checks) != 0 {
|
|
t.Fatalf("empty list recorded checks: %#v", store.checks)
|
|
}
|
|
}
|
|
|
|
// 编译期约束:探活超时常量必须明显小于调度间隔,避免探活互相堆叠。
|
|
func TestGatewayHealthProbeTimeoutBounded(t *testing.T) {
|
|
if gatewayHealthProbeTimeout > 10*time.Second {
|
|
t.Fatalf("probe timeout %v must stay below scheduling cadence", gatewayHealthProbeTimeout)
|
|
}
|
|
}
|