Files
sub2api/backend/internal/service/concurrency_service_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

619 lines
20 KiB
Go

//go:build unit
package service
import (
"context"
"errors"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// stubConcurrencyCacheForTest 用于并发服务单元测试的缓存桩
type stubConcurrencyCacheForTest struct {
acquireResult bool
acquireErr error
releaseErr error
concurrency int
concurrencyErr error
waitAllowed bool
waitErr error
waitCount int
waitCountErr error
loadBatch map[int64]*AccountLoadInfo
loadBatchErr error
usersLoadBatch map[int64]*UserLoadInfo
usersLoadErr error
cleanupErr error
apiKeyTrackErr error
apiKeyReleaseErr error
apiKeyConcurrency map[int64]int
apiKeyConcurrencyErr error
// 记录调用
releasedAccountIDs []int64
releasedRequestIDs []string
loadBatchCalls atomic.Int64
trackedAPIKeyIDs []int64
trackedAPIKeyRequestIDs []string
releasedAPIKeyIDs []int64
releasedAPIKeyRequestIDs []string
}
type ingressLeaseCacheForTest struct {
stubConcurrencyCacheForTest
acquireIngressResult bool
acquireIngressErr error
acquireIngressFn func(context.Context, int64, int, string) (bool, error)
refreshIngressResult bool
refreshIngressErr error
refreshIngressFn func(context.Context, int64, string) (bool, error)
releaseIngressErr error
releaseIngressFn func(context.Context, int64, string) error
acquireIngressCalls int
refreshIngressCalls int
releaseIngressCalls int
}
func (c *ingressLeaseCacheForTest) AcquireOpenAIWSIngressLease(ctx context.Context, apiKeyID int64, maxConnections int, leaseID string) (bool, error) {
c.acquireIngressCalls++
if c.acquireIngressFn != nil {
return c.acquireIngressFn(ctx, apiKeyID, maxConnections, leaseID)
}
return c.acquireIngressResult, c.acquireIngressErr
}
func (c *ingressLeaseCacheForTest) RefreshOpenAIWSIngressLease(ctx context.Context, apiKeyID int64, leaseID string) (bool, error) {
c.refreshIngressCalls++
if c.refreshIngressFn != nil {
return c.refreshIngressFn(ctx, apiKeyID, leaseID)
}
return c.refreshIngressResult, c.refreshIngressErr
}
func (c *ingressLeaseCacheForTest) ReleaseOpenAIWSIngressLease(ctx context.Context, apiKeyID int64, leaseID string) error {
c.releaseIngressCalls++
if c.releaseIngressFn != nil {
return c.releaseIngressFn(ctx, apiKeyID, leaseID)
}
return c.releaseIngressErr
}
var _ ConcurrencyCache = (*stubConcurrencyCacheForTest)(nil)
var _ OpenAIWSIngressLeaseCache = (*ingressLeaseCacheForTest)(nil)
func (c *stubConcurrencyCacheForTest) AcquireAccountSlot(_ context.Context, _ int64, _ int, _ string) (bool, error) {
return c.acquireResult, c.acquireErr
}
func (c *stubConcurrencyCacheForTest) ReleaseAccountSlot(_ context.Context, accountID int64, requestID string) error {
c.releasedAccountIDs = append(c.releasedAccountIDs, accountID)
c.releasedRequestIDs = append(c.releasedRequestIDs, requestID)
return c.releaseErr
}
func (c *stubConcurrencyCacheForTest) GetAccountConcurrency(_ context.Context, _ int64) (int, error) {
return c.concurrency, c.concurrencyErr
}
func (c *stubConcurrencyCacheForTest) GetAccountConcurrencyBatch(_ context.Context, accountIDs []int64) (map[int64]int, error) {
result := make(map[int64]int, len(accountIDs))
for _, accountID := range accountIDs {
if c.concurrencyErr != nil {
return nil, c.concurrencyErr
}
result[accountID] = c.concurrency
}
return result, nil
}
func (c *stubConcurrencyCacheForTest) IncrementAccountWaitCount(_ context.Context, _ int64, _ int) (bool, error) {
return c.waitAllowed, c.waitErr
}
func (c *stubConcurrencyCacheForTest) DecrementAccountWaitCount(_ context.Context, _ int64) error {
return nil
}
func (c *stubConcurrencyCacheForTest) GetAccountWaitingCount(_ context.Context, _ int64) (int, error) {
return c.waitCount, c.waitCountErr
}
func (c *stubConcurrencyCacheForTest) AcquireUserSlot(_ context.Context, _ int64, _ int, _ string) (bool, error) {
return c.acquireResult, c.acquireErr
}
func (c *stubConcurrencyCacheForTest) ReleaseUserSlot(_ context.Context, _ int64, _ string) error {
return c.releaseErr
}
func (c *stubConcurrencyCacheForTest) GetUserConcurrency(_ context.Context, _ int64) (int, error) {
return c.concurrency, c.concurrencyErr
}
func (c *stubConcurrencyCacheForTest) TrackAPIKeySlot(_ context.Context, apiKeyID int64, requestID string) error {
c.trackedAPIKeyIDs = append(c.trackedAPIKeyIDs, apiKeyID)
c.trackedAPIKeyRequestIDs = append(c.trackedAPIKeyRequestIDs, requestID)
return c.apiKeyTrackErr
}
func (c *stubConcurrencyCacheForTest) ReleaseAPIKeySlot(_ context.Context, apiKeyID int64, requestID string) error {
c.releasedAPIKeyIDs = append(c.releasedAPIKeyIDs, apiKeyID)
c.releasedAPIKeyRequestIDs = append(c.releasedAPIKeyRequestIDs, requestID)
return c.apiKeyReleaseErr
}
func (c *stubConcurrencyCacheForTest) GetAPIKeyConcurrencyBatch(_ context.Context, apiKeyIDs []int64) (map[int64]int, error) {
if c.apiKeyConcurrencyErr != nil {
return nil, c.apiKeyConcurrencyErr
}
result := make(map[int64]int, len(apiKeyIDs))
for _, apiKeyID := range apiKeyIDs {
result[apiKeyID] = c.apiKeyConcurrency[apiKeyID]
}
return result, nil
}
func (c *stubConcurrencyCacheForTest) IncrementWaitCount(_ context.Context, _ int64, _ int) (bool, error) {
return c.waitAllowed, c.waitErr
}
func (c *stubConcurrencyCacheForTest) DecrementWaitCount(_ context.Context, _ int64) error {
return nil
}
func (c *stubConcurrencyCacheForTest) GetAccountsLoadBatch(_ context.Context, _ []AccountWithConcurrency) (map[int64]*AccountLoadInfo, error) {
c.loadBatchCalls.Add(1)
return c.loadBatch, c.loadBatchErr
}
func (c *stubConcurrencyCacheForTest) GetUsersLoadBatch(_ context.Context, _ []UserWithConcurrency) (map[int64]*UserLoadInfo, error) {
return c.usersLoadBatch, c.usersLoadErr
}
func (c *stubConcurrencyCacheForTest) CleanupExpiredAccountSlots(_ context.Context, _ int64) error {
return c.cleanupErr
}
func (c *stubConcurrencyCacheForTest) CleanupExpiredAccountSlotKeys(_ context.Context) error {
return c.cleanupErr
}
func (c *stubConcurrencyCacheForTest) CleanupStaleProcessSlots(_ context.Context, _ string) error {
return c.cleanupErr
}
type trackingConcurrencyCache struct {
stubConcurrencyCacheForTest
cleanupPrefix string
}
func (c *trackingConcurrencyCache) CleanupStaleProcessSlots(_ context.Context, prefix string) error {
c.cleanupPrefix = prefix
return c.cleanupErr
}
func TestCleanupStaleProcessSlots_NilCache(t *testing.T) {
svc := &ConcurrencyService{cache: nil}
require.NoError(t, svc.CleanupStaleProcessSlots(context.Background()))
}
func TestCleanupStaleProcessSlots_DelegatesPrefix(t *testing.T) {
cache := &trackingConcurrencyCache{}
svc := NewConcurrencyService(cache)
require.NoError(t, svc.CleanupStaleProcessSlots(context.Background()))
require.Equal(t, RequestIDPrefix(), cache.cleanupPrefix)
}
func TestAcquireAccountSlot_Success(t *testing.T) {
cache := &stubConcurrencyCacheForTest{acquireResult: true}
svc := NewConcurrencyService(cache)
result, err := svc.AcquireAccountSlot(context.Background(), 1, 5)
require.NoError(t, err)
require.True(t, result.Acquired)
require.NotNil(t, result.ReleaseFunc)
}
func TestAcquireAccountSlot_Failure(t *testing.T) {
cache := &stubConcurrencyCacheForTest{acquireResult: false}
svc := NewConcurrencyService(cache)
result, err := svc.AcquireAccountSlot(context.Background(), 1, 5)
require.NoError(t, err)
require.False(t, result.Acquired)
require.Nil(t, result.ReleaseFunc)
}
func TestAcquireAccountSlot_UnlimitedConcurrency(t *testing.T) {
svc := NewConcurrencyService(&stubConcurrencyCacheForTest{})
for _, maxConcurrency := range []int{0, -1} {
result, err := svc.AcquireAccountSlot(context.Background(), 1, maxConcurrency)
require.NoError(t, err)
require.True(t, result.Acquired, "maxConcurrency=%d 应无限制通过", maxConcurrency)
require.NotNil(t, result.ReleaseFunc, "ReleaseFunc 应为 no-op 函数")
}
}
func TestAcquireAccountSlot_CacheError(t *testing.T) {
cache := &stubConcurrencyCacheForTest{acquireErr: errors.New("redis down")}
svc := NewConcurrencyService(cache)
result, err := svc.AcquireAccountSlot(context.Background(), 1, 5)
require.Error(t, err)
require.Nil(t, result)
}
func TestAcquireAccountSlot_ReleaseDecrements(t *testing.T) {
cache := &stubConcurrencyCacheForTest{acquireResult: true}
svc := NewConcurrencyService(cache)
result, err := svc.AcquireAccountSlot(context.Background(), 42, 5)
require.NoError(t, err)
require.True(t, result.Acquired)
// 调用 ReleaseFunc 应释放槽位
result.ReleaseFunc()
require.Len(t, cache.releasedAccountIDs, 1)
require.Equal(t, int64(42), cache.releasedAccountIDs[0])
require.Len(t, cache.releasedRequestIDs, 1)
require.NotEmpty(t, cache.releasedRequestIDs[0], "requestID 不应为空")
}
func TestAcquireUserSlot_IndependentFromAccount(t *testing.T) {
cache := &stubConcurrencyCacheForTest{acquireResult: true}
svc := NewConcurrencyService(cache)
// 用户槽位获取应独立于账户槽位
result, err := svc.AcquireUserSlot(context.Background(), 100, 3)
require.NoError(t, err)
require.True(t, result.Acquired)
require.NotNil(t, result.ReleaseFunc)
}
func TestAcquireUserSlot_UnlimitedConcurrency(t *testing.T) {
svc := NewConcurrencyService(&stubConcurrencyCacheForTest{})
result, err := svc.AcquireUserSlot(context.Background(), 1, 0)
require.NoError(t, err)
require.True(t, result.Acquired)
}
func TestTrackAPIKeySlot_ReleaseDecrements(t *testing.T) {
cache := &stubConcurrencyCacheForTest{}
svc := NewConcurrencyService(cache)
release := svc.TrackAPIKeySlot(context.Background(), 88)
require.NotNil(t, release)
require.Equal(t, []int64{88}, cache.trackedAPIKeyIDs)
require.Len(t, cache.trackedAPIKeyRequestIDs, 1)
require.NotEmpty(t, cache.trackedAPIKeyRequestIDs[0])
release()
require.Equal(t, []int64{88}, cache.releasedAPIKeyIDs)
require.Equal(t, cache.trackedAPIKeyRequestIDs, cache.releasedAPIKeyRequestIDs)
}
func TestTrackAPIKeySlot_FailOpen(t *testing.T) {
cache := &stubConcurrencyCacheForTest{apiKeyTrackErr: errors.New("redis down")}
svc := NewConcurrencyService(cache)
release := svc.TrackAPIKeySlot(context.Background(), 88)
require.NotNil(t, release)
require.Equal(t, []int64{88}, cache.trackedAPIKeyIDs)
require.NotPanics(t, release)
require.Empty(t, cache.releasedAPIKeyIDs)
}
func TestGetAPIKeyConcurrencyBatch_Fallbacks(t *testing.T) {
t.Run("nil cache returns zeroes", func(t *testing.T) {
svc := &ConcurrencyService{cache: nil}
counts, err := svc.GetAPIKeyConcurrencyBatch(context.Background(), []int64{1, 2})
require.NoError(t, err)
require.Equal(t, map[int64]int{1: 0, 2: 0}, counts)
})
t.Run("redis error returns zeroes", func(t *testing.T) {
cache := &stubConcurrencyCacheForTest{apiKeyConcurrencyErr: errors.New("redis down")}
svc := NewConcurrencyService(cache)
counts, err := svc.GetAPIKeyConcurrencyBatch(context.Background(), []int64{1, 2})
require.NoError(t, err)
require.Equal(t, map[int64]int{1: 0, 2: 0}, counts)
})
t.Run("success returns counts", func(t *testing.T) {
cache := &stubConcurrencyCacheForTest{apiKeyConcurrency: map[int64]int{1: 3, 2: 0}}
svc := NewConcurrencyService(cache)
counts, err := svc.GetAPIKeyConcurrencyBatch(context.Background(), []int64{1, 2})
require.NoError(t, err)
require.Equal(t, map[int64]int{1: 3, 2: 0}, counts)
})
}
func TestAcquireOpenAIWSIngressLease(t *testing.T) {
t.Run("zero value release is safe", func(t *testing.T) {
var lease OpenAIWSIngressLease
require.NotPanics(t, lease.Release)
})
t.Run("disabled", func(t *testing.T) {
cache := &ingressLeaseCacheForTest{}
lease, acquired, err := NewConcurrencyService(cache).AcquireOpenAIWSIngressLease(nil, 1, 0)
require.NoError(t, err)
require.True(t, acquired)
require.Nil(t, lease)
require.Zero(t, cache.acquireIngressCalls)
})
t.Run("unsupported cache fails closed", func(t *testing.T) {
lease, acquired, err := NewConcurrencyService(&stubConcurrencyCacheForTest{}).AcquireOpenAIWSIngressLease(context.Background(), 1, 1)
require.Error(t, err)
require.False(t, acquired)
require.Nil(t, lease)
})
t.Run("capacity rejected", func(t *testing.T) {
cache := &ingressLeaseCacheForTest{acquireIngressResult: false}
lease, acquired, err := NewConcurrencyService(cache).AcquireOpenAIWSIngressLease(context.Background(), 1, 1)
require.NoError(t, err)
require.False(t, acquired)
require.Nil(t, lease)
})
t.Run("release returns capacity", func(t *testing.T) {
cache := &ingressLeaseCacheForTest{acquireIngressResult: true, refreshIngressResult: true}
lease, acquired, err := NewConcurrencyService(cache).AcquireOpenAIWSIngressLease(nil, 1, 1)
require.NoError(t, err)
require.True(t, acquired)
require.NotNil(t, lease)
lease.Release()
lease.Release()
require.Equal(t, 1, cache.releaseIngressCalls)
})
}
func TestOpenAIWSIngressLeaseRefreshLoss(t *testing.T) {
t.Run("missing lease is lost immediately", func(t *testing.T) {
cache := &ingressLeaseCacheForTest{refreshIngressResult: false}
lease := &OpenAIWSIngressLease{cache: cache, apiKeyID: 1, leaseID: "missing"}
_, lost := lease.refresh(time.Now())
require.True(t, lost)
require.Equal(t, 1, cache.refreshIngressCalls)
})
t.Run("persistent redis errors lose lease after ttl", func(t *testing.T) {
cache := &ingressLeaseCacheForTest{refreshIngressErr: errors.New("redis unavailable")}
lease := &OpenAIWSIngressLease{cache: cache, apiKeyID: 1, leaseID: "unconfirmed"}
_, lost := lease.refresh(time.Now().Add(-openAIWSIngressLeaseTTL))
require.True(t, lost)
require.Equal(t, 1, cache.refreshIngressCalls)
})
}
func TestOpenAIWSIngressLeaseReleaseWaitsForInFlightRefresh(t *testing.T) {
refreshStarted := make(chan struct{})
allowRefresh := make(chan struct{})
cache := &ingressLeaseCacheForTest{
refreshIngressFn: func(context.Context, int64, string) (bool, error) {
close(refreshStarted)
<-allowRefresh
return true, nil
},
}
ctx, cancel := context.WithCancelCause(context.Background())
lease := &OpenAIWSIngressLease{
ctx: ctx,
cancel: cancel,
cache: cache,
apiKeyID: 1,
leaseID: "in-flight-refresh",
stopCh: make(chan struct{}),
refreshDone: make(chan struct{}),
}
go func() {
defer close(lease.refreshDone)
_, _ = lease.refresh(time.Now())
}()
<-refreshStarted
released := make(chan struct{})
go func() {
lease.Release()
close(released)
}()
select {
case <-released:
t.Fatal("release returned before the in-flight refresh completed")
case <-time.After(20 * time.Millisecond):
}
require.Zero(t, cache.releaseIngressCalls)
close(allowRefresh)
select {
case <-released:
case <-time.After(time.Second):
t.Fatal("release did not complete after the refresh returned")
}
require.Equal(t, 1, cache.releaseIngressCalls)
}
func TestGenerateRequestID_UsesStablePrefixAndMonotonicCounter(t *testing.T) {
id1 := generateRequestID()
id2 := generateRequestID()
require.NotEmpty(t, id1)
require.NotEmpty(t, id2)
p1 := strings.Split(id1, "-")
p2 := strings.Split(id2, "-")
require.Len(t, p1, 2)
require.Len(t, p2, 2)
require.Equal(t, p1[0], p2[0], "同一进程前缀应保持一致")
n1, err := strconv.ParseUint(p1[1], 36, 64)
require.NoError(t, err)
n2, err := strconv.ParseUint(p2[1], 36, 64)
require.NoError(t, err)
require.Equal(t, n1+1, n2, "计数器应单调递增")
}
func TestGetAccountsLoadBatch_ReturnsCorrectData(t *testing.T) {
expected := map[int64]*AccountLoadInfo{
1: {AccountID: 1, CurrentConcurrency: 3, WaitingCount: 0, LoadRate: 60},
2: {AccountID: 2, CurrentConcurrency: 5, WaitingCount: 2, LoadRate: 100},
}
cache := &stubConcurrencyCacheForTest{loadBatch: expected}
svc := NewConcurrencyService(cache)
accounts := []AccountWithConcurrency{
{ID: 1, MaxConcurrency: 5},
{ID: 2, MaxConcurrency: 5},
}
result, err := svc.GetAccountsLoadBatch(context.Background(), accounts)
require.NoError(t, err)
require.Equal(t, expected, result)
}
func TestGetAccountsLoadBatch_NilCache(t *testing.T) {
svc := &ConcurrencyService{cache: nil}
result, err := svc.GetAccountsLoadBatch(context.Background(), nil)
require.NoError(t, err)
require.Empty(t, result)
}
func TestGetAccountsLoadBatch_UsesShortTTLCache(t *testing.T) {
cache := &stubConcurrencyCacheForTest{
loadBatch: map[int64]*AccountLoadInfo{
1: {AccountID: 1, CurrentConcurrency: 1, LoadRate: 20},
},
}
svc := NewConcurrencyService(cache)
svc.SetAccountLoadBatchCacheTTL(time.Second)
accounts := []AccountWithConcurrency{{ID: 1, MaxConcurrency: 5}}
first, err := svc.GetAccountsLoadBatch(context.Background(), accounts)
require.NoError(t, err)
require.Equal(t, 1, first[int64(1)].CurrentConcurrency)
cache.loadBatch[1] = &AccountLoadInfo{AccountID: 1, CurrentConcurrency: 4, LoadRate: 80}
second, err := svc.GetAccountsLoadBatch(context.Background(), accounts)
require.NoError(t, err)
require.Equal(t, 1, second[int64(1)].CurrentConcurrency)
require.Equal(t, int64(1), cache.loadBatchCalls.Load())
}
func TestGetAccountsLoadBatchFresh_BypassesShortTTLCache(t *testing.T) {
cache := &stubConcurrencyCacheForTest{
loadBatch: map[int64]*AccountLoadInfo{
1: {AccountID: 1, CurrentConcurrency: 1, LoadRate: 20},
},
}
svc := NewConcurrencyService(cache)
svc.SetAccountLoadBatchCacheTTL(time.Second)
accounts := []AccountWithConcurrency{{ID: 1, MaxConcurrency: 5}}
_, err := svc.GetAccountsLoadBatch(context.Background(), accounts)
require.NoError(t, err)
cache.loadBatch[1] = &AccountLoadInfo{AccountID: 1, CurrentConcurrency: 4, LoadRate: 80}
fresh, err := svc.GetAccountsLoadBatchFresh(context.Background(), accounts)
require.NoError(t, err)
require.Equal(t, 4, fresh[int64(1)].CurrentConcurrency)
require.Equal(t, int64(2), cache.loadBatchCalls.Load())
}
func TestIncrementWaitCount_Success(t *testing.T) {
cache := &stubConcurrencyCacheForTest{waitAllowed: true}
svc := NewConcurrencyService(cache)
allowed, err := svc.IncrementWaitCount(context.Background(), 1, 25)
require.NoError(t, err)
require.True(t, allowed)
}
func TestIncrementWaitCount_QueueFull(t *testing.T) {
cache := &stubConcurrencyCacheForTest{waitAllowed: false}
svc := NewConcurrencyService(cache)
allowed, err := svc.IncrementWaitCount(context.Background(), 1, 25)
require.NoError(t, err)
require.False(t, allowed)
}
func TestIncrementWaitCount_FailOpen(t *testing.T) {
// Redis 错误时应 fail-open(允许请求通过)
cache := &stubConcurrencyCacheForTest{waitErr: errors.New("redis timeout")}
svc := NewConcurrencyService(cache)
allowed, err := svc.IncrementWaitCount(context.Background(), 1, 25)
require.NoError(t, err, "Redis 错误不应传播")
require.True(t, allowed, "Redis 错误时应 fail-open")
}
func TestIncrementWaitCount_NilCache(t *testing.T) {
svc := &ConcurrencyService{cache: nil}
allowed, err := svc.IncrementWaitCount(context.Background(), 1, 25)
require.NoError(t, err)
require.True(t, allowed, "nil cache 应 fail-open")
}
func TestCalculateMaxWait(t *testing.T) {
tests := []struct {
concurrency int
expected int
}{
{5, 25}, // 5 + 20
{1, 21}, // 1 + 20
{0, 21}, // min(1) + 20
{-1, 21}, // min(1) + 20
{10, 30}, // 10 + 20
}
for _, tt := range tests {
result := CalculateMaxWait(tt.concurrency)
require.Equal(t, tt.expected, result, "CalculateMaxWait(%d)", tt.concurrency)
}
}
func TestGetAccountWaitingCount(t *testing.T) {
cache := &stubConcurrencyCacheForTest{waitCount: 5}
svc := NewConcurrencyService(cache)
count, err := svc.GetAccountWaitingCount(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, 5, count)
}
func TestGetAccountWaitingCount_NilCache(t *testing.T) {
svc := &ConcurrencyService{cache: nil}
count, err := svc.GetAccountWaitingCount(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, 0, count)
}
func TestGetAccountConcurrencyBatch(t *testing.T) {
cache := &stubConcurrencyCacheForTest{concurrency: 3}
svc := NewConcurrencyService(cache)
result, err := svc.GetAccountConcurrencyBatch(context.Background(), []int64{1, 2, 3})
require.NoError(t, err)
require.Len(t, result, 3)
for _, id := range []int64{1, 2, 3} {
require.Equal(t, 3, result[id])
}
}
func TestIncrementAccountWaitCount_FailOpen(t *testing.T) {
cache := &stubConcurrencyCacheForTest{waitErr: errors.New("redis error")}
svc := NewConcurrencyService(cache)
allowed, err := svc.IncrementAccountWaitCount(context.Background(), 1, 10)
require.NoError(t, err, "Redis 错误不应传播")
require.True(t, allowed, "Redis 错误时应 fail-open")
}
func TestIncrementAccountWaitCount_NilCache(t *testing.T) {
svc := &ConcurrencyService{cache: nil}
allowed, err := svc.IncrementAccountWaitCount(context.Background(), 1, 10)
require.NoError(t, err)
require.True(t, allowed)
}