Files
sub2api/backend/internal/service/token_refresh_pool_health_test.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

972 lines
33 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"reflect"
"sort"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/stretchr/testify/require"
)
type poolHealthAccountRepo struct {
AccountRepository
mu sync.Mutex
pages map[int64][]Account
requests []OAuthRefreshPageOptions
updatedCredentialIDs []int64
setErrorCalls int
setTempUnschedCalls int
getByIDErr error
}
func (r *poolHealthAccountRepo) GetByID(_ context.Context, _ int64) (*Account, error) {
if r.getByIDErr != nil {
return nil, r.getByIDErr
}
return nil, ErrAccountNotFound
}
func (r *poolHealthAccountRepo) ListOAuthRefreshCandidatePage(_ context.Context, options OAuthRefreshPageOptions) (*OAuthRefreshCandidatePage, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.requests = append(r.requests, options)
accounts := append([]Account(nil), r.pages[options.AfterID]...)
page := &OAuthRefreshCandidatePage{Accounts: accounts, HasMore: len(accounts) == options.Limit}
if len(accounts) > 0 {
page.NextAfterID = accounts[len(accounts)-1].ID
}
return page, nil
}
func (r *poolHealthAccountRepo) UpdateCredentials(_ context.Context, id int64, _ map[string]any) error {
r.mu.Lock()
defer r.mu.Unlock()
r.updatedCredentialIDs = append(r.updatedCredentialIDs, id)
return nil
}
func (r *poolHealthAccountRepo) UpdateGrokOAuthCredentialsIfUnchanged(
_ context.Context,
id int64,
_ map[string]any,
_ *int64,
_ map[string]any,
) (bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.updatedCredentialIDs = append(r.updatedCredentialIDs, id)
return true, nil
}
func (r *poolHealthAccountRepo) SetError(context.Context, int64, string) error {
r.mu.Lock()
defer r.mu.Unlock()
r.setErrorCalls++
return nil
}
func (r *poolHealthAccountRepo) SetGrokOAuthErrorIfCredentialsUnchanged(context.Context, int64, map[string]any, string) (bool, error) {
return false, nil
}
func (r *poolHealthAccountRepo) SetGrokOAuthRefreshErrorIfCredentialsUnchanged(context.Context, int64, map[string]any, *int64, string) (bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.setErrorCalls++
return true, nil
}
func (r *poolHealthAccountRepo) SetGrokOAuthRefreshTempUnschedulableIfCredentialsUnchanged(context.Context, int64, map[string]any, *int64, time.Time, string) (bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.setTempUnschedCalls++
return true, nil
}
func (r *poolHealthAccountRepo) SetTempUnschedulable(context.Context, int64, time.Time, string) error {
r.mu.Lock()
defer r.mu.Unlock()
r.setTempUnschedCalls++
return nil
}
func (r *poolHealthAccountRepo) snapshot() ([]OAuthRefreshPageOptions, []int64, int, int) {
r.mu.Lock()
defer r.mu.Unlock()
return append([]OAuthRefreshPageOptions(nil), r.requests...), append([]int64(nil), r.updatedCredentialIDs...), r.setErrorCalls, r.setTempUnschedCalls
}
type poolHealthRefresher struct {
err error
delay time.Duration
startDelays []time.Duration
ignoreContext bool
cancel context.CancelFunc
newCredentials map[string]any
calls atomic.Int64
active atomic.Int64
maxActive atomic.Int64
startMu sync.Mutex
startTimes []time.Time
}
type countingRefreshAttemptGate struct {
calls atomic.Int64
}
type rejectedRefreshAttemptGate struct {
err error
}
type poolHealthTokenCacheStub struct {
GeminiTokenCache
}
type tripBeforeRateAdmissionGate struct {
state *tokenRefreshProviderState
}
func (g *tripBeforeRateAdmissionGate) acquire(ctx context.Context) (func(), error) {
release, err := g.state.acquire(ctx)
if err != nil {
return nil, err
}
g.state.mu.Lock()
g.state.tripped = true
g.state.mu.Unlock()
return release, nil
}
func (g *tripBeforeRateAdmissionGate) acquireRate(ctx context.Context) (func(), error) {
return g.state.acquireRate(ctx)
}
type breakerTripAccountRepo struct {
*productionPathRateRepo
setErrorCalls atomic.Int64
setTempCalls atomic.Int64
}
func (r *breakerTripAccountRepo) SetGrokOAuthRefreshErrorIfCredentialsUnchanged(context.Context, int64, map[string]any, *int64, string) (bool, error) {
r.setErrorCalls.Add(1)
return true, nil
}
func (r *breakerTripAccountRepo) SetGrokOAuthRefreshTempUnschedulableIfCredentialsUnchanged(context.Context, int64, map[string]any, *int64, time.Time, string) (bool, error) {
r.setTempCalls.Add(1)
return true, nil
}
func (g *rejectedRefreshAttemptGate) acquire(context.Context) (func(), error) {
return nil, g.err
}
type productionPathRateRepo struct {
AccountRepository
mu sync.Mutex
accounts map[int64]*Account
}
func (r *productionPathRateRepo) GetByID(_ context.Context, id int64) (*Account, error) {
r.mu.Lock()
defer r.mu.Unlock()
account := r.accounts[id]
if account == nil {
return nil, ErrAccountNotFound
}
return snapshotOAuthRefreshAccount(account), nil
}
func (r *productionPathRateRepo) UpdateCredentials(_ context.Context, id int64, credentials map[string]any) error {
r.mu.Lock()
defer r.mu.Unlock()
account := r.accounts[id]
if account == nil {
return ErrAccountNotFound
}
account.Credentials = shallowCopyMap(credentials)
return nil
}
func (r *productionPathRateRepo) UpdateGrokOAuthCredentialsIfUnchanged(
_ context.Context,
id int64,
expectedCredentials map[string]any,
expectedProxyID *int64,
credentials map[string]any,
) (bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
account := r.accounts[id]
if account == nil || !reflect.DeepEqual(account.Credentials, expectedCredentials) ||
!reflect.DeepEqual(account.ProxyID, expectedProxyID) {
return false, nil
}
account.Credentials = shallowCopyMap(credentials)
return true, nil
}
type productionPathRefreshStart struct {
accountID int64
at time.Time
}
type productionPathRateExecutor struct {
firstStarted chan struct{}
releaseFirst chan struct{}
calls atomic.Int64
startMu sync.Mutex
starts []productionPathRefreshStart
}
func (e *productionPathRateExecutor) CacheKey(account *Account) string {
return fmt.Sprintf("production-path-rate:%d", account.ID)
}
func (e *productionPathRateExecutor) CanRefresh(account *Account) bool {
return account != nil && account.IsGrokOAuth()
}
func (e *productionPathRateExecutor) NeedsRefresh(account *Account, _ time.Duration) bool {
needsRefresh, _ := account.Credentials["needs_refresh"].(bool)
return needsRefresh
}
func (e *productionPathRateExecutor) Refresh(ctx context.Context, account *Account) (map[string]any, error) {
call := e.calls.Add(1)
e.startMu.Lock()
e.starts = append(e.starts, productionPathRefreshStart{accountID: account.ID, at: time.Now()})
e.startMu.Unlock()
if call == 1 {
close(e.firstStarted)
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-e.releaseFirst:
}
}
return map[string]any{
"access_token": fmt.Sprintf("fresh-access-%d", account.ID),
"refresh_token": fmt.Sprintf("fresh-refresh-%d", account.ID),
"needs_refresh": false,
}, nil
}
func (e *productionPathRateExecutor) startsSnapshot() []productionPathRefreshStart {
e.startMu.Lock()
defer e.startMu.Unlock()
return append([]productionPathRefreshStart(nil), e.starts...)
}
func (g *countingRefreshAttemptGate) acquire(ctx context.Context) (func(), error) {
if err := ctx.Err(); err != nil {
return nil, err
}
g.calls.Add(1)
return func() {}, nil
}
func (r *poolHealthRefresher) CacheKey(account *Account) string {
return fmt.Sprintf("pool-health:%d", account.ID)
}
func (r *poolHealthRefresher) CanRefresh(account *Account) bool {
return account != nil && account.Platform == PlatformGrok && account.Type == AccountTypeOAuth
}
func (r *poolHealthRefresher) NeedsRefresh(*Account, time.Duration) bool { return true }
func (r *poolHealthRefresher) Refresh(ctx context.Context, _ *Account) (map[string]any, error) {
r.calls.Add(1)
active := r.active.Add(1)
defer r.active.Add(-1)
r.startMu.Lock()
startIndex := len(r.startTimes)
r.startTimes = append(r.startTimes, time.Now())
delay := r.delay
if startIndex < len(r.startDelays) {
delay = r.startDelays[startIndex]
}
r.startMu.Unlock()
for {
maxActive := r.maxActive.Load()
if active <= maxActive || r.maxActive.CompareAndSwap(maxActive, active) {
break
}
}
if r.cancel != nil {
r.cancel()
}
if delay > 0 {
if r.ignoreContext {
time.Sleep(delay)
} else {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-timer.C:
}
}
}
if r.err != nil {
return nil, r.err
}
if r.newCredentials != nil {
credentials := make(map[string]any, len(r.newCredentials))
for key, value := range r.newCredentials {
credentials[key] = value
}
return credentials, nil
}
return map[string]any{"access_token": "new-token", "refresh_token": "new-refresh-token"}, nil
}
func (r *poolHealthRefresher) startsSnapshot() []time.Time {
r.startMu.Lock()
defer r.startMu.Unlock()
return append([]time.Time(nil), r.startTimes...)
}
func grokPoolAccount(id int64) Account {
return Account{
ID: id,
Platform: PlatformGrok,
Type: AccountTypeOAuth,
Status: StatusActive,
Credentials: map[string]any{
"access_token": "old-token",
"refresh_token": "refresh-token",
},
}
}
func newPoolHealthService(repo *poolHealthAccountRepo, refresher *poolHealthRefresher, cfg config.TokenRefreshConfig) *TokenRefreshService {
return &TokenRefreshService{
accountRepo: repo,
candidatePager: repo,
registrations: []tokenRefreshRegistration{{
platform: PlatformGrok,
refresher: refresher,
executor: refresher,
}},
refreshPolicy: DefaultBackgroundRefreshPolicy(),
cfg: &cfg,
}
}
func TestTokenRefreshService_RegistrationsAreCandidateEligibilitySource(t *testing.T) {
cfg := &config.Config{}
svc := NewTokenRefreshService(nil, nil, nil, nil, nil, nil, nil, cfg, nil)
require.Equal(t, []string{
PlatformAnthropic,
PlatformOpenAI,
PlatformGemini,
PlatformAntigravity,
PlatformGrok,
}, svc.eligiblePlatforms())
require.Len(t, svc.registrations, 5)
for _, registration := range svc.registrations {
require.NotNil(t, registration.refresher)
require.NotNil(t, registration.executor)
}
}
func TestTokenRefreshService_ProcessRefreshPagesByStableCursor(t *testing.T) {
repo := &poolHealthAccountRepo{pages: map[int64][]Account{
0: {grokPoolAccount(1), grokPoolAccount(2)},
2: {grokPoolAccount(3)},
}}
refresher := &poolHealthRefresher{}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{
RefreshBeforeExpiryHours: 1,
MaxRetries: 1,
CandidatePageSize: 2,
ProviderConcurrency: 4,
ProviderQPS: 10000,
AttemptTimeoutSeconds: 1,
CycleTimeoutSeconds: 2,
})
svc.processRefreshContext(context.Background())
requests, updatedIDs, _, _ := repo.snapshot()
require.Len(t, requests, 2)
require.Equal(t, int64(0), requests[0].AfterID)
require.Equal(t, int64(2), requests[1].AfterID)
require.Equal(t, []string{PlatformGrok}, requests[0].Platforms)
require.True(t, requests[0].ActiveOnly)
require.True(t, requests[0].RequireRefreshToken)
require.True(t, requests[0].ExcludeRetryCooldown)
sort.Slice(updatedIDs, func(i, j int) bool { return updatedIDs[i] < updatedIDs[j] })
require.Equal(t, []int64{1, 2, 3}, updatedIDs)
require.Zero(t, svc.candidateAfterID(), "a short final page must wrap the next cycle to the beginning")
}
func TestTokenRefreshService_BoundsPerProviderConcurrency(t *testing.T) {
accounts := make([]Account, 0, 8)
for id := int64(1); id <= 8; id++ {
accounts = append(accounts, grokPoolAccount(id))
}
repo := &poolHealthAccountRepo{pages: map[int64][]Account{0: accounts}}
refresher := &poolHealthRefresher{delay: 20 * time.Millisecond}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{
MaxRetries: 1,
CandidatePageSize: 20,
ProviderConcurrency: 2,
ProviderQPS: 10000,
AttemptTimeoutSeconds: 1,
CycleTimeoutSeconds: 2,
})
svc.processRefreshContext(context.Background())
require.Equal(t, int64(8), refresher.calls.Load())
require.Equal(t, int64(2), refresher.maxActive.Load())
}
func TestTokenRefreshRateGate_ReservesSpacedSlotsAndHonorsCancellation(t *testing.T) {
const interval = 25 * time.Millisecond
gate := newTokenRefreshRateGateWithInterval(interval)
base := time.Unix(1_700_000_000, 0)
require.Equal(t, base, gate.reserveSlot(base))
require.Equal(t, base.Add(interval), gate.reserveSlot(base))
require.Equal(t, base.Add(2*interval), gate.reserveSlot(base))
jumped := base.Add(time.Second)
require.Equal(t, jumped, gate.reserveSlot(jumped), "an idle gate should not retain stale delay")
cancelGate := newTokenRefreshRateGateWithInterval(time.Hour)
require.NoError(t, cancelGate.wait(context.Background()), "the first slot is immediately available")
ctx, cancel := context.WithCancel(context.Background())
cancel()
started := time.Now()
require.ErrorIs(t, cancelGate.wait(ctx), context.Canceled)
require.Less(t, time.Since(started), 100*time.Millisecond, "cancellation must not wait for the reserved slot")
}
func TestTokenRefreshService_RetriesAcquireRateSlotPerAttempt(t *testing.T) {
repo := &poolHealthAccountRepo{}
refresher := &poolHealthRefresher{err: errors.New("temporary provider failure")}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{MaxRetries: 3})
gate := &countingRefreshAttemptGate{}
account := grokPoolAccount(44)
err := svc.refreshWithRetryWithRateGate(context.Background(), &account, refresher, nil, time.Hour, gate)
require.Error(t, err)
require.Equal(t, int64(3), refresher.calls.Load())
require.Equal(t, int64(3), gate.calls.Load(), "every upstream retry must consume a provider rate slot")
}
func TestTokenRefreshService_ProcessProviderAccountsLegacyNilReleaseGateIsSafe(t *testing.T) {
repo := &poolHealthAccountRepo{}
refresher := &poolHealthRefresher{}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{
MaxRetries: 1,
ProviderConcurrency: 1,
})
state := &tokenRefreshProviderState{
service: svc,
registration: tokenRefreshRegistration{
platform: PlatformGrok,
refresher: refresher,
// nil executor deliberately exercises the legacy/direct fallback.
executor: nil,
},
// Admission rejection validly returns no release callback. The direct
// fallback must propagate the skip without dereferencing that nil handle.
rateGate: &rejectedRefreshAttemptGate{err: errRefreshSkipped},
poolGate: nil,
}
account := grokPoolAccount(45)
refreshed, skipped, failed := svc.processProviderAccounts(
context.Background(),
state,
[]*Account{&account},
time.Hour,
)
require.Zero(t, refreshed)
require.Equal(t, 1, skipped)
require.Zero(t, failed)
require.Zero(t, refresher.calls.Load(), "rejected rate admission must not reach the legacy upstream refresher")
}
func TestTokenRefreshService_ProviderRateGateIsSharedAcrossRuns(t *testing.T) {
svc := &TokenRefreshService{cfg: &config.TokenRefreshConfig{ProviderQPS: 40}}
first := svc.providerRateGate(PlatformGrok)
second := svc.providerRateGate(PlatformGrok)
require.Same(t, first, second, "background cycles and reconciliation must share the process-local provider limiter")
base := time.Unix(1_700_000_000, 0)
require.Equal(t, base, first.reserveSlot(base))
require.Equal(t, base.Add(25*time.Millisecond), second.reserveSlot(base))
}
func TestTokenRefreshService_ProviderConcurrencyGateIsSharedAcrossBackgroundAndConcurrentAdminReconciliation(t *testing.T) {
accounts := []Account{
grokPoolAccount(1),
grokPoolAccount(2),
grokPoolAccount(3),
grokPoolAccount(4),
}
repo := &poolHealthAccountRepo{pages: map[int64][]Account{0: accounts}}
refresher := &poolHealthRefresher{delay: 80 * time.Millisecond}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{
RefreshBeforeExpiryHours: 1,
MaxRetries: 1,
CandidatePageSize: 20,
ProviderConcurrency: 2,
ProviderQPS: 100,
ProviderFailureThreshold: 20,
AttemptTimeoutSeconds: 1,
CycleTimeoutSeconds: 3,
})
firstGate := svc.providerConcurrencyGate(PlatformGrok)
require.Same(t, firstGate, svc.providerConcurrencyGate(PlatformGrok))
start := make(chan struct{})
adminErrors := make(chan error, 2)
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
<-start
svc.processRefreshContext(context.Background())
}()
for i := 0; i < 2; i++ {
go func() {
defer wg.Done()
<-start
_, err := svc.ReconcileGrokOAuth(context.Background(), GrokOAuthReconcileInput{Apply: true, Limit: 20})
adminErrors <- err
}()
}
close(start)
wg.Wait()
close(adminErrors)
for err := range adminErrors {
require.NoError(t, err)
}
require.Equal(t, int64(12), refresher.calls.Load(), "background and both admin calls must all execute")
require.Equal(t, int64(2), refresher.maxActive.Load(),
"all entry points must share the configured per-provider upstream concurrency cap")
}
func TestTokenRefreshService_SaturatedProviderPreservesConcurrencyAndActualQPSStartSpacing(t *testing.T) {
const (
providerConcurrency = 2
providerQPS = 20
attemptCount = 8
)
repo := &poolHealthAccountRepo{}
refresher := &poolHealthRefresher{
// The first two QPS-spaced attempts finish together. If queued callers
// reserve QPS slots before acquiring provider capacity, two expired
// reservations can then burst upstream at the same time.
startDelays: []time.Duration{
220 * time.Millisecond,
170 * time.Millisecond,
20 * time.Millisecond,
20 * time.Millisecond,
20 * time.Millisecond,
20 * time.Millisecond,
20 * time.Millisecond,
20 * time.Millisecond,
},
}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{
MaxRetries: 1,
ProviderConcurrency: providerConcurrency,
ProviderQPS: providerQPS,
AttemptTimeoutSeconds: 1,
})
registration := svc.registrations[0]
sharedRateGate := svc.providerRateGate(PlatformGrok)
sharedPoolGate := svc.providerConcurrencyGate(PlatformGrok)
start := make(chan struct{})
errorsCh := make(chan error, attemptCount)
var wg sync.WaitGroup
for i := 0; i < attemptCount; i++ {
account := grokPoolAccount(int64(i + 1))
state := &tokenRefreshProviderState{
service: svc,
registration: registration,
rateGate: sharedRateGate,
poolGate: sharedPoolGate,
}
wg.Add(1)
go func() {
defer wg.Done()
<-start
errorsCh <- svc.refreshWithRetryWithRateGate(context.Background(), &account, refresher, nil, time.Hour, state)
}()
}
close(start)
wg.Wait()
close(errorsCh)
for err := range errorsCh {
require.NoError(t, err)
}
require.Equal(t, int64(providerConcurrency), refresher.maxActive.Load(),
"the scripted attempts must actually saturate the provider semaphore")
starts := refresher.startsSnapshot()
require.Len(t, starts, attemptCount)
configuredSpacing := time.Second / time.Duration(providerQPS)
// The floor is a tenth of the configured spacing, not spacing minus a few
// milliseconds. Each start timestamp is taken after the rate gate releases
// the goroutine, so scheduler delay can compress one observed gap without
// the gate having done anything wrong: this assertion failed on a loaded
// machine at 13ms and again at 37ms against a 40ms floor, both times with
// the gate pacing correctly.
//
// A tenth still separates the two cases by a wide margin. Measured with
// providerQPS=20 (50ms spacing), 8 attempts, providerConcurrency=2:
//
// gate at 50ms -> minimum observed gap 49.97ms
// gate at 0 -> minimum observed gap 22µs
//
// So an unpaced gate lands three orders of magnitude below the 5ms floor
// and is still caught, while jitter on a busy machine has room to move.
//
// Do not swap this for an assertion on the total span of the starts: the
// span is dominated by how long each attempt takes under
// providerConcurrency, not by the gate. Same measurement — 471ms paced
// against 241ms unpaced — so a span check passes with the gate disabled and
// tests nothing.
minimumObservedSpacing := configuredSpacing / 10
actualMinimumSpacing := starts[1].Sub(starts[0])
for i := 1; i < len(starts); i++ {
spacing := starts[i].Sub(starts[i-1])
if spacing < actualMinimumSpacing {
actualMinimumSpacing = spacing
}
require.GreaterOrEqualf(t, spacing, minimumObservedSpacing,
"upstream starts %d and %d violated configured QPS spacing", i-1, i)
}
t.Logf("max_active=%d configured_concurrency=%d minimum_start_spacing=%s configured_spacing=%s",
refresher.maxActive.Load(), providerConcurrency, actualMinimumSpacing, configuredSpacing)
}
func TestTokenRefreshService_ProductionPathRatesOnlyActualRefreshAfterSameAccountContention(t *testing.T) {
const interval = 200 * time.Millisecond
accountOne := grokPoolAccount(71)
accountOne.Credentials["needs_refresh"] = true
accountTwo := grokPoolAccount(72)
accountTwo.Credentials["needs_refresh"] = true
firstSelection := snapshotOAuthRefreshAccount(&accountOne)
contendingSelection := snapshotOAuthRefreshAccount(&accountOne)
differentSelection := snapshotOAuthRefreshAccount(&accountTwo)
repo := &productionPathRateRepo{accounts: map[int64]*Account{
accountOne.ID: snapshotOAuthRefreshAccount(&accountOne),
accountTwo.ID: snapshotOAuthRefreshAccount(&accountTwo),
}}
executor := &productionPathRateExecutor{
firstStarted: make(chan struct{}),
releaseFirst: make(chan struct{}),
}
svc := &TokenRefreshService{
accountRepo: repo,
refreshAPI: NewOAuthRefreshAPI(repo, nil),
refreshPolicy: DefaultBackgroundRefreshPolicy(),
cfg: &config.TokenRefreshConfig{MaxRetries: 1},
attemptTimeoutOverride: 2 * time.Second,
}
state := &tokenRefreshProviderState{
service: svc,
rateGate: newTokenRefreshRateGateWithInterval(interval),
poolGate: newTokenRefreshConcurrencyGate(2),
}
errorsCh := make(chan error, 3)
go func() {
errorsCh <- svc.refreshWithRetryWithRateGate(context.Background(), firstSelection, executor, executor, time.Hour, state)
}()
select {
case <-executor.firstStarted:
case <-time.After(time.Second):
require.FailNow(t, "first production-path refresh did not reach the upstream executor")
}
go func() {
errorsCh <- svc.refreshWithRetryWithRateGate(context.Background(), contendingSelection, executor, executor, time.Hour, state)
}()
require.Eventually(t, func() bool {
return len(state.poolGate.slots) == 2
}, time.Second, time.Millisecond, "same-account contender must hold the second provider slot while waiting on the local refresh lock")
go func() {
errorsCh <- svc.refreshWithRetryWithRateGate(context.Background(), differentSelection, executor, executor, time.Hour, state)
}()
close(executor.releaseFirst)
skipped := 0
for i := 0; i < 3; i++ {
err := <-errorsCh
if errors.Is(err, errRefreshSkipped) {
skipped++
continue
}
require.NoError(t, err)
}
require.Equal(t, 1, skipped, "the same-account contender must reread the refreshed row and skip without upstream admission")
starts := executor.startsSnapshot()
require.Len(t, starts, 2, "only the two accounts that actually refresh may consume QPS admission")
require.Equal(t, int64(71), starts[0].accountID)
require.Equal(t, int64(72), starts[1].accountID)
spacing := starts[1].at.Sub(starts[0].at)
require.GreaterOrEqual(t, spacing, interval-30*time.Millisecond)
require.Less(t, spacing, 350*time.Millisecond,
"a same-account lock waiter must not consume a rate slot and push the different-account refresh to the second interval")
t.Logf("actual_refresh_calls=%d actual_start_spacing=%s configured_spacing=%s", executor.calls.Load(), spacing, interval)
}
func TestTokenRefreshService_ProviderTripBeforeRateAdmissionSkipsWithoutAccountMutation(t *testing.T) {
account := grokPoolAccount(73)
stored := snapshotOAuthRefreshAccount(&account)
repo := &breakerTripAccountRepo{productionPathRateRepo: &productionPathRateRepo{
accounts: map[int64]*Account{account.ID: stored},
}}
refresher := &poolHealthRefresher{}
svc := &TokenRefreshService{
accountRepo: repo,
refreshAPI: NewOAuthRefreshAPI(repo, nil),
refreshPolicy: DefaultBackgroundRefreshPolicy(),
cfg: &config.TokenRefreshConfig{MaxRetries: 1},
}
state := &tokenRefreshProviderState{
service: svc,
rateGate: newTokenRefreshRateGate(1),
poolGate: newTokenRefreshConcurrencyGate(1),
}
gate := &tripBeforeRateAdmissionGate{state: state}
err := svc.refreshWithRetryWithRateGate(context.Background(), &account, refresher, refresher, time.Hour, gate)
require.ErrorIs(t, err, errRefreshSkipped)
require.Zero(t, refresher.calls.Load(), "a tripped provider must not reach upstream rate admission")
require.Zero(t, repo.setErrorCalls.Load())
require.Zero(t, repo.setTempCalls.Load(), "provider skip must never fall through to per-account cooldown")
}
func TestTokenRefreshService_ConfigBounds(t *testing.T) {
maxInt := int(^uint(0) >> 1)
svc := &TokenRefreshService{cfg: &config.TokenRefreshConfig{
MaxRetries: maxInt,
RetryBackoffSeconds: maxInt,
ProviderFailureThreshold: maxInt,
AttemptTimeoutSeconds: maxInt,
CycleTimeoutSeconds: maxInt,
}}
require.Equal(t, maxTokenRefreshMaxRetries, svc.maxRetries())
require.Equal(t, maxTokenRefreshProviderFailureThreshold, svc.providerFailureThreshold())
require.Equal(t, maxTokenRefreshAttemptTimeout, svc.attemptTimeout())
require.Equal(t, maxTokenRefreshCycleTimeout, svc.cycleTimeout())
require.LessOrEqual(t, svc.retryBackoff(1, maxTokenRefreshMaxRetries), maxTokenRefreshRetryBackoff)
require.Equal(t, maxGrokOAuthReconcilePageSize, svc.grokOAuthReconcileMaxPageSize())
}
func TestTokenRefreshService_AttemptTimeoutStaysInsideDistributedLockLease(t *testing.T) {
cache := &poolHealthTokenCacheStub{}
svc := &TokenRefreshService{
cfg: &config.TokenRefreshConfig{AttemptTimeoutSeconds: int(maxTokenRefreshAttemptTimeout / time.Second)},
refreshAPI: NewOAuthRefreshAPI(&poolHealthAccountRepo{}, cache),
}
require.Equal(t, 55*time.Second, svc.attemptTimeout())
require.Less(t, svc.attemptTimeout(), defaultRefreshLockTTL)
}
func TestTokenRefreshService_SharedProviderFailureContainsCycleWithoutAccountMutation(t *testing.T) {
accounts := make([]Account, 0, 5)
for id := int64(1); id <= 5; id++ {
accounts = append(accounts, grokPoolAccount(id))
}
repo := &poolHealthAccountRepo{pages: map[int64][]Account{0: accounts}}
refresher := &poolHealthRefresher{err: errors.New("invalid_client: provider configuration rejected")}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{
MaxRetries: 1,
CandidatePageSize: 10,
ProviderConcurrency: 4,
ProviderQPS: 10000,
ProviderFailureThreshold: 3,
AttemptTimeoutSeconds: 1,
CycleTimeoutSeconds: 2,
})
svc.processRefreshContext(context.Background())
_, _, setErrorCalls, setTempUnschedCalls := repo.snapshot()
require.Equal(t, int64(1), refresher.calls.Load(), "shared provider configuration failures must open the in-cycle breaker immediately")
require.Zero(t, setErrorCalls, "shared provider failures must not mass-disable accounts")
require.Zero(t, setTempUnschedCalls, "shared provider failures must not mutate per-account scheduling state")
}
func TestTokenRefreshService_SharedDBRereadFailureContainsCycleWithoutAccountMutation(t *testing.T) {
accounts := []Account{grokPoolAccount(1), grokPoolAccount(2), grokPoolAccount(3)}
repo := &poolHealthAccountRepo{
pages: map[int64][]Account{0: accounts},
getByIDErr: errors.New("database unavailable"),
}
refresher := &poolHealthRefresher{}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{
MaxRetries: 3,
CandidatePageSize: 10,
ProviderConcurrency: 4,
ProviderQPS: 10000,
AttemptTimeoutSeconds: 1,
CycleTimeoutSeconds: 2,
})
svc.refreshAPI = NewOAuthRefreshAPI(repo, nil)
svc.processRefreshContext(context.Background())
_, _, setErrorCalls, setTempUnschedCalls := repo.snapshot()
require.Zero(t, refresher.calls.Load(), "refresh must fail closed before using stale account credentials")
require.Zero(t, setErrorCalls)
require.Zero(t, setTempUnschedCalls, "a shared DB outage must not mutate the selected account")
}
func TestTokenRefreshService_GenericGrokForbiddenContainsCycleWithoutAccountMutation(t *testing.T) {
accounts := []Account{grokPoolAccount(1), grokPoolAccount(2), grokPoolAccount(3)}
repo := &poolHealthAccountRepo{pages: map[int64][]Account{0: accounts}}
refresher := &poolHealthRefresher{err: errors.New(`GROK_OAUTH_ENTITLEMENT_DENIED: token refresh failed: status 403, body: <html>request blocked</html>`)}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{
MaxRetries: 1,
CandidatePageSize: 10,
ProviderConcurrency: 4,
ProviderQPS: 10000,
ProviderFailureThreshold: 3,
AttemptTimeoutSeconds: 1,
CycleTimeoutSeconds: 2,
})
svc.processRefreshContext(context.Background())
_, _, setErrorCalls, setTempUnschedCalls := repo.snapshot()
require.Equal(t, int64(1), refresher.calls.Load(), "an ambiguous Grok 403 must contain the provider immediately")
require.Zero(t, setErrorCalls, "a generic 403 is not evidence that an account credential is permanently invalid")
require.Zero(t, setTempUnschedCalls, "provider containment must not mutate account scheduling state")
}
func TestTokenRefreshService_ExplicitGrokEntitlementDenialIsPermanent(t *testing.T) {
repo := &poolHealthAccountRepo{pages: map[int64][]Account{0: {grokPoolAccount(1)}}}
refresher := &poolHealthRefresher{err: errors.New(`GROK_OAUTH_ENTITLEMENT_DENIED: token refresh failed: status 403, body: {"error":"subscription required"}`)}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{
MaxRetries: 1,
CandidatePageSize: 10,
ProviderConcurrency: 1,
ProviderQPS: 10000,
AttemptTimeoutSeconds: 1,
CycleTimeoutSeconds: 2,
})
svc.processRefreshContext(context.Background())
_, _, setErrorCalls, setTempUnschedCalls := repo.snapshot()
require.Equal(t, int64(1), refresher.calls.Load())
require.Equal(t, 1, setErrorCalls, "explicit entitlement evidence is an account-permanent failure")
require.Zero(t, setTempUnschedCalls)
}
func TestTokenRefreshService_AttemptTimeoutTripsRetryableProviderThreshold(t *testing.T) {
accounts := []Account{grokPoolAccount(1), grokPoolAccount(2), grokPoolAccount(3)}
repo := &poolHealthAccountRepo{pages: map[int64][]Account{0: accounts}}
refresher := &poolHealthRefresher{delay: time.Second}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{
MaxRetries: 1,
CandidatePageSize: 10,
ProviderConcurrency: 1,
ProviderQPS: 10000,
ProviderFailureThreshold: 2,
CycleTimeoutSeconds: 2,
})
svc.attemptTimeoutOverride = 20 * time.Millisecond
svc.processRefreshContext(context.Background())
_, _, setErrorCalls, setTempUnschedCalls := repo.snapshot()
require.Equal(t, int64(2), refresher.calls.Load(), "two attempt timeouts should trip the retryable provider threshold")
require.Zero(t, setErrorCalls)
require.Equal(t, 2, setTempUnschedCalls, "attempt timeouts remain account-transient failures before containment opens")
}
func TestTokenRefreshService_ParentCancellationStopsRetryWithoutAccountMutation(t *testing.T) {
repo := &poolHealthAccountRepo{}
ctx, cancel := context.WithCancel(context.Background())
refresher := &poolHealthRefresher{
err: errors.New("temporary provider failure"),
cancel: cancel,
}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{
MaxRetries: 3,
RetryBackoffSeconds: 1,
AttemptTimeoutSeconds: 1,
})
account := grokPoolAccount(42)
err := svc.refreshWithRetry(ctx, &account, refresher, nil, time.Hour)
require.ErrorIs(t, err, context.Canceled)
_, _, setErrorCalls, setTempUnschedCalls := repo.snapshot()
require.Zero(t, setErrorCalls)
require.Zero(t, setTempUnschedCalls)
}
func TestTokenRefreshService_LateSuccessPastAttemptDeadlineIsRejected(t *testing.T) {
repo := &poolHealthAccountRepo{}
refresher := &poolHealthRefresher{
delay: 30 * time.Millisecond,
ignoreContext: true,
}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{MaxRetries: 1})
svc.attemptTimeoutOverride = 10 * time.Millisecond
account := grokPoolAccount(43)
err := svc.refreshWithRetry(context.Background(), &account, refresher, nil, time.Hour)
var timeoutErr *refreshAttemptTimeoutError
require.ErrorAs(t, err, &timeoutErr)
_, updatedIDs, setErrorCalls, setTempUnschedCalls := repo.snapshot()
require.Empty(t, updatedIDs, "credentials returned after the deadline must not be persisted")
require.Zero(t, setErrorCalls)
require.Equal(t, 1, setTempUnschedCalls)
}
func TestTokenRefreshService_NonRetryableGrokFailureInvalidatesTokenCache(t *testing.T) {
repo := &poolHealthAccountRepo{}
invalidator := &reconcileInvalidator{}
refresher := &poolHealthRefresher{err: errors.New("invalid_grant: revoked")}
svc := newPoolHealthService(repo, refresher, config.TokenRefreshConfig{MaxRetries: 1})
svc.cacheInvalidator = invalidator
account := grokPoolAccount(77)
err := svc.refreshWithRetry(context.Background(), &account, refresher, nil, time.Hour)
require.Error(t, err)
_, _, setErrorCalls, setTempUnschedCalls := repo.snapshot()
require.Equal(t, 1, setErrorCalls)
require.Zero(t, setTempUnschedCalls)
require.Equal(t, 1, invalidator.count())
}