Files
gpt-load/internal/control/validation.go
T

324 lines
7.9 KiB
Go

package control
import (
"context"
"crypto/sha256"
"encoding/binary"
"hash"
"net/textproto"
"sort"
"strings"
"sync"
"github.com/sirupsen/logrus"
"gpt-load/internal/dialect"
"gpt-load/internal/health"
"gpt-load/internal/platform/encryption"
"gpt-load/internal/protocol"
"gpt-load/internal/state"
)
const validationConcurrency = 8
type validationSweep interface {
Validate(context.Context)
}
type validationRegistry interface {
BlacklistedKeys() []state.KeyRef
RecoverIfMatch(ref state.KeyRef, weight int) bool
}
type statsResetter interface {
Reset(uint)
}
type snapshotSource interface {
Current() *state.ConfigSnapshot
WithCurrentSnapshot(func(*state.ConfigSnapshot) bool) bool
}
type credentialDecryptor interface {
Decrypt(string) (string, error)
}
type validationWorker struct {
snapshots snapshotSource
registry validationRegistry
stats statsResetter
mutations keyMutationCoordinator
decryptor credentialDecryptor
dialects dialect.Set
}
type groupValidationSignature [sha256.Size]byte
type groupValidationTarget struct {
protocol protocol.Protocol
model string
signature groupValidationSignature
}
var _ validationSweep = (*validationWorker)(nil)
func newValidationWorker(
manager *state.Manager,
registry *state.KeyRegistry,
stats *health.StatsStore,
mutations *health.MutationCoordinator,
decryptor encryption.Service,
dialects dialect.Set,
) *validationWorker {
return &validationWorker{
snapshots: manager,
registry: registry,
stats: stats,
mutations: mutations,
decryptor: decryptor,
dialects: dialects,
}
}
func (worker *validationWorker) Validate(ctx context.Context) {
if worker == nil || worker.snapshots == nil || worker.registry == nil {
return
}
snapshot := worker.snapshots.Current()
if snapshot == nil {
return
}
refs := worker.registry.BlacklistedKeys()
if len(refs) == 0 {
return
}
concurrency := min(validationConcurrency, len(refs))
jobs := make(chan state.KeyRef)
var workers sync.WaitGroup
workers.Add(concurrency)
for range concurrency {
go func() {
defer workers.Done()
worker.consumeValidationJobs(ctx, snapshot, jobs)
}()
}
dispatch:
for _, ref := range refs {
if ctx.Err() != nil {
break
}
select {
case <-ctx.Done():
break dispatch
case jobs <- ref:
}
}
close(jobs)
workers.Wait()
}
func (worker *validationWorker) consumeValidationJobs(
ctx context.Context,
snapshot *state.ConfigSnapshot,
jobs <-chan state.KeyRef,
) {
for {
select {
case <-ctx.Done():
return
case ref, ok := <-jobs:
if !ok || ctx.Err() != nil {
return
}
worker.validateRef(ctx, snapshot, ref)
}
}
}
func (worker *validationWorker) validateRef(ctx context.Context, snapshot *state.ConfigSnapshot, ref state.KeyRef) {
if ctx.Err() != nil {
return
}
group, ok := snapshot.Groups[ref.GroupID]
if !ok {
logValidationFailure(ref, "", "missing_group")
return
}
target, ok := buildGroupValidationTarget(group)
if !ok && len(group.Protocols) == 0 {
logValidationFailure(ref, "", "missing_protocol")
return
}
if !ok {
logValidationFailure(ref, string(group.Protocols[0]), "missing_model")
return
}
selectedDialect, ok := worker.dialects[target.protocol]
if !ok || selectedDialect == nil {
logValidationFailure(ref, string(target.protocol), "missing_dialect")
return
}
if worker.decryptor == nil {
logValidationFailure(ref, string(target.protocol), "decrypt")
return
}
if worker.stats == nil {
logValidationFailure(ref, string(target.protocol), "conditional_recover")
return
}
apiKey, err := worker.decryptor.Decrypt(ref.EncryptedValue)
if err != nil {
if ctx.Err() == nil {
logValidationFailure(ref, string(target.protocol), "decrypt")
}
return
}
if err := selectedDialect.Probe(
ctx,
group.UpstreamURL,
apiKey,
group.HeaderRules,
target.model,
); err != nil {
if ctx.Err() == nil {
logValidationFailure(ref, string(target.protocol), "probe")
}
return
}
if worker.mutations == nil {
if ctx.Err() == nil {
logValidationFailure(ref, string(target.protocol), "conditional_recover")
}
return
}
// This callback follows Manager publishMu -> coordinator stripe ->
// Registry/Stats locks. Keep it to current reads, pure signature work, and
// coordinated recover/reset; decrypt, probe, DB/network, and logging stay
// outside the publication boundary.
recovered := worker.snapshots.WithCurrentSnapshot(func(current *state.ConfigSnapshot) bool {
if current == nil {
return false
}
currentGroup, exists := current.Groups[ref.GroupID]
if !exists {
return false
}
currentTarget, valid := buildGroupValidationTarget(currentGroup)
if !valid || currentTarget.signature != target.signature {
return false
}
var matched bool
worker.mutations.Do(ref.ID, func() {
matched = worker.registry.RecoverIfMatch(ref, state.DefaultWeight)
if matched {
worker.stats.Reset(ref.ID)
}
})
return matched
})
if !recovered && ctx.Err() == nil {
logValidationFailure(ref, string(target.protocol), "conditional_recover")
}
}
func buildGroupValidationTarget(group state.GroupView) (groupValidationTarget, bool) {
if len(group.Protocols) == 0 {
return groupValidationTarget{}, false
}
selectedProtocol := group.Protocols[0]
probeModel := strings.TrimSpace(group.ValidationModel)
if probeModel == "" && len(group.Models) > 0 {
probeModel = strings.TrimSpace(group.Models[0].ID)
}
if probeModel == "" {
return groupValidationTarget{}, false
}
return groupValidationTarget{
protocol: selectedProtocol,
model: probeModel,
signature: computeGroupValidationSignature(group, selectedProtocol, probeModel),
}, true
}
func computeGroupValidationSignature(
group state.GroupView,
selectedProtocol protocol.Protocol,
probeModel string,
) groupValidationSignature {
hasher := sha256.New()
writeValidationSignatureUint64(hasher, uint64(group.ID))
writeValidationSignaturePart(hasher, []byte(group.UpstreamURL))
writeValidationSignaturePart(hasher, []byte(selectedProtocol))
writeValidationSignaturePart(hasher, []byte(probeModel))
type headerSetPart struct {
name string
value string
}
setParts := make([]headerSetPart, 0, len(group.HeaderRules.Set))
for name, value := range group.HeaderRules.Set {
setParts = append(setParts, headerSetPart{
name: normalizeValidationHeaderName(name),
value: value,
})
}
sort.Slice(setParts, func(i, j int) bool {
if setParts[i].name != setParts[j].name {
return setParts[i].name < setParts[j].name
}
return setParts[i].value < setParts[j].value
})
writeValidationSignatureUint64(hasher, uint64(len(setParts)))
for _, part := range setParts {
writeValidationSignaturePart(hasher, []byte(part.name))
writeValidationSignaturePart(hasher, []byte(part.value))
}
removeParts := make([]string, len(group.HeaderRules.Remove))
for index, name := range group.HeaderRules.Remove {
removeParts[index] = normalizeValidationHeaderName(name)
}
sort.Strings(removeParts)
writeValidationSignatureUint64(hasher, uint64(len(removeParts)))
for _, name := range removeParts {
writeValidationSignaturePart(hasher, []byte(name))
}
var signature groupValidationSignature
copy(signature[:], hasher.Sum(nil))
return signature
}
func writeValidationSignatureUint64(hasher hash.Hash, value uint64) {
var encoded [8]byte
binary.BigEndian.PutUint64(encoded[:], value)
writeValidationSignaturePart(hasher, encoded[:])
}
func writeValidationSignaturePart(hasher hash.Hash, value []byte) {
var encodedLength [8]byte
binary.BigEndian.PutUint64(encodedLength[:], uint64(len(value)))
_, _ = hasher.Write(encodedLength[:])
_, _ = hasher.Write(value)
}
func normalizeValidationHeaderName(name string) string {
return strings.ToLower(textproto.CanonicalMIMEHeaderKey(name))
}
func logValidationFailure(ref state.KeyRef, protocol, stage string) {
logrus.WithFields(logrus.Fields{
"key_id": ref.ID,
"group_id": ref.GroupID,
"protocol": protocol,
"stage": stage,
}).Warn("validation failed")
}