mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #6320 from spongehah/fix/runtime-block-honor-persisted-cooldown
fix(schedule): 选号按持久化冷却条件清除本机账号级阻断
This commit is contained in:
@@ -476,8 +476,94 @@ func (s *OpenAIGatewayService) isOpenAIAccountModelRuntimeBlocked(account *Accou
|
||||
return state.isBlocked(account.ID, openAIAccountModelTransientModel(canonicalModel), time.Now())
|
||||
}
|
||||
|
||||
func accountPersistedSchedulingCooldownActive(account *Account) bool {
|
||||
if account == nil {
|
||||
return false
|
||||
}
|
||||
now := time.Now()
|
||||
if account.TempUnschedulableUntil != nil && now.Before(*account.TempUnschedulableUntil) {
|
||||
return true
|
||||
}
|
||||
if account.RateLimitResetAt != nil && now.Before(*account.RateLimitResetAt) {
|
||||
return true
|
||||
}
|
||||
if account.OverloadUntil != nil && now.Before(*account.OverloadUntil) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type openAIAccountRuntimeBlockSnapshot struct {
|
||||
until time.Time
|
||||
generation uint64
|
||||
blocked bool
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) peekOpenAIAccountRuntimeBlock(account *Account) openAIAccountRuntimeBlockSnapshot {
|
||||
if s == nil || !isOpenAIAccount(account) {
|
||||
return openAIAccountRuntimeBlockSnapshot{}
|
||||
}
|
||||
mu := s.openAIAccountRuntimeBlockLock(account.ID)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
value, ok := s.openaiAccountRuntimeBlockUntil.Load(account.ID)
|
||||
if !ok {
|
||||
return openAIAccountRuntimeBlockSnapshot{}
|
||||
}
|
||||
until, isTime := value.(time.Time)
|
||||
if !isTime || until.IsZero() || !time.Now().Before(until) {
|
||||
s.openaiAccountRuntimeBlockUntil.Delete(account.ID)
|
||||
s.openaiOAuth429RetryStartedAt.Delete(account.ID)
|
||||
s.openaiAccountRuntimeBlockGeneration.Store(account.ID, s.openaiAccountRuntimeBlockSequence.Add(1))
|
||||
return openAIAccountRuntimeBlockSnapshot{}
|
||||
}
|
||||
generation, _ := s.openaiAccountRuntimeBlockGeneration.Load(account.ID)
|
||||
gen, _ := generation.(uint64)
|
||||
return openAIAccountRuntimeBlockSnapshot{until: until, generation: gen, blocked: true}
|
||||
}
|
||||
|
||||
// clearOpenAIAccountRuntimeBlockIfUnchanged deletes the in-process account block
|
||||
// only when generation and deadline are unchanged. A newer block installed after
|
||||
// peek must be kept even if its deadline happens to match.
|
||||
func (s *OpenAIGatewayService) clearOpenAIAccountRuntimeBlockIfUnchanged(accountID int64, snapshot openAIAccountRuntimeBlockSnapshot) {
|
||||
if s == nil || accountID <= 0 || !snapshot.blocked {
|
||||
return
|
||||
}
|
||||
mu := s.openAIAccountRuntimeBlockLock(accountID)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
generation, ok := s.openaiAccountRuntimeBlockGeneration.Load(accountID)
|
||||
if !ok || generation != snapshot.generation {
|
||||
return
|
||||
}
|
||||
current, ok := s.openaiAccountRuntimeBlockUntil.Load(accountID)
|
||||
currentUntil, isTime := current.(time.Time)
|
||||
if !ok || !isTime || !currentUntil.Equal(snapshot.until) {
|
||||
return
|
||||
}
|
||||
s.openaiAccountRuntimeBlockUntil.Delete(accountID)
|
||||
s.openaiOAuth429RetryStartedAt.Delete(accountID)
|
||||
s.openaiAccountRuntimeBlockGeneration.Store(accountID, s.openaiAccountRuntimeBlockSequence.Add(1))
|
||||
}
|
||||
|
||||
// isOpenAIAccountRequestRuntimeBlocked treats persisted cooldown fields on the
|
||||
// scheduling Account as source of truth. When TempUnschedulableUntil,
|
||||
// RateLimitResetAt, and OverloadUntil are all inactive, a stale local account
|
||||
// block is dropped with generation+deadline CAS. Model-scoped transient blocks
|
||||
// are left alone. This is fail-open if a DB write failed or the snapshot has
|
||||
// not caught up yet: empty cooldown fields drop the local account-level block.
|
||||
func (s *OpenAIGatewayService) isOpenAIAccountRequestRuntimeBlocked(account *Account, requestedModel string) bool {
|
||||
return s != nil && (s.isOpenAIAccountRuntimeBlocked(account) || s.isOpenAIAccountModelRuntimeBlocked(account, requestedModel))
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
snapshot := s.peekOpenAIAccountRuntimeBlock(account)
|
||||
if snapshot.blocked {
|
||||
if accountPersistedSchedulingCooldownActive(account) {
|
||||
return true
|
||||
}
|
||||
s.clearOpenAIAccountRuntimeBlockIfUnchanged(account.ID, snapshot)
|
||||
}
|
||||
return s.isOpenAIAccountModelRuntimeBlocked(account, requestedModel)
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) recordOpenAIOAuth429() {
|
||||
|
||||
@@ -766,6 +766,45 @@ func TestOpenAIRuntimeBlock_ClearAccountSchedulingBlock(t *testing.T) {
|
||||
require.False(t, svc.isOpenAIAccountRuntimeBlocked(account))
|
||||
}
|
||||
|
||||
func TestRuntimeBlockHonorsClearedPersistedCooldown(t *testing.T) {
|
||||
svc := &OpenAIGatewayService{}
|
||||
account := &Account{ID: 92, Platform: PlatformGrok, Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true}
|
||||
svc.BlockAccountScheduling(account, time.Now().Add(30*time.Minute), "grok payment required")
|
||||
require.False(t, svc.isOpenAIAccountRequestRuntimeBlocked(account, "grok-3"))
|
||||
require.False(t, svc.isOpenAIAccountRuntimeBlocked(account))
|
||||
}
|
||||
|
||||
func TestRuntimeBlockConditionalClearSkipsNewerGeneration(t *testing.T) {
|
||||
svc := &OpenAIGatewayService{}
|
||||
account := &Account{ID: 94, Platform: PlatformGrok, Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true}
|
||||
firstUntil := time.Now().Add(10 * time.Minute)
|
||||
svc.BlockAccountScheduling(account, firstUntil, "stale")
|
||||
snapshot := svc.peekOpenAIAccountRuntimeBlock(account)
|
||||
require.True(t, snapshot.blocked)
|
||||
newerUntil := time.Now().Add(30 * time.Minute)
|
||||
svc.BlockAccountScheduling(account, newerUntil, "fresh")
|
||||
svc.clearOpenAIAccountRuntimeBlockIfUnchanged(account.ID, snapshot)
|
||||
require.True(t, svc.isOpenAIAccountRuntimeBlocked(account))
|
||||
require.False(t, svc.isOpenAIAccountRequestRuntimeBlocked(account, "grok-3"))
|
||||
require.False(t, svc.isOpenAIAccountRuntimeBlocked(account))
|
||||
}
|
||||
|
||||
func TestRuntimeBlockKeepsActivePersistedCooldown(t *testing.T) {
|
||||
svc := &OpenAIGatewayService{}
|
||||
until := time.Now().Add(30 * time.Minute)
|
||||
account := &Account{
|
||||
ID: 93,
|
||||
Platform: PlatformGrok,
|
||||
Type: AccountTypeOAuth,
|
||||
Status: StatusActive,
|
||||
Schedulable: true,
|
||||
TempUnschedulableUntil: &until,
|
||||
}
|
||||
svc.BlockAccountScheduling(account, until, "grok payment required")
|
||||
require.True(t, svc.isOpenAIAccountRequestRuntimeBlocked(account, "grok-3"))
|
||||
require.True(t, svc.isOpenAIAccountRuntimeBlocked(account))
|
||||
}
|
||||
|
||||
func TestShouldStopOpenAIOAuth429Failover_AfterBoundedFullWindows(t *testing.T) {
|
||||
svc := &OpenAIGatewayService{}
|
||||
account := &Account{ID: 42, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
|
||||
|
||||
@@ -111,6 +111,8 @@ func TestHandleOpenAITransientError_HardDisableStillBlocksWholeAccount(t *testin
|
||||
|
||||
svc.BlockAccountScheduling(account, time.Now().Add(time.Minute), "upstream_disable")
|
||||
|
||||
require.True(t, svc.isOpenAIAccountRequestRuntimeBlocked(account, "gpt-5.5"))
|
||||
require.True(t, svc.isOpenAIAccountRequestRuntimeBlocked(account, "gpt-5.6-sol"))
|
||||
require.True(t, svc.isOpenAIAccountRuntimeBlocked(account))
|
||||
require.False(t, svc.isOpenAIAccountRequestRuntimeBlocked(account, "gpt-5.5"))
|
||||
require.False(t, svc.isOpenAIAccountRequestRuntimeBlocked(account, "gpt-5.6-sol"))
|
||||
require.False(t, svc.isOpenAIAccountRuntimeBlocked(account))
|
||||
}
|
||||
|
||||
@@ -165,8 +165,9 @@ func (s *OpenAIGatewayService) tempUnscheduleOpenAITransportError(ctx context.Co
|
||||
until := time.Now().Add(openAITransportErrorTempUnschedDuration)
|
||||
reason := "upstream transport error (proxy/network): " + safeErr
|
||||
|
||||
// Immediate in-memory block (honoured by the scheduler at selection time),
|
||||
// effective even if the DB write below fails or the account cache lags.
|
||||
// Immediate in-memory block so this process skips the account until the
|
||||
// persisted cooldown is visible on the scheduling Account. Selection is
|
||||
// fail-open: empty snapshot/DB cooldown fields drop a stale local block.
|
||||
s.BlockAccountScheduling(account, until, "transport_error")
|
||||
|
||||
if s.accountRepo == nil {
|
||||
|
||||
Reference in New Issue
Block a user