// Package healthcheck 提供 im-service 的 HTTP 和 gRPC 健康检查统一状态模型。 package healthcheck import ( "context" "errors" "fmt" "sync/atomic" "time" "google.golang.org/grpc" "google.golang.org/grpc/connectivity" ) const ( // serviceName 是 gRPC health 和 HTTP health 对外稳定暴露的服务名。 serviceName = "im-service" // readyBudget 限制 ready 总耗时,避免探针积压拖垮进程。 readyBudget = 500 * time.Millisecond // roomGRPCBudget 限制 room-service 连接探测时间,ready 失败应快速反馈给调度层。 roomGRPCBudget = 300 * time.Millisecond // componentStatus 是所有健康检查项成功时使用的稳定字符串。 componentStatus = "ok" // failStatus 是任一健康检查项失败时使用的稳定字符串。 failStatus = "fail" ) // RuntimeProbe 覆盖 im-service 进程内、无 IO 的核心运行时状态。 type RuntimeProbe interface { // HealthCheck 检查进程内依赖是否初始化完整,不应执行慢 IO。 HealthCheck() error } // Check 描述单个健康检查项,失败时会直接暴露给部署系统和排障工具。 type Check struct { // Name 是检查项稳定名称,便于监控系统聚合。 Name string `json:"name"` // Status 只能是 ok 或 fail。 Status string `json:"status"` // Message 只在失败时返回,避免健康响应泄露过多运行细节。 Message string `json:"message,omitempty"` } // State 记录 listener、drain 和 room-service 连接这些健康检查硬边界。 type State struct { // nodeID 用于定位具体 im-service 实例。 nodeID string // runtime 检查 session manager、store 等进程内对象是否可用。 runtime RuntimeProbe // room 是到 room-service 的 gRPC 连接,ready 需要它可连接。 room *grpc.ClientConn // httpServing 表示 HTTP/WebSocket listener 已进入 Serve 循环。 httpServing atomic.Bool // grpcServing 表示内部 gRPC listener 已进入 Serve 循环。 grpcServing atomic.Bool // draining 表示实例正在下线,ready 应立即失败。 draining atomic.Bool } // NewState 创建 im-service 健康状态所有者;业务代码只通过标记方法更新状态。 func NewState(nodeID string, runtime RuntimeProbe, room *grpc.ClientConn) *State { return &State{ nodeID: nodeID, runtime: runtime, room: room, } } // ServiceName 返回健康检查对外声明的服务边界名。 func (s *State) ServiceName() string { return serviceName } // NodeID 返回当前进程节点 ID,HTTP health response 会透出它用于定位实例。 func (s *State) NodeID() string { return s.nodeID } // MarkHTTPServing 标记 HTTP/WebSocket listener 已进入 Serve 循环。 func (s *State) MarkHTTPServing() { s.httpServing.Store(true) } // MarkHTTPStopped 标记 HTTP/WebSocket listener 已退出 Serve 循环。 func (s *State) MarkHTTPStopped() { s.httpServing.Store(false) } // MarkGRPCServing 标记内部 gRPC listener 已进入 Serve 循环。 func (s *State) MarkGRPCServing() { s.grpcServing.Store(true) } // MarkGRPCStopped 标记内部 gRPC listener 已退出 Serve 循环。 func (s *State) MarkGRPCStopped() { s.grpcServing.Store(false) } // MarkDraining 让 ready 立即失败,保证下线时不会继续接新连接或新 RPC。 func (s *State) MarkDraining() { s.draining.Store(true) } // Live 检查本进程入口和内存运行时是否仍可工作,不探测下游依赖。 func (s *State) Live() []Check { // live 只看本进程是否还应该存活,不把 room-service 抖动作为重启本进程的理由。 checks := []Check{ boolCheck("http_server", s.httpServing.Load(), "HTTP/WebSocket listener is not serving"), boolCheck("grpc_bridge", s.grpcServing.Load(), "gRPC bridge listener is not serving"), } if s.runtime == nil { // runtime 缺失代表 App 装配异常,live 失败可以让进程被 supervisor 重启。 checks = append(checks, failCheck("session_manager", "runtime probe is not initialized")) return checks } if err := s.runtime.HealthCheck(); err != nil { // 内存态依赖不可用时继续服务会导致连接和投递路径异常。 checks = append(checks, failCheck("session_manager", err.Error())) return checks } // 当前 session manager 是同步内存索引,没有后台 loop;这里确认其依赖已初始化。 checks = append(checks, okCheck("session_manager")) return checks } // Ready 检查 im-service 接新流量所需的硬条件,包含 room-service gRPC 可连接性。 func (s *State) Ready(ctx context.Context) []Check { // ready 包含下游依赖和 drain 状态,用于决定是否继续接新 WS 和内部 RPC。 ctx, cancel := context.WithTimeout(ctx, readyBudget) defer cancel() checks := []Check{ boolCheck("http_listener", s.httpServing.Load(), "HTTP/WebSocket listener is not serving"), boolCheck("grpc_listener", s.grpcServing.Load(), "gRPC listener is not serving"), boolCheck("draining", !s.draining.Load(), "node is draining"), } roomCtx, roomCancel := context.WithTimeout(ctx, roomGRPCBudget) defer roomCancel() if err := waitGRPCReady(roomCtx, s.room); err != nil { // room-service 不可达时新订阅和公屏发言无法做守卫校验,因此不应接新流量。 checks = append(checks, failCheck("room_grpc", err.Error())) } else { checks = append(checks, okCheck("room_grpc")) } return checks } // Healthy 汇总检查项状态,HTTP status code 和 gRPC serving status 都依赖它。 func Healthy(checks []Check) bool { for _, check := range checks { if check.Status != componentStatus { // 任一组件失败即整体失败,避免 ready/liveness 聚合时掩盖部分不可用。 return false } } return true } func waitGRPCReady(ctx context.Context, conn *grpc.ClientConn) error { if conn == nil { // 没有连接对象说明 App.New 装配失败,不应报告 ready。 return errors.New("room gRPC connection is not initialized") } for { state := conn.GetState() if state == connectivity.Ready { // Ready 表示至少已经完成一次可用连接建立。 return nil } if state == connectivity.Shutdown { // Shutdown 连接不可恢复,必须让 ready 失败等待进程重建。 return errors.New("room gRPC connection is shutdown") } // grpc.Dial 是惰性连接;健康检查显式触发一次连接,再等待状态变化。 conn.Connect() if !conn.WaitForStateChange(ctx, state) { return fmt.Errorf("room gRPC not ready before deadline: %w", ctx.Err()) } } } func okCheck(name string) Check { // 成功检查项不附带 message,保持健康响应短小稳定。 return Check{Name: name, Status: componentStatus} } func failCheck(name string, message string) Check { // 失败检查项必须携带 message,方便直接从探针响应定位失败边界。 return Check{Name: name, Status: failStatus, Message: message} } func boolCheck(name string, ok bool, message string) Check { if ok { return okCheck(name) } // bool 型检查用于 listener/drain 等本地状态,失败原因由调用方传入稳定文本。 return failCheck(name, message) }