| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- //go:build unit
- package service
- import (
- "context"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- )
- type snapshotHydrationCache struct {
- snapshot []*Account
- accounts map[int64]*Account
- }
- func (c *snapshotHydrationCache) GetSnapshot(ctx context.Context, bucket SchedulerBucket) ([]*Account, bool, error) {
- return c.snapshot, true, nil
- }
- func (c *snapshotHydrationCache) CaptureBucketWriteToken(ctx context.Context, bucket SchedulerBucket) (SchedulerBucketWriteToken, error) {
- return SchedulerBucketWriteToken{Bucket: bucket, Epoch: 1}, nil
- }
- func (c *snapshotHydrationCache) SetSnapshot(ctx context.Context, bucket SchedulerBucket, token SchedulerBucketWriteToken, accounts []Account) error {
- return nil
- }
- func (c *snapshotHydrationCache) RetireBucket(ctx context.Context, bucket SchedulerBucket) error {
- return nil
- }
- func (c *snapshotHydrationCache) ReopenBucket(ctx context.Context, bucket SchedulerBucket) (SchedulerBucketWriteToken, error) {
- return SchedulerBucketWriteToken{Bucket: bucket, Epoch: 1}, nil
- }
- func (c *snapshotHydrationCache) TryAcquireGroupLifecycleLease(context.Context, int64, time.Duration) (SchedulerGroupLifecycleLease, bool, error) {
- return SchedulerGroupLifecycleLease{}, false, nil
- }
- func (c *snapshotHydrationCache) ReleaseGroupLifecycleLease(context.Context, SchedulerGroupLifecycleLease) error {
- return nil
- }
- func (c *snapshotHydrationCache) GetAccount(ctx context.Context, accountID int64) (*Account, error) {
- if c.accounts == nil {
- return nil, nil
- }
- return c.accounts[accountID], nil
- }
- func (c *snapshotHydrationCache) SetAccount(ctx context.Context, account *Account) error {
- return nil
- }
- func (c *snapshotHydrationCache) DeleteAccount(ctx context.Context, accountID int64) error {
- return nil
- }
- func (c *snapshotHydrationCache) UpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error {
- return nil
- }
- func (c *snapshotHydrationCache) TryLockBucket(ctx context.Context, bucket SchedulerBucket, ttl time.Duration) (bool, error) {
- return true, nil
- }
- func (c *snapshotHydrationCache) UnlockBucket(ctx context.Context, bucket SchedulerBucket) error {
- return nil
- }
- func (c *snapshotHydrationCache) ListBuckets(ctx context.Context) ([]SchedulerBucket, error) {
- return nil, nil
- }
- func (c *snapshotHydrationCache) GetOutboxWatermark(ctx context.Context) (int64, error) {
- return 0, nil
- }
- func (c *snapshotHydrationCache) SetOutboxWatermark(ctx context.Context, id int64) error {
- return nil
- }
- func TestOpenAISelectAccountWithLoadAwareness_HydratesSelectedAccountFromSchedulerSnapshot(t *testing.T) {
- cache := &snapshotHydrationCache{
- snapshot: []*Account{
- {
- ID: 1,
- Platform: PlatformOpenAI,
- Type: AccountTypeAPIKey,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Priority: 1,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "gpt-4": "gpt-4",
- },
- },
- },
- },
- accounts: map[int64]*Account{
- 1: {
- ID: 1,
- Platform: PlatformOpenAI,
- Type: AccountTypeAPIKey,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Priority: 1,
- Credentials: map[string]any{
- "api_key": "sk-live",
- "model_mapping": map[string]any{"gpt-4": "gpt-4"},
- },
- },
- },
- }
- schedulerSnapshot := NewSchedulerSnapshotService(cache, nil, nil, nil, nil)
- groupID := int64(2)
- svc := &OpenAIGatewayService{
- schedulerSnapshot: schedulerSnapshot,
- cache: &stubGatewayCache{},
- }
- selection, err := svc.SelectAccountWithLoadAwareness(context.Background(), &groupID, "", "gpt-4", nil)
- if err != nil {
- t.Fatalf("SelectAccountWithLoadAwareness error: %v", err)
- }
- if selection == nil || selection.Account == nil {
- t.Fatalf("expected selected account")
- }
- if got := selection.Account.GetOpenAIApiKey(); got != "sk-live" {
- t.Fatalf("expected hydrated api key, got %q", got)
- }
- }
- func TestOpenAINewAcquiredSelectionResult_ReleasesSlotWhenHydrationFails(t *testing.T) {
- cache := &snapshotHydrationCache{
- accounts: map[int64]*Account{},
- }
- schedulerSnapshot := NewSchedulerSnapshotService(cache, nil, stubOpenAIAccountRepo{}, nil, nil)
- svc := &OpenAIGatewayService{
- schedulerSnapshot: schedulerSnapshot,
- }
- releaseCalls := 0
- selection, err := svc.newAcquiredSelectionResult(context.Background(), &Account{ID: 1001}, func() {
- releaseCalls++
- })
- if err == nil {
- t.Fatalf("expected hydration error")
- }
- if selection != nil {
- t.Fatalf("expected nil selection on hydration error")
- }
- if releaseCalls != 1 {
- t.Fatalf("expected release to be called once, got %d", releaseCalls)
- }
- }
- func TestGatewaySelectAccountWithLoadAwareness_HydratesSelectedAccountFromSchedulerSnapshot(t *testing.T) {
- cache := &snapshotHydrationCache{
- snapshot: []*Account{
- {
- ID: 9,
- Platform: PlatformAnthropic,
- Type: AccountTypeAPIKey,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Priority: 1,
- },
- },
- accounts: map[int64]*Account{
- 9: {
- ID: 9,
- Platform: PlatformAnthropic,
- Type: AccountTypeAPIKey,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Priority: 1,
- Credentials: map[string]any{
- "api_key": "anthropic-live-key",
- },
- },
- },
- }
- schedulerSnapshot := NewSchedulerSnapshotService(cache, nil, nil, nil, nil)
- svc := &GatewayService{
- schedulerSnapshot: schedulerSnapshot,
- cache: &mockGatewayCacheForPlatform{},
- cfg: testConfig(),
- }
- result, err := svc.SelectAccountWithLoadAwareness(context.Background(), nil, "", "claude-3-5-sonnet-20241022", nil, "", 0)
- if err != nil {
- t.Fatalf("SelectAccountWithLoadAwareness error: %v", err)
- }
- if result == nil || result.Account == nil {
- t.Fatalf("expected selected account")
- }
- if got := result.Account.GetCredential("api_key"); got != "anthropic-live-key" {
- t.Fatalf("expected hydrated api key, got %q", got)
- }
- }
- func TestGatewaySelectAccountWithLoadAwareness_SkipsAntigravityGeminiFamilyRateLimitedSnapshot(t *testing.T) {
- resetAt := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
- cache := &snapshotHydrationCache{
- snapshot: []*Account{
- {
- ID: 1,
- Platform: PlatformAntigravity,
- Type: AccountTypeOAuth,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Priority: 1,
- AccountGroups: []AccountGroup{
- {AccountID: 1, GroupID: 22},
- },
- GroupIDs: []int64{22},
- Extra: map[string]any{
- "mixed_scheduling": true,
- modelRateLimitsKey: map[string]any{
- antigravityGeminiModelRateLimitKey: map[string]any{
- "rate_limit_reset_at": resetAt,
- },
- },
- },
- },
- {
- ID: 2,
- Platform: PlatformAntigravity,
- Type: AccountTypeOAuth,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Priority: 2,
- AccountGroups: []AccountGroup{
- {AccountID: 2, GroupID: 22},
- },
- GroupIDs: []int64{22},
- Extra: map[string]any{
- "mixed_scheduling": true,
- },
- },
- },
- accounts: map[int64]*Account{
- 1: {ID: 1, Platform: PlatformAntigravity, Type: AccountTypeOAuth},
- 2: {ID: 2, Platform: PlatformAntigravity, Type: AccountTypeOAuth},
- },
- }
- groupID := int64(22)
- svc := &GatewayService{
- schedulerSnapshot: NewSchedulerSnapshotService(cache, nil, nil, nil, nil),
- groupRepo: &mockGroupRepoForGateway{
- groups: map[int64]*Group{
- groupID: {
- ID: groupID,
- Platform: PlatformGemini,
- Status: StatusActive,
- Hydrated: true,
- },
- },
- },
- concurrencyService: NewConcurrencyService(&mockConcurrencyCache{}),
- cfg: &config.Config{
- Gateway: config.GatewayConfig{
- Scheduling: config.GatewaySchedulingConfig{
- LoadBatchEnabled: true,
- StickySessionMaxWaiting: 3,
- StickySessionWaitTimeout: time.Second,
- FallbackWaitTimeout: time.Second,
- FallbackMaxWaiting: 10,
- },
- },
- },
- }
- result, err := svc.SelectAccountWithLoadAwareness(context.Background(), &groupID, "", "gemini-3-flash-preview", nil, "", 0)
- if err != nil {
- t.Fatalf("SelectAccountWithLoadAwareness error: %v", err)
- }
- if result == nil || result.Account == nil {
- t.Fatalf("expected selected account")
- }
- if result.Account.ID != 2 {
- t.Fatalf("expected scheduler to skip Gemini-family limited antigravity account 1, got %d", result.Account.ID)
- }
- }
|