package grpchealth import ( "context" "strings" "sync/atomic" "time" ) const defaultDependencyBudget = 300 * time.Millisecond // Dependency 是 readiness 中必须可用的外部依赖。 type Dependency struct { Name string Budget time.Duration Check func(ctx context.Context) error } // ServingChecker 覆盖无外部硬依赖的占位或 stub gRPC 服务 readiness。 type ServingChecker struct { serviceName string dependencies []Dependency serving atomic.Bool draining atomic.Bool } // NewServingChecker 创建只依赖 gRPC listener 和 draining 状态的 checker。 func NewServingChecker(serviceName string, dependencies ...Dependency) *ServingChecker { return &ServingChecker{serviceName: serviceName, dependencies: dependencies} } // ServiceName 返回标准 gRPC health 使用的服务边界名。 func (c *ServingChecker) ServiceName() string { return c.serviceName } // MarkServing 标记 gRPC listener 已进入 Serve 循环。 func (c *ServingChecker) MarkServing() { c.serving.Store(true) } // MarkStopped 标记 gRPC listener 已退出 Serve 循环。 func (c *ServingChecker) MarkStopped() { c.serving.Store(false) } // MarkDraining 让 ready 立即失败,服务下线时不再进入可用池。 func (c *ServingChecker) MarkDraining() { c.draining.Store(true) } // Ready 返回 listener、draining 和外部硬依赖的聚合 readiness。 func (c *ServingChecker) Ready(ctx context.Context) []Check { checks := []Check{ Bool("grpc_listener", c.serving.Load(), "gRPC listener is not serving"), Bool("draining", !c.draining.Load(), "node is draining"), } for _, dependency := range c.dependencies { checks = append(checks, dependency.Ready(ctx)) } return checks } // Ready executes a dependency check with a short bounded budget. func (d Dependency) Ready(ctx context.Context) Check { name := strings.TrimSpace(d.Name) if name == "" { name = "dependency" } if d.Check == nil { return Fail(name, "dependency check is not configured") } budget := d.Budget if budget <= 0 { budget = defaultDependencyBudget } checkCtx, cancel := context.WithTimeout(ctx, budget) defer cancel() if err := d.Check(checkCtx); err != nil { return Fail(name, err.Error()) } return OK(name) }