148 lines
5.0 KiB
Go
148 lines
5.0 KiB
Go
// Package healthcheck 验证 im-service HTTP/gRPC 健康检查对 listener、drain 和下游连接的映射。
|
||
package healthcheck
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"net"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"testing"
|
||
"time"
|
||
|
||
"google.golang.org/grpc"
|
||
"google.golang.org/grpc/credentials/insecure"
|
||
healthpb "google.golang.org/grpc/health/grpc_health_v1"
|
||
)
|
||
|
||
// okRuntime 模拟进程内 session/store 状态正常的 RuntimeProbe。
|
||
type okRuntime struct{}
|
||
|
||
// HealthCheck 返回 nil,使测试聚焦在 listener、drain 和 room gRPC 可连接性。
|
||
func (okRuntime) HealthCheck() error {
|
||
return nil
|
||
}
|
||
|
||
// TestHTTPHealthLiveAndReady 验证 HTTP live/ready 响应码、JSON 契约和 drain 行为。
|
||
func TestHTTPHealthLiveAndReady(t *testing.T) {
|
||
conn := startRoomGRPC(t)
|
||
state := NewState("im-node-test", okRuntime{}, conn)
|
||
// listener serving 标记由 app.Run 维护,测试里手动设置来隔离 handler 行为。
|
||
state.MarkHTTPServing()
|
||
state.MarkGRPCServing()
|
||
waitReady(t, state)
|
||
|
||
handler := NewHTTPHandler(state)
|
||
|
||
liveRecorder := httptest.NewRecorder()
|
||
handler.Live(liveRecorder, httptest.NewRequest(http.MethodGet, "/healthz/live", nil))
|
||
if liveRecorder.Code != http.StatusOK {
|
||
t.Fatalf("live status mismatch: got %d want %d body=%s", liveRecorder.Code, http.StatusOK, liveRecorder.Body.String())
|
||
}
|
||
|
||
var live Response
|
||
if err := json.NewDecoder(liveRecorder.Body).Decode(&live); err != nil {
|
||
t.Fatalf("decode live response failed: %v", err)
|
||
}
|
||
if live.Status != "ok" || live.Service != "im-service" || live.NodeID != "im-node-test" {
|
||
t.Fatalf("unexpected live response: %+v", live)
|
||
}
|
||
|
||
// ready 成功依赖 listener、非 drain 和 room-service gRPC 可连接。
|
||
readyRecorder := httptest.NewRecorder()
|
||
handler.Ready(readyRecorder, httptest.NewRequest(http.MethodGet, "/healthz/ready", nil))
|
||
if readyRecorder.Code != http.StatusOK {
|
||
t.Fatalf("ready status mismatch: got %d want %d body=%s", readyRecorder.Code, http.StatusOK, readyRecorder.Body.String())
|
||
}
|
||
|
||
state.MarkDraining()
|
||
// 进入 drain 后 live 仍可成功,但 ready 必须失败,防止继续接新连接。
|
||
drainingRecorder := httptest.NewRecorder()
|
||
handler.Ready(drainingRecorder, httptest.NewRequest(http.MethodGet, "/healthz/ready", nil))
|
||
if drainingRecorder.Code != http.StatusServiceUnavailable {
|
||
t.Fatalf("draining ready status mismatch: got %d want %d body=%s", drainingRecorder.Code, http.StatusServiceUnavailable, drainingRecorder.Body.String())
|
||
}
|
||
|
||
var draining Response
|
||
if err := json.NewDecoder(drainingRecorder.Body).Decode(&draining); err != nil {
|
||
t.Fatalf("decode draining response failed: %v", err)
|
||
}
|
||
if draining.Status != "fail" || len(draining.Checks) == 0 {
|
||
t.Fatalf("draining response should expose failing checks: %+v", draining)
|
||
}
|
||
}
|
||
|
||
// TestGRPCHealthCheckMapsReadiness 验证标准 gRPC health 的 SERVING 状态严格映射 ready。
|
||
func TestGRPCHealthCheckMapsReadiness(t *testing.T) {
|
||
conn := startRoomGRPC(t)
|
||
state := NewState("im-node-test", okRuntime{}, conn)
|
||
state.MarkHTTPServing()
|
||
state.MarkGRPCServing()
|
||
waitReady(t, state)
|
||
|
||
server := NewGRPCServer(state)
|
||
resp, err := server.Check(context.Background(), &healthpb.HealthCheckRequest{Service: "im-service"})
|
||
if err != nil {
|
||
t.Fatalf("gRPC health check failed: %v", err)
|
||
}
|
||
if resp.GetStatus() != healthpb.HealthCheckResponse_SERVING {
|
||
t.Fatalf("gRPC health status mismatch: got %s want SERVING", resp.GetStatus())
|
||
}
|
||
|
||
state.MarkDraining()
|
||
// drain 后 gRPC health 也必须变为 NOT_SERVING,方便内部调用方停止路由。
|
||
resp, err = server.Check(context.Background(), &healthpb.HealthCheckRequest{Service: "im-service"})
|
||
if err != nil {
|
||
t.Fatalf("gRPC draining health check failed: %v", err)
|
||
}
|
||
if resp.GetStatus() != healthpb.HealthCheckResponse_NOT_SERVING {
|
||
t.Fatalf("gRPC draining health status mismatch: got %s want NOT_SERVING", resp.GetStatus())
|
||
}
|
||
}
|
||
|
||
// startRoomGRPC 启动一个最小 gRPC server,让 ready 检查可以建立真实连接。
|
||
func startRoomGRPC(t *testing.T) *grpc.ClientConn {
|
||
t.Helper()
|
||
|
||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||
if err != nil {
|
||
t.Fatalf("listen fake room gRPC failed: %v", err)
|
||
}
|
||
|
||
server := grpc.NewServer()
|
||
go func() {
|
||
// 测试只需要连接状态变为 Ready,不需要注册具体 room service。
|
||
_ = server.Serve(listener)
|
||
}()
|
||
|
||
conn, err := grpc.Dial(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||
if err != nil {
|
||
t.Fatalf("dial fake room gRPC failed: %v", err)
|
||
}
|
||
|
||
t.Cleanup(func() {
|
||
// 清理顺序保证 client conn、server 和 listener 都不会泄漏到其他测试。
|
||
_ = conn.Close()
|
||
server.Stop()
|
||
_ = listener.Close()
|
||
})
|
||
|
||
return conn
|
||
}
|
||
|
||
// waitReady 等待 gRPC 惰性连接完成,避免测试在 Dial 后立即断言 ready 导致偶发失败。
|
||
func waitReady(t *testing.T, state *State) {
|
||
t.Helper()
|
||
|
||
deadline := time.Now().Add(2 * time.Second)
|
||
for time.Now().Before(deadline) {
|
||
if Healthy(state.Ready(context.Background())) {
|
||
return
|
||
}
|
||
|
||
time.Sleep(10 * time.Millisecond)
|
||
}
|
||
|
||
t.Fatalf("state did not become ready before deadline: %+v", state.Ready(context.Background()))
|
||
}
|