Files
orbit/internal/transport/httpserver/server.go

129 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package httpserver exposes health and authenticated task-management endpoints.
package httpserver
import (
"context"
"encoding/json"
"errors"
"net/http"
"sync/atomic"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/buildinfo"
"git.yuntuo.hk/k2safe/orbit/internal/consoleauth"
"git.yuntuo.hk/k2safe/orbit/internal/observability"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
)
type Checker func(context.Context) error
// Readiness 将数据库/结构检查与进程排空状态合并为一个就绪门控。
// 收到终止信号后先调用 BeginDrain,再停止接收新请求;存量请求仍由
// http.Server.Shutdown 在限定时间内完成。这样编排平台有机会先看到 503。
type Readiness struct {
checker Checker
draining atomic.Bool
}
// NewReadiness 创建可在优雅下线期间切换为未就绪的门控。
func NewReadiness(checker Checker) *Readiness {
return &Readiness{checker: checker}
}
// Check 执行就绪检查;排空开始后始终返回内部错误,由 HTTP 层统一隐藏细节。
func (r *Readiness) Check(ctx context.Context) error {
if r == nil || r.draining.Load() {
return errors.New("server is draining")
}
if r.checker == nil {
return errors.New("readiness checker is unavailable")
}
return r.checker(ctx)
}
// BeginDrain 先于 http.Server.Shutdown 调用,阻止排空窗口内的新流量。
func (r *Readiness) BeginDrain() {
if r != nil {
r.draining.Store(true)
}
}
func New(addr string, ready Checker, tasks ...*service.Service) *http.Server {
return newServer(addr, ready, nil, tasks...)
}
// NewWithConsoleAuth 创建带数据库控制台登录会话的 HTTP 服务。
// 保留 New 供只测试 API 的调用方使用;生产入口应传入 ConsoleAuthStore。
func NewWithConsoleAuth(addr string, ready Checker, consoleAuth consoleauth.Store, tasks ...*service.Service) *http.Server {
return newServer(addr, ready, consoleAuth, tasks...)
}
func newServer(addr string, ready Checker, consoleAuth consoleauth.Store, tasks ...*service.Service) *http.Server {
mux := http.NewServeMux()
write := func(w http.ResponseWriter, status int, body map[string]string) {
// 探针响应不能被代理或浏览器缓存,否则下线节点可能继续被路由流量。
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
}
mux.HandleFunc("GET /health/live", func(w http.ResponseWriter, r *http.Request) {
write(w, http.StatusOK, map[string]string{"status": "alive"})
})
mux.HandleFunc("GET /health/ready", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if ready == nil || ready(ctx) != nil {
// 给负载均衡器一个明确的短暂重试提示;真正的重试节奏仍由探针策略控制。
w.Header().Set("Retry-After", "1")
write(w, http.StatusServiceUnavailable, map[string]string{"status": "not_ready"})
return
}
write(w, http.StatusOK, map[string]string{"status": "ready", "scope": "database_and_schema"})
})
mux.HandleFunc("GET /version", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(buildinfo.Current())
})
if len(tasks) > 0 && tasks[0] != nil {
registerTasks(mux, tasks[0])
registerHistory(mux, tasks[0])
}
server := &http.Server{Addr: addr, Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 16 << 10}
RegisterConsole(server, consoleAuth)
return server
}
// WrapConsoleAuth 必须在所有 Register* 路由完成后调用,因为路由注册器需要
// 直接访问底层 ServeMux;包装完成后,浏览器会话才会转换为内部 Bearer 请求。
func WrapConsoleAuth(server *http.Server, consoleAuth consoleauth.Store, scopes ...consoleauth.Scopes) {
if server == nil || consoleAuth == nil {
return
}
server.Handler = ConsoleSessionMiddleware(server.Handler, consoleAuth, scopes...)
}
// WrapMetrics must run after all Register* functions because those functions
// intentionally register routes on the underlying ServeMux.
func WrapMetrics(server *http.Server, metrics *observability.Registry) {
if server == nil || metrics == nil {
return
}
server.Handler = metrics.Middleware(server.Handler)
}
// NewMetricsServer uses a distinct listener so public API routing cannot expose
// operational aggregates. It does not provide business or database endpoints.
func NewMetricsServer(addr string, metrics *observability.Registry) *http.Server {
mux := http.NewServeMux()
if metrics != nil {
mux.Handle("GET /metrics", metrics)
}
return &http.Server{Addr: addr, Handler: mux, ReadHeaderTimeout: 2 * time.Second,
ReadTimeout: 3 * time.Second, WriteTimeout: 5 * time.Second, IdleTimeout: 15 * time.Second, MaxHeaderBytes: 8 << 10}
}