将 Worker 发现、路由画像、容量与执行传输抽象为平台无关接口,新增 Kubernetes 和静态容量适配器,并以 shadow 模式接入生产配置。 实现网络与容量评分、路由防抖、池队列、同步 Worker 租约、一次性执行令牌,以及提交状态不明时禁止重复分配的安全语义。 新增 0105 兼容迁移、管理接口、指标、OpenAPI 和回归测试。已执行全量 Go 测试、go vet、OpenAPI、迁移安全、Compose 与 Kustomize 验证。
138 lines
4.1 KiB
Go
138 lines
4.1 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"net/http"
|
|
"net/http/httptrace"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/executionpool"
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
)
|
|
|
|
type passiveRouteObserver struct {
|
|
mu sync.Mutex
|
|
samples int
|
|
successes int
|
|
connectTLS []time.Duration
|
|
uploadBytesRate []float64
|
|
}
|
|
|
|
type passiveRouteRoundTripper struct {
|
|
base http.RoundTripper
|
|
observer *passiveRouteObserver
|
|
}
|
|
|
|
func (observer *passiveRouteObserver) wrap(client *http.Client) *http.Client {
|
|
if client == nil {
|
|
client = http.DefaultClient
|
|
}
|
|
cloned := *client
|
|
base := cloned.Transport
|
|
if base == nil {
|
|
base = http.DefaultTransport
|
|
}
|
|
cloned.Transport = passiveRouteRoundTripper{base: base, observer: observer}
|
|
return &cloned
|
|
}
|
|
|
|
func (transport passiveRouteRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
startedAt := time.Now()
|
|
var connectStartedAt time.Time
|
|
var connectDuration time.Duration
|
|
var tlsStartedAt time.Time
|
|
var tlsDuration time.Duration
|
|
var wroteRequestAt time.Time
|
|
trace := &httptrace.ClientTrace{
|
|
ConnectStart: func(_, _ string) { connectStartedAt = time.Now() },
|
|
ConnectDone: func(_, _ string, _ error) {
|
|
if !connectStartedAt.IsZero() {
|
|
connectDuration = time.Since(connectStartedAt)
|
|
}
|
|
},
|
|
TLSHandshakeStart: func() { tlsStartedAt = time.Now() },
|
|
TLSHandshakeDone: func(tls.ConnectionState, error) {
|
|
if !tlsStartedAt.IsZero() {
|
|
tlsDuration = time.Since(tlsStartedAt)
|
|
}
|
|
},
|
|
WroteRequest: func(httptrace.WroteRequestInfo) { wroteRequestAt = time.Now() },
|
|
}
|
|
traced := request.Clone(httptrace.WithClientTrace(request.Context(), trace))
|
|
response, err := transport.base.RoundTrip(traced)
|
|
transport.observer.record(
|
|
err == nil,
|
|
connectDuration+tlsDuration,
|
|
request.ContentLength,
|
|
startedAt,
|
|
wroteRequestAt,
|
|
)
|
|
return response, err
|
|
}
|
|
|
|
func (observer *passiveRouteObserver) record(success bool, connectTLS time.Duration, contentLength int64, startedAt, wroteRequestAt time.Time) {
|
|
observer.mu.Lock()
|
|
defer observer.mu.Unlock()
|
|
observer.samples++
|
|
if success {
|
|
observer.successes++
|
|
}
|
|
if connectTLS > 0 {
|
|
observer.connectTLS = append(observer.connectTLS, connectTLS)
|
|
}
|
|
if contentLength > 0 && !wroteRequestAt.IsZero() && wroteRequestAt.After(startedAt) {
|
|
observer.uploadBytesRate = append(observer.uploadBytesRate, float64(contentLength)/wroteRequestAt.Sub(startedAt).Seconds())
|
|
}
|
|
}
|
|
|
|
func (observer *passiveRouteObserver) snapshot(poolID, routeProfileKey string) executionpool.RouteObservation {
|
|
observer.mu.Lock()
|
|
defer observer.mu.Unlock()
|
|
return executionpool.RouteObservation{
|
|
PoolID: poolID, RouteProfileKey: routeProfileKey,
|
|
SampleCount: observer.samples, SuccessCount: observer.successes,
|
|
ConnectTLSP95: durationP95(observer.connectTLS),
|
|
UploadBytesPerSecond: floatP95(observer.uploadBytesRate),
|
|
}
|
|
}
|
|
|
|
func durationP95(values []time.Duration) time.Duration {
|
|
if len(values) == 0 {
|
|
return 0
|
|
}
|
|
copyOfValues := append([]time.Duration(nil), values...)
|
|
sort.Slice(copyOfValues, func(i, j int) bool { return copyOfValues[i] < copyOfValues[j] })
|
|
return copyOfValues[(len(copyOfValues)*95+99)/100-1]
|
|
}
|
|
|
|
func floatP95(values []float64) float64 {
|
|
if len(values) == 0 {
|
|
return 0
|
|
}
|
|
copyOfValues := append([]float64(nil), values...)
|
|
sort.Float64s(copyOfValues)
|
|
return copyOfValues[(len(copyOfValues)*95+99)/100-1]
|
|
}
|
|
|
|
func (s *Service) recordPassiveRouteObservation(task store.GatewayTask, observer *passiveRouteObserver) {
|
|
if observer == nil || !s.routingEnabled() || task.RouteProfileKey == "" {
|
|
return
|
|
}
|
|
poolID := task.AssignedPoolID
|
|
if poolID == "" && s.cfg.RunsAsyncExecutionWorker() {
|
|
poolID = s.cfg.ExecutionPoolID
|
|
}
|
|
observation := observer.snapshot(poolID, task.RouteProfileKey)
|
|
if observation.PoolID == "" || observation.SampleCount == 0 {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
|
defer cancel()
|
|
if err := s.coordinationStore.RecordRouteObservation(ctx, observation); err != nil && s.logger != nil {
|
|
s.logger.Warn("record passive route observation failed", "error_category", "route_observation_failed")
|
|
}
|
|
}
|