| package handler |
|
|
| import ( |
| "context" |
| "net/http" |
| "time" |
|
|
| "github.com/Wei-Shaw/sub2api/internal/pkg/logger" |
| "github.com/Wei-Shaw/sub2api/internal/service" |
| "go.uber.org/zap" |
| ) |
|
|
| |
| |
| type TempUnscheduler interface { |
| TempUnscheduleRetryableError(ctx context.Context, accountID int64, failoverErr *service.UpstreamFailoverError) |
| } |
|
|
| |
| type FailoverAction int |
|
|
| const ( |
| |
| FailoverContinue FailoverAction = iota |
| |
| FailoverExhausted |
| |
| FailoverCanceled |
| ) |
|
|
| const ( |
| |
| maxSameAccountRetries = 3 |
| |
| sameAccountRetryDelay = 500 * time.Millisecond |
| |
| |
| |
| singleAccountBackoffDelay = 2 * time.Second |
| ) |
|
|
| |
| type FailoverState struct { |
| SwitchCount int |
| MaxSwitches int |
| FailedAccountIDs map[int64]struct{} |
| SameAccountRetryCount map[int64]int |
| LastFailoverErr *service.UpstreamFailoverError |
| ForceCacheBilling bool |
| hasBoundSession bool |
| } |
|
|
| |
| func NewFailoverState(maxSwitches int, hasBoundSession bool) *FailoverState { |
| return &FailoverState{ |
| MaxSwitches: maxSwitches, |
| FailedAccountIDs: make(map[int64]struct{}), |
| SameAccountRetryCount: make(map[int64]int), |
| hasBoundSession: hasBoundSession, |
| } |
| } |
|
|
| |
| |
| func (s *FailoverState) HandleFailoverError( |
| ctx context.Context, |
| gatewayService TempUnscheduler, |
| accountID int64, |
| platform string, |
| failoverErr *service.UpstreamFailoverError, |
| ) FailoverAction { |
| s.LastFailoverErr = failoverErr |
|
|
| |
| if needForceCacheBilling(s.hasBoundSession, failoverErr) { |
| s.ForceCacheBilling = true |
| } |
|
|
| |
| if failoverErr.RetryableOnSameAccount && s.SameAccountRetryCount[accountID] < maxSameAccountRetries { |
| s.SameAccountRetryCount[accountID]++ |
| logger.FromContext(ctx).Warn("gateway.failover_same_account_retry", |
| zap.Int64("account_id", accountID), |
| zap.Int("upstream_status", failoverErr.StatusCode), |
| zap.Int("same_account_retry_count", s.SameAccountRetryCount[accountID]), |
| zap.Int("same_account_retry_max", maxSameAccountRetries), |
| ) |
| if !sleepWithContext(ctx, sameAccountRetryDelay) { |
| return FailoverCanceled |
| } |
| return FailoverContinue |
| } |
|
|
| |
| if failoverErr.RetryableOnSameAccount { |
| gatewayService.TempUnscheduleRetryableError(ctx, accountID, failoverErr) |
| } |
|
|
| |
| s.FailedAccountIDs[accountID] = struct{}{} |
|
|
| |
| if s.SwitchCount >= s.MaxSwitches { |
| return FailoverExhausted |
| } |
|
|
| |
| s.SwitchCount++ |
| logger.FromContext(ctx).Warn("gateway.failover_switch_account", |
| zap.Int64("account_id", accountID), |
| zap.Int("upstream_status", failoverErr.StatusCode), |
| zap.Int("switch_count", s.SwitchCount), |
| zap.Int("max_switches", s.MaxSwitches), |
| ) |
|
|
| |
| if platform == service.PlatformAntigravity { |
| delay := time.Duration(s.SwitchCount-1) * time.Second |
| if !sleepWithContext(ctx, delay) { |
| return FailoverCanceled |
| } |
| } |
|
|
| return FailoverContinue |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| func (s *FailoverState) HandleSelectionExhausted(ctx context.Context) FailoverAction { |
| if s.LastFailoverErr != nil && |
| s.LastFailoverErr.StatusCode == http.StatusServiceUnavailable && |
| s.SwitchCount <= s.MaxSwitches { |
|
|
| logger.FromContext(ctx).Warn("gateway.failover_single_account_backoff", |
| zap.Duration("backoff_delay", singleAccountBackoffDelay), |
| zap.Int("switch_count", s.SwitchCount), |
| zap.Int("max_switches", s.MaxSwitches), |
| ) |
| if !sleepWithContext(ctx, singleAccountBackoffDelay) { |
| return FailoverCanceled |
| } |
| logger.FromContext(ctx).Warn("gateway.failover_single_account_retry", |
| zap.Int("switch_count", s.SwitchCount), |
| zap.Int("max_switches", s.MaxSwitches), |
| ) |
| s.FailedAccountIDs = make(map[int64]struct{}) |
| return FailoverContinue |
| } |
| return FailoverExhausted |
| } |
|
|
| |
| |
| func needForceCacheBilling(hasBoundSession bool, failoverErr *service.UpstreamFailoverError) bool { |
| return hasBoundSession || (failoverErr != nil && failoverErr.ForceCacheBilling) |
| } |
|
|
| |
| func sleepWithContext(ctx context.Context, d time.Duration) bool { |
| if d <= 0 { |
| return true |
| } |
| select { |
| case <-ctx.Done(): |
| return false |
| case <-time.After(d): |
| return true |
| } |
| } |
|
|