Skip to content

Commit 4a48799

Browse files
committed
feat: prioritize premium 5h recovery windows
1 parent e0919ce commit 4a48799

12 files changed

Lines changed: 266 additions & 35 deletions

File tree

admin/handler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ type schedulerBreakdownResponse struct {
357357
FailurePenalty float64 `json:"failure_penalty"`
358358
SuccessBonus float64 `json:"success_bonus"`
359359
UsagePenalty7d float64 `json:"usage_penalty_7d"`
360+
UsageUrgencyBonus5h float64 `json:"usage_urgency_bonus_5h"`
360361
LatencyPenalty float64 `json:"latency_penalty"`
361362
SuccessRatePenalty float64 `json:"success_rate_penalty"`
362363
}
@@ -430,6 +431,7 @@ func (h *Handler) ListAccounts(c *gin.Context) {
430431
FailurePenalty: debug.Breakdown.FailurePenalty,
431432
SuccessBonus: debug.Breakdown.SuccessBonus,
432433
UsagePenalty7d: debug.Breakdown.UsagePenalty7d,
434+
UsageUrgencyBonus5h: debug.Breakdown.UsageUrgencyBonus5h,
433435
LatencyPenalty: debug.Breakdown.LatencyPenalty,
434436
SuccessRatePenalty: debug.Breakdown.SuccessRatePenalty,
435437
}

admin/responses_test.go

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -384,31 +384,31 @@ func TestAccountsResponse(t *testing.T) {
384384

385385
func TestAccountResponseFields(t *testing.T) {
386386
resp := accountResponse{
387-
ID: 1,
388-
Name: "Test Account",
389-
Email: "test@example.com",
390-
PlanType: "plus",
391-
Status: "active",
392-
HealthTier: "healthy",
393-
SchedulerScore: 95.5,
394-
ConcurrencyCap: 4,
395-
ProxyURL: "http://proxy:8080",
396-
CreatedAt: time.Now().Format(time.RFC3339),
397-
UpdatedAt: time.Now().Format(time.RFC3339),
398-
ActiveRequests: 2,
399-
TotalRequests: 100,
400-
LastUsedAt: time.Now().Format(time.RFC3339),
401-
SuccessRequests: 95,
402-
ErrorRequests: 5,
403-
UsagePercent7d: floatPtr(75.5),
404-
UsagePercent5h: floatPtr(50.0),
405-
Reset5hAt: time.Now().Format(time.RFC3339),
406-
Reset7dAt: time.Now().Add(7 * 24 * time.Hour).Format(time.RFC3339),
407-
ScoreBreakdown: schedulerBreakdownResponse{},
408-
LastUnauthorizedAt: time.Now().Format(time.RFC3339),
409-
LastRateLimitedAt: time.Now().Format(time.RFC3339),
410-
LastTimeoutAt: time.Now().Format(time.RFC3339),
411-
LastServerErrorAt: time.Now().Format(time.RFC3339),
387+
ID: 1,
388+
Name: "Test Account",
389+
Email: "test@example.com",
390+
PlanType: "plus",
391+
Status: "active",
392+
HealthTier: "healthy",
393+
SchedulerScore: 95.5,
394+
ConcurrencyCap: 4,
395+
ProxyURL: "http://proxy:8080",
396+
CreatedAt: time.Now().Format(time.RFC3339),
397+
UpdatedAt: time.Now().Format(time.RFC3339),
398+
ActiveRequests: 2,
399+
TotalRequests: 100,
400+
LastUsedAt: time.Now().Format(time.RFC3339),
401+
SuccessRequests: 95,
402+
ErrorRequests: 5,
403+
UsagePercent7d: floatPtr(75.5),
404+
UsagePercent5h: floatPtr(50.0),
405+
Reset5hAt: time.Now().Format(time.RFC3339),
406+
Reset7dAt: time.Now().Add(7 * 24 * time.Hour).Format(time.RFC3339),
407+
ScoreBreakdown: schedulerBreakdownResponse{},
408+
LastUnauthorizedAt: time.Now().Format(time.RFC3339),
409+
LastRateLimitedAt: time.Now().Format(time.RFC3339),
410+
LastTimeoutAt: time.Now().Format(time.RFC3339),
411+
LastServerErrorAt: time.Now().Format(time.RFC3339),
412412
}
413413

414414
if resp.Name != "Test Account" {
@@ -435,6 +435,7 @@ func TestSchedulerBreakdownResponse(t *testing.T) {
435435
FailurePenalty: 1.0,
436436
SuccessBonus: 4.0,
437437
UsagePenalty7d: 8.0,
438+
UsageUrgencyBonus5h: 6.0,
438439
LatencyPenalty: 2.5,
439440
SuccessRatePenalty: 1.5,
440441
}
@@ -445,6 +446,9 @@ func TestSchedulerBreakdownResponse(t *testing.T) {
445446
if resp.SuccessBonus != 4.0 {
446447
t.Errorf("SuccessBonus = %v, want 4.0", resp.SuccessBonus)
447448
}
449+
if resp.UsageUrgencyBonus5h != 6.0 {
450+
t.Errorf("UsageUrgencyBonus5h = %v, want 6.0", resp.UsageUrgencyBonus5h)
451+
}
448452
}
449453

450454
// ==================== Usage Logs Response Tests ====================

auth/fast_scheduler.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package auth
22

33
import (
4+
"math"
45
"sort"
56
"sync"
67
"sync/atomic"
@@ -248,8 +249,8 @@ func (s *FastScheduler) scanRangeLocked(expectedTier AccountHealthTier, rangeSta
248249
if filter != nil && !filter(entry.acc) {
249250
continue
250251
}
251-
tier, _, limit, _, available := entry.acc.fastSchedulerSnapshot(baseLimit, now)
252-
if tier != expectedTier {
252+
tier, dispatchScore, limit, proven, available := entry.acc.fastSchedulerSnapshot(baseLimit, now)
253+
if tier != expectedTier || proven != entry.proven || math.Abs(dispatchScore-entry.dispatchScore) >= 1 {
253254
s.removeLocked(entry.dbID)
254255
if available && limit > 0 {
255256
s.insertLocked(entry.acc, now)

auth/fast_scheduler_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,68 @@ func TestFastSchedulerProvenPhaseUsesTotalRequestsOnly(t *testing.T) {
383383
}
384384
}
385385

386+
func TestFastSchedulerPrefersPremium5hResetSoonWithinTier(t *testing.T) {
387+
now := time.Now()
388+
later := newFastSchedulerTestAccount(1, HealthTierHealthy, 150, 1)
389+
later.PlanType = "plus"
390+
later.UsagePercent5h = 25
391+
later.UsagePercent5hValid = true
392+
later.Reset5hAt = now.Add(5 * time.Hour)
393+
394+
soon := newFastSchedulerTestAccount(2, HealthTierHealthy, 150, 1)
395+
soon.PlanType = "plus"
396+
soon.UsagePercent5h = 25
397+
soon.UsagePercent5hValid = true
398+
soon.Reset5hAt = now.Add(30 * time.Minute)
399+
400+
scheduler := NewFastScheduler(1)
401+
scheduler.Rebuild([]*Account{later, soon})
402+
403+
got := scheduler.Acquire()
404+
if got == nil {
405+
t.Fatal("Acquire() returned nil")
406+
}
407+
defer scheduler.Release(got)
408+
409+
if got.DBID != soon.DBID {
410+
t.Fatalf("Acquire() picked dbID=%d, want reset-soon account %d", got.DBID, soon.DBID)
411+
}
412+
}
413+
414+
func TestPersistUsageSnapshot5hOnlyUpdatesFastSchedulerPriority(t *testing.T) {
415+
now := time.Now()
416+
later := newFastSchedulerTestAccount(1, HealthTierHealthy, 150, 1)
417+
later.PlanType = "plus"
418+
later.UsagePercent5h = 25
419+
later.UsagePercent5hValid = true
420+
later.Reset5hAt = now.Add(5 * time.Hour)
421+
422+
soon := newFastSchedulerTestAccount(2, HealthTierHealthy, 150, 1)
423+
soon.PlanType = "plus"
424+
soon.UsagePercent5h = 25
425+
soon.UsagePercent5hValid = true
426+
soon.Reset5hAt = now.Add(5 * time.Hour)
427+
428+
store := &Store{
429+
accounts: []*Account{later, soon},
430+
maxConcurrency: 1,
431+
}
432+
store.SetFastSchedulerEnabled(true)
433+
434+
soon.SetUsageSnapshot5h(25, time.Now().Add(30*time.Minute))
435+
store.PersistUsageSnapshot5hOnly(soon)
436+
437+
got := store.Next()
438+
if got == nil {
439+
t.Fatal("Next() returned nil")
440+
}
441+
defer store.Release(got)
442+
443+
if got.DBID != soon.DBID {
444+
t.Fatalf("Next() picked dbID=%d, want reset-soon account %d", got.DBID, soon.DBID)
445+
}
446+
}
447+
386448
func TestStoreFastSchedulerToggle(t *testing.T) {
387449
cooling := newFastSchedulerTestAccount(1, HealthTierWarm, 80, 1)
388450
cooling.Status = StatusCooldown

auth/premium_rate_limit.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func (a *Account) GetUsageSnapshot5h() (pct float64, resetAt time.Time, ok bool)
9898

9999
// PersistUsageSnapshot5hOnly 持久化仅包含 5h 数据的用量快照。
100100
func (s *Store) PersistUsageSnapshot5hOnly(acc *Account) {
101-
if acc == nil || s == nil || s.db == nil {
101+
if acc == nil || s == nil {
102102
return
103103
}
104104

@@ -112,6 +112,12 @@ func (s *Store) PersistUsageSnapshot5hOnly(acc *Account) {
112112
acc.UsageUpdatedAt = updatedAt
113113
acc.mu.Unlock()
114114

115+
s.fastSchedulerUpdate(acc)
116+
117+
if s.db == nil {
118+
return
119+
}
120+
115121
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
116122
defer cancel()
117123
if err := s.db.UpdateUsageSnapshot5h(ctx, acc.DBID, pct5h, reset5hAt, updatedAt); err != nil {

auth/store.go

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ const (
122122
defaultBackgroundRefreshInterval = 2 * time.Minute
123123
defaultUsageProbeMaxAge = 10 * time.Minute
124124
defaultRecoveryProbeInterval = 30 * time.Minute
125+
premium5hUrgencyWindow = 4 * time.Hour
126+
premium5hUrgencyMaxBonus = 25.0
127+
premium5hUrgencyMinRemainingPct = 5.0
128+
premium5hUrgencyFullRemainingPct = 50.0
125129
)
126130

127131
// SchedulerBreakdown 调度评分拆解
@@ -134,6 +138,7 @@ type SchedulerBreakdown struct {
134138
SuccessBonus float64
135139
ProvenBonus float64 // 经过验证的账号(TotalRequests > 10)加分
136140
UsagePenalty7d float64
141+
UsageUrgencyBonus5h float64
137142
LatencyPenalty float64
138143
SuccessRatePenalty float64 // 滑动窗口成功率惩罚
139144
}
@@ -388,8 +393,7 @@ func linearDecay(base float64, elapsed, window time.Duration) float64 {
388393
return base * (1.0 - float64(elapsed)/float64(window))
389394
}
390395

391-
func (a *Account) schedulerBreakdownLocked() SchedulerBreakdown {
392-
now := time.Now()
396+
func (a *Account) schedulerBreakdownLocked(now time.Time) SchedulerBreakdown {
393397
breakdown := SchedulerBreakdown{}
394398
premium5hLimited := a.premium5hRateLimitedLocked(now)
395399

@@ -457,6 +461,51 @@ func (a *Account) schedulerBreakdownLocked() SchedulerBreakdown {
457461
return breakdown
458462
}
459463

464+
func (a *Account) premium5hUsageUrgencyBonusLocked(now time.Time) float64 {
465+
if !isPremium5hPlan(a.PlanType) {
466+
return 0
467+
}
468+
if !a.UsagePercent5hValid || a.Reset5hAt.IsZero() {
469+
return 0
470+
}
471+
if a.UsagePercent5h >= 100 || a.premium5hRateLimitedLocked(now) {
472+
return 0
473+
}
474+
if a.AccessToken == "" || a.Status == StatusError || a.HealthTier == HealthTierBanned {
475+
return 0
476+
}
477+
if atomic.LoadInt32(&a.DispatchPaused) != 0 {
478+
return 0
479+
}
480+
if a.Status == StatusCooldown && now.Before(a.CooldownUtil) {
481+
return 0
482+
}
483+
if a.usageExhaustedLocked() {
484+
return 0
485+
}
486+
487+
timeRemaining := a.Reset5hAt.Sub(now)
488+
if timeRemaining <= 0 || timeRemaining > premium5hUrgencyWindow {
489+
return 0
490+
}
491+
492+
quotaRemaining := 100 - a.UsagePercent5h
493+
if quotaRemaining <= premium5hUrgencyMinRemainingPct {
494+
return 0
495+
}
496+
497+
timeFactor := 1 - float64(timeRemaining)/float64(premium5hUrgencyWindow)
498+
quotaFactor := quotaRemaining / premium5hUrgencyFullRemainingPct
499+
if quotaFactor > 1 {
500+
quotaFactor = 1
501+
}
502+
if quotaFactor < 0 {
503+
quotaFactor = 0
504+
}
505+
506+
return premium5hUrgencyMaxBonus * timeFactor * quotaFactor
507+
}
508+
460509
func (a *Account) effectiveBaseConcurrencyLocked(storeBaseLimit int64) int64 {
461510
if a.BaseConcurrencyOverride != nil && *a.BaseConcurrencyOverride > 0 {
462511
return *a.BaseConcurrencyOverride
@@ -501,7 +550,7 @@ func (a *Account) effectiveScoreBiasLocked(now time.Time, tier AccountHealthTier
501550

502551
func (a *Account) recomputeSchedulerLocked(baseLimit int64) {
503552
now := time.Now()
504-
breakdown := a.schedulerBreakdownLocked()
553+
breakdown := a.schedulerBreakdownLocked(now)
505554
score := 100.0 -
506555
breakdown.UnauthorizedPenalty -
507556
breakdown.RateLimitPenalty -
@@ -545,7 +594,10 @@ func (a *Account) recomputeSchedulerLocked(baseLimit int64) {
545594

546595
baseConcurrencyEffective := a.effectiveBaseConcurrencyLocked(baseLimit)
547596
scoreBiasEffective := a.effectiveScoreBiasLocked(now, tier)
548-
dispatchScore := score + float64(scoreBiasEffective)
597+
if a.dispatchBonusEligibleLocked(now, tier) {
598+
breakdown.UsageUrgencyBonus5h = a.premium5hUsageUrgencyBonusLocked(now)
599+
}
600+
dispatchScore := score + float64(scoreBiasEffective) + breakdown.UsageUrgencyBonus5h
549601

550602
a.HealthTier = tier
551603
a.SchedulerScore = score
@@ -1045,6 +1097,11 @@ func (a *Account) GetSchedulerDebugSnapshot(baseLimit int64) SchedulerDebugSnaps
10451097
defer a.mu.Unlock()
10461098

10471099
a.recomputeSchedulerLocked(baseLimit)
1100+
now := time.Now()
1101+
breakdown := a.schedulerBreakdownLocked(now)
1102+
if a.dispatchBonusEligibleLocked(now, a.HealthTier) {
1103+
breakdown.UsageUrgencyBonus5h = a.premium5hUsageUrgencyBonusLocked(now)
1104+
}
10481105
return SchedulerDebugSnapshot{
10491106
HealthTier: string(a.HealthTier),
10501107
SchedulerScore: a.SchedulerScore,
@@ -1054,7 +1111,7 @@ func (a *Account) GetSchedulerDebugSnapshot(baseLimit int64) SchedulerDebugSnaps
10541111
BaseConcurrencyOverride: cloneInt64Ptr(a.BaseConcurrencyOverride),
10551112
BaseConcurrencyEffective: a.BaseConcurrencyEffective,
10561113
DynamicConcurrencyLimit: a.DynamicConcurrencyLimit,
1057-
Breakdown: a.schedulerBreakdownLocked(),
1114+
Breakdown: breakdown,
10581115
LastUnauthorizedAt: a.LastUnauthorizedAt,
10591116
LastRateLimitedAt: a.LastRateLimitedAt,
10601117
LastTimeoutAt: a.LastTimeoutAt,
@@ -2665,6 +2722,7 @@ func (s *Store) PersistUsageSnapshot(acc *Account, pct7d float64) {
26652722

26662723
now := time.Now()
26672724
acc.SetUsageSnapshot(pct7d, now)
2725+
s.fastSchedulerUpdate(acc)
26682726

26692727
if s.db == nil {
26702728
return

0 commit comments

Comments
 (0)