da9c8334d8
- SSE Keepalive Ping (15s心跳防止代理断连) - Timing HTTP 头 (X-Timing-Queue/Inference/Total-Ms) - Adapter Request-ID 传播到后端 - Session 清理日志回调 - Server 安全加固 (ReadHeaderTimeout/MaxHeaderBytes 防 slowloris) - Usage Tracker 数据保留清理 (retentionDays + 定期清理) - Config Reload 后 Adapter Registry 更新 (RegisterIfAbsent + RWMutex) - Rate Limiter 空闲 Bucket 清理 (30分钟过期) - Shutdown Drain 超时可配置 (ShutdownDrainSeconds) - Config 模型字段校验增强 (provider/endpoint/actual_model) - Auth 过期 Key 自动清理 (5分钟扫描) - Admin API Rate Limiting - Adapter Health Check 独立超时 (每个 adapter 3s) - TCP 连接阶段超时 (DialContext 5s + KeepAlive 30s) - 幂等键缓存、审计日志、Gzip 中间件、CORS Expose Headers - Backpressure 响应头、熔断器 Prometheus 指标 - 连接池优化、Trace-ID 全链路传播
148 lines
3.5 KiB
Go
148 lines
3.5 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/edgeai/gateway/internal/handler"
|
|
)
|
|
|
|
// idempotencyEntry 幂等键缓存条目。
|
|
// 记录请求处理状态和响应,用于在重复请求时返回缓存结果。
|
|
type idempotencyEntry struct {
|
|
status int
|
|
response []byte
|
|
createdAt time.Time
|
|
inFlight bool // 正在处理中
|
|
}
|
|
|
|
const (
|
|
// idempotencyTTL 幂等键缓存存活时间。
|
|
idempotencyTTL = 10 * time.Minute
|
|
// idempotencyCleanupInterval 清理间隔。
|
|
idempotencyCleanupInterval = 5 * time.Minute
|
|
)
|
|
|
|
// checkIdempotency 检查幂等键,如果重复请求则返回缓存的响应。
|
|
// 如果是首次请求,返回 nil 表示可以继续处理。
|
|
// 如果是正在处理中的重复请求,返回 409 Conflict。
|
|
func (s *Server) checkIdempotency(w http.ResponseWriter, key, appID string) bool {
|
|
if key == "" {
|
|
return false
|
|
}
|
|
|
|
cacheKey := appID + ":" + key
|
|
|
|
s.idempotencyMu.Lock()
|
|
entry, exists := s.idempotencyCache[cacheKey]
|
|
if exists {
|
|
if entry.inFlight {
|
|
// 正在处理中,返回 409
|
|
s.idempotencyMu.Unlock()
|
|
handler.WriteError(w, handler.NewGatewayError(handler.ErrInvalidRequest, "duplicate idempotency key, request already in progress"))
|
|
return true
|
|
}
|
|
|
|
// 检查是否过期
|
|
if time.Since(entry.createdAt) > idempotencyTTL {
|
|
delete(s.idempotencyCache, cacheKey)
|
|
} else {
|
|
// 返回缓存的响应
|
|
s.idempotencyMu.Unlock()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("X-Idempotent-Replay", "true")
|
|
w.WriteHeader(entry.status)
|
|
w.Write(entry.response)
|
|
return true
|
|
}
|
|
}
|
|
|
|
// 标记为正在处理中
|
|
s.idempotencyCache[cacheKey] = &idempotencyEntry{
|
|
inFlight: true,
|
|
createdAt: time.Now(),
|
|
}
|
|
s.idempotencyMu.Unlock()
|
|
return false
|
|
}
|
|
|
|
// storeIdempotencyResult 存储幂等请求的响应结果。
|
|
func (s *Server) storeIdempotencyResult(key, appID string, status int, response []byte) {
|
|
if key == "" {
|
|
return
|
|
}
|
|
|
|
cacheKey := appID + ":" + key
|
|
|
|
s.idempotencyMu.Lock()
|
|
s.idempotencyCache[cacheKey] = &idempotencyEntry{
|
|
status: status,
|
|
response: response,
|
|
createdAt: time.Now(),
|
|
inFlight: false,
|
|
}
|
|
s.idempotencyMu.Unlock()
|
|
}
|
|
|
|
// cleanupIdempotencyCache 清理过期的幂等键缓存。
|
|
func (s *Server) cleanupIdempotencyCache() {
|
|
s.idempotencyMu.Lock()
|
|
defer s.idempotencyMu.Unlock()
|
|
|
|
now := time.Now()
|
|
for k, entry := range s.idempotencyCache {
|
|
if now.Sub(entry.createdAt) > idempotencyTTL {
|
|
delete(s.idempotencyCache, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// startIdempotencyCleanup 启动后台清理任务,定期清理过期的幂等键。
|
|
// 返回停止函数。
|
|
func (s *Server) startIdempotencyCleanup() func() {
|
|
ticker := time.NewTicker(idempotencyCleanupInterval)
|
|
stopCh := make(chan struct{})
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
s.cleanupIdempotencyCache()
|
|
case <-stopCh:
|
|
ticker.Stop()
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return func() {
|
|
close(stopCh)
|
|
}
|
|
}
|
|
|
|
// captureResponseWriter 捕获响应状态和响应体,用于幂等性缓存。
|
|
type captureResponseWriter struct {
|
|
http.ResponseWriter
|
|
statusCode int
|
|
body []byte
|
|
}
|
|
|
|
func (crw *captureResponseWriter) WriteHeader(code int) {
|
|
crw.statusCode = code
|
|
crw.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
func (crw *captureResponseWriter) Write(b []byte) (int, error) {
|
|
if crw.statusCode == 0 {
|
|
crw.statusCode = 200
|
|
}
|
|
crw.body = append(crw.body, b...)
|
|
return crw.ResponseWriter.Write(b)
|
|
}
|
|
|
|
func (crw *captureResponseWriter) Flush() {
|
|
if f, ok := crw.ResponseWriter.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|