feat(P1/P2): 完成TDD开发及P1/P2设计文档
## 设计文档 - multi_role_permission_design: 多角色权限设计 (CONDITIONAL GO) - audit_log_enhancement_design: 审计日志增强 (CONDITIONAL GO) - routing_strategy_template_design: 路由策略模板 (CONDITIONAL GO) - sso_saml_technical_research: SSO/SAML调研 (CONDITIONAL GO) - compliance_capability_package_design: 合规能力包设计 (CONDITIONAL GO) ## TDD开发成果 - IAM模块: supply-api/internal/iam/ (111个测试) - 审计日志模块: supply-api/internal/audit/ (40+测试) - 路由策略模块: gateway/internal/router/ (33+测试) - 合规能力包: gateway/internal/compliance/ + scripts/ci/compliance/ ## 规范文档 - parallel_agent_output_quality_standards: 并行Agent产出质量规范 - project_experience_summary: 项目经验总结 (v2) - 2026-04-02-p1-p2-tdd-execution-plan: TDD执行计划 ## 评审报告 - 5个CONDITIONAL GO设计文档评审报告 - fix_verification_report: 修复验证报告 - full_verification_report: 全面质量验证报告 - tdd_module_quality_verification: TDD模块质量验证 - tdd_execution_summary: TDD执行总结 依据: Superpowers执行框架 + TDD规范
This commit is contained in:
145
gateway/internal/router/fallback/fallback.go
Normal file
145
gateway/internal/router/fallback/fallback.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package fallback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"lijiaoqiao/gateway/internal/adapter"
|
||||
"lijiaoqiao/gateway/internal/router/strategy"
|
||||
)
|
||||
|
||||
// ErrAllTiersFailed 所有Fallback层级都失败
|
||||
var ErrAllTiersFailed = errors.New("all fallback tiers failed")
|
||||
|
||||
// ErrRateLimitExceeded 限流错误
|
||||
var ErrRateLimitExceeded = errors.New("rate limit exceeded")
|
||||
|
||||
// FallbackHandler Fallback处理器
|
||||
type FallbackHandler struct {
|
||||
tiers []TierConfig
|
||||
router FallbackRouter
|
||||
metrics FallbackMetrics
|
||||
providerGetter ProviderGetter
|
||||
}
|
||||
|
||||
// TierConfig Fallback层级配置
|
||||
type TierConfig struct {
|
||||
Tier int
|
||||
Providers []string
|
||||
TimeoutMs int64
|
||||
}
|
||||
|
||||
// FallbackMetrics Fallback指标接口
|
||||
type FallbackMetrics interface {
|
||||
RecordTakeoverMark(provider string, tier int)
|
||||
}
|
||||
|
||||
// ProviderGetter Provider获取器接口
|
||||
type ProviderGetter interface {
|
||||
GetProvider(name string) adapter.ProviderAdapter
|
||||
}
|
||||
|
||||
// FallbackRouter Fallback路由器接口
|
||||
type FallbackRouter interface {
|
||||
SelectProvider(ctx context.Context, req *strategy.RoutingRequest, providerName string) (*strategy.RoutingDecision, error)
|
||||
}
|
||||
|
||||
// NewFallbackHandler 创建Fallback处理器
|
||||
func NewFallbackHandler() *FallbackHandler {
|
||||
return &FallbackHandler{
|
||||
tiers: make([]TierConfig, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// SetTiers 设置Fallback层级
|
||||
func (h *FallbackHandler) SetTiers(tiers []TierConfig) {
|
||||
h.tiers = tiers
|
||||
}
|
||||
|
||||
// SetRouter 设置路由器
|
||||
func (h *FallbackHandler) SetRouter(router FallbackRouter) {
|
||||
h.router = router
|
||||
}
|
||||
|
||||
// SetMetrics 设置指标收集器
|
||||
func (h *FallbackHandler) SetMetrics(metrics FallbackMetrics) {
|
||||
h.metrics = metrics
|
||||
}
|
||||
|
||||
// SetProviderGetter 设置Provider获取器
|
||||
func (h *FallbackHandler) SetProviderGetter(getter ProviderGetter) {
|
||||
h.providerGetter = getter
|
||||
}
|
||||
|
||||
// Handle 处理Fallback
|
||||
func (h *FallbackHandler) Handle(ctx context.Context, req *strategy.RoutingRequest) (*strategy.RoutingDecision, error) {
|
||||
if len(h.tiers) == 0 {
|
||||
return nil, ErrAllTiersFailed
|
||||
}
|
||||
|
||||
// 按层级顺序尝试
|
||||
for _, tier := range h.tiers {
|
||||
decision, err := h.tryTier(ctx, req, tier)
|
||||
if err == nil {
|
||||
// 成功,记录指标
|
||||
if h.metrics != nil {
|
||||
h.metrics.RecordTakeoverMark(decision.Provider, tier.Tier)
|
||||
}
|
||||
return decision, nil
|
||||
}
|
||||
|
||||
// 检查是否是限流错误
|
||||
if errors.Is(err, ErrRateLimitExceeded) {
|
||||
// 限流错误立即返回,不继续降级
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 其他错误,尝试下一层级
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, ErrAllTiersFailed
|
||||
}
|
||||
|
||||
// tryTier 尝试单个层级
|
||||
func (h *FallbackHandler) tryTier(ctx context.Context, req *strategy.RoutingRequest, tier TierConfig) (*strategy.RoutingDecision, error) {
|
||||
for _, providerName := range tier.Providers {
|
||||
decision, err := h.router.SelectProvider(ctx, req, providerName)
|
||||
if err == nil {
|
||||
decision.TakeoverMark = true
|
||||
return decision, nil
|
||||
}
|
||||
|
||||
// 检查是否是限流错误
|
||||
if isRateLimitError(err) {
|
||||
return nil, ErrRateLimitExceeded
|
||||
}
|
||||
|
||||
// 其他错误,继续尝试下一个provider
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, ErrAllTiersFailed
|
||||
}
|
||||
|
||||
// isRateLimitError 判断是否是限流错误
|
||||
func isRateLimitError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
// 检查错误消息中是否包含rate limit
|
||||
return containsRateLimit(err.Error())
|
||||
}
|
||||
|
||||
func containsRateLimit(s string) bool {
|
||||
return len(s) > 0 && (contains(s, "rate limit") || contains(s, "ratelimit") || contains(s, "too many requests"))
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
192
gateway/internal/router/fallback/fallback_test.go
Normal file
192
gateway/internal/router/fallback/fallback_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package fallback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"lijiaoqiao/gateway/internal/router/strategy"
|
||||
)
|
||||
|
||||
// TestFallback_Tier1_Success 测试Tier1可用时直接返回
|
||||
func TestFallback_Tier1_Success(t *testing.T) {
|
||||
fb := NewFallbackHandler()
|
||||
|
||||
// 设置Tier1 provider
|
||||
fb.tiers = []TierConfig{
|
||||
{
|
||||
Tier: 1,
|
||||
Providers: []string{"ProviderA"},
|
||||
},
|
||||
}
|
||||
|
||||
// 创建mock router
|
||||
fb.router = &MockFallbackRouter{
|
||||
providers: map[string]*MockFallbackProvider{
|
||||
"ProviderA": {
|
||||
name: "ProviderA",
|
||||
available: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 设置metrics
|
||||
fb.metrics = &MockFallbackMetrics{}
|
||||
|
||||
req := &strategy.RoutingRequest{
|
||||
Model: "gpt-4",
|
||||
UserID: "user123",
|
||||
}
|
||||
|
||||
decision, err := fb.Handle(context.Background(), req)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, decision)
|
||||
assert.Equal(t, "ProviderA", decision.Provider, "Should select Tier1 provider")
|
||||
assert.True(t, decision.TakeoverMark, "TakeoverMark should be true")
|
||||
}
|
||||
|
||||
// TestFallback_Tier1_Fail_Tier2 测试Tier1失败时降级到Tier2
|
||||
func TestFallback_Tier1_Fail_Tier2(t *testing.T) {
|
||||
fb := NewFallbackHandler()
|
||||
|
||||
// 设置多级tier
|
||||
fb.tiers = []TierConfig{
|
||||
{Tier: 1, Providers: []string{"ProviderA"}},
|
||||
{Tier: 2, Providers: []string{"ProviderB"}},
|
||||
}
|
||||
|
||||
// Tier1不可用,Tier2可用
|
||||
fb.router = &MockFallbackRouter{
|
||||
providers: map[string]*MockFallbackProvider{
|
||||
"ProviderA": {
|
||||
name: "ProviderA",
|
||||
available: false, // Tier1 不可用
|
||||
},
|
||||
"ProviderB": {
|
||||
name: "ProviderB",
|
||||
available: true, // Tier2 可用
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fb.metrics = &MockFallbackMetrics{}
|
||||
|
||||
req := &strategy.RoutingRequest{
|
||||
Model: "gpt-4",
|
||||
UserID: "user123",
|
||||
}
|
||||
|
||||
decision, err := fb.Handle(context.Background(), req)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, decision)
|
||||
assert.Equal(t, "ProviderB", decision.Provider, "Should fallback to Tier2")
|
||||
}
|
||||
|
||||
// TestFallback_AllFail 测试全部失败返回错误
|
||||
func TestFallback_AllFail(t *testing.T) {
|
||||
fb := NewFallbackHandler()
|
||||
|
||||
fb.tiers = []TierConfig{
|
||||
{Tier: 1, Providers: []string{"ProviderA"}},
|
||||
{Tier: 2, Providers: []string{"ProviderB"}},
|
||||
}
|
||||
|
||||
// 所有provider都不可用
|
||||
fb.router = &MockFallbackRouter{
|
||||
providers: map[string]*MockFallbackProvider{
|
||||
"ProviderA": {name: "ProviderA", available: false},
|
||||
"ProviderB": {name: "ProviderB", available: false},
|
||||
},
|
||||
}
|
||||
|
||||
fb.metrics = &MockFallbackMetrics{}
|
||||
|
||||
req := &strategy.RoutingRequest{
|
||||
Model: "gpt-4",
|
||||
UserID: "user123",
|
||||
}
|
||||
|
||||
decision, err := fb.Handle(context.Background(), req)
|
||||
|
||||
assert.Error(t, err, "Should return error when all tiers fail")
|
||||
assert.Nil(t, decision)
|
||||
}
|
||||
|
||||
// TestFallback_RatelimitIntegration 测试Fallback与ratelimit集成
|
||||
func TestFallback_RatelimitIntegration(t *testing.T) {
|
||||
fb := NewFallbackHandler()
|
||||
|
||||
fb.tiers = []TierConfig{
|
||||
{Tier: 1, Providers: []string{"ProviderA"}},
|
||||
}
|
||||
|
||||
fb.router = &MockFallbackRouter{
|
||||
providers: map[string]*MockFallbackProvider{
|
||||
"ProviderA": {
|
||||
name: "ProviderA",
|
||||
available: true,
|
||||
rateLimitError: errors.New("rate limit exceeded"), // 触发ratelimit
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fb.metrics = &MockFallbackMetrics{}
|
||||
|
||||
req := &strategy.RoutingRequest{
|
||||
Model: "gpt-4",
|
||||
UserID: "user123",
|
||||
}
|
||||
|
||||
_, err := fb.Handle(context.Background(), req)
|
||||
|
||||
// 应该检测到ratelimit错误并返回
|
||||
assert.Error(t, err, "Should return error on rate limit")
|
||||
assert.Contains(t, err.Error(), "rate limit", "Error should mention rate limit")
|
||||
}
|
||||
|
||||
// MockFallbackRouter 用于测试的Mock Router
|
||||
type MockFallbackRouter struct {
|
||||
providers map[string]*MockFallbackProvider
|
||||
}
|
||||
|
||||
func (r *MockFallbackRouter) SelectProvider(ctx context.Context, req *strategy.RoutingRequest, providerName string) (*strategy.RoutingDecision, error) {
|
||||
provider, ok := r.providers[providerName]
|
||||
if !ok {
|
||||
return nil, errors.New("provider not found")
|
||||
}
|
||||
|
||||
if !provider.available {
|
||||
return nil, errors.New("provider not available")
|
||||
}
|
||||
|
||||
if provider.rateLimitError != nil {
|
||||
return nil, provider.rateLimitError
|
||||
}
|
||||
|
||||
return &strategy.RoutingDecision{
|
||||
Provider: providerName,
|
||||
TakeoverMark: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MockFallbackProvider 用于测试的Mock Provider
|
||||
type MockFallbackProvider struct {
|
||||
name string
|
||||
available bool
|
||||
rateLimitError error
|
||||
}
|
||||
|
||||
// MockFallbackMetrics 用于测试的Mock Metrics
|
||||
type MockFallbackMetrics struct {
|
||||
recordCalled bool
|
||||
tier int
|
||||
}
|
||||
|
||||
func (m *MockFallbackMetrics) RecordTakeoverMark(provider string, tier int) {
|
||||
m.recordCalled = true
|
||||
m.tier = tier
|
||||
}
|
||||
Reference in New Issue
Block a user