Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions backend/internal/application/account/quota_refresh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package account
import (
"context"
"errors"
"fmt"
"path/filepath"
"sync"
"sync/atomic"
Expand Down Expand Up @@ -411,6 +412,57 @@ func TestRefreshWebImagineQuotaModeAtomicallyReplacesGroup(t *testing.T) {
}
}

func TestRefreshPaidWebImagineFallsBackToSharedWeeklyQuota(t *testing.T) {
ctx := context.Background()
database, err := relational.OpenSQLite(ctx, filepath.Join(t.TempDir(), "imagine-shared-weekly.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
if err := database.InitializeSchema(ctx); err != nil {
t.Fatal(err)
}
accounts := relational.NewAccountRepository(database)
credential, _, err := accounts.UpsertByIdentity(ctx, accountdomain.Credential{
Provider: accountdomain.ProviderWeb, AuthType: accountdomain.AuthTypeSSO, WebTier: accountdomain.WebTierSuper,
Name: "web-imagine-weekly", SourceKey: "web-imagine-weekly", EncryptedAccessToken: "encrypted",
Enabled: true, AuthStatus: accountdomain.AuthStatusActive,
})
if err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
if err := accounts.SaveQuotaWindows(ctx, credential.ID, accountdomain.WebTierSuper, now, []accountdomain.QuotaWindow{
{AccountID: credential.ID, Mode: "weekly", Remaining: 50, Total: 100, UpdatedAt: now},
{AccountID: credential.ID, Mode: accountdomain.QuotaModeWebImagePro, Remaining: 4, UpdatedAt: now},
}); err != nil {
t.Fatal(err)
}
adapter := &sharedWeeklyImagineAdapter{}
service := NewService(accounts, nil, nil, nil, provider.NewRegistry(adapter), nil, nil)
window, err := service.RefreshQuotaMode(ctx, credential.ID, accountdomain.QuotaModeWebImagePro)
if err != nil {
t.Fatal(err)
}
if window.Mode != "weekly" || window.Remaining != 80 || adapter.groupCalls.Load() != 1 || adapter.modeCalls.Load() != 1 {
t.Fatalf("window = %#v, group calls = %d, mode calls = %d", window, adapter.groupCalls.Load(), adapter.modeCalls.Load())
}
stored, err := accounts.GetQuotaWindows(ctx, []uint64{credential.ID})
if err != nil {
t.Fatal(err)
}
byMode := make(map[string]accountdomain.QuotaWindow)
for _, current := range stored[credential.ID] {
byMode[current.Mode] = current
}
if byMode["weekly"].Remaining != 80 {
t.Fatalf("weekly quota was not refreshed: %#v", stored[credential.ID])
}
if _, exists := byMode[accountdomain.QuotaModeWebImagePro]; exists {
t.Fatalf("stale product quota survived availability-only refresh: %#v", stored[credential.ID])
}
}

func TestRefreshConsoleQuotaModePersistsCompleteUsageSnapshot(t *testing.T) {
ctx := context.Background()
database, err := relational.OpenSQLite(ctx, filepath.Join(t.TempDir(), "console-quota-mode.db"))
Expand Down Expand Up @@ -900,6 +952,48 @@ type imagineQuotaGroupAdapter struct {
calls atomic.Int64
}

type sharedWeeklyImagineAdapter struct {
groupCalls atomic.Int64
modeCalls atomic.Int64
}

func (a *sharedWeeklyImagineAdapter) Provider() accountdomain.Provider {
return accountdomain.ProviderWeb
}

func (a *sharedWeeklyImagineAdapter) Definition() provider.Definition {
return provider.Definition{
Provider: accountdomain.ProviderWeb, ModelNamespace: accountdomain.ProviderWeb.ModelNamespace(),
Quota: provider.QuotaRemoteWindow, Credential: provider.CredentialSurface{AuthType: accountdomain.AuthTypeSSO},
}
}

func (a *sharedWeeklyImagineAdapter) SyncQuota(context.Context, accountdomain.Credential) (provider.QuotaSnapshot, error) {
return provider.QuotaSnapshot{}, errors.New("unexpected full quota sync")
}

func (a *sharedWeeklyImagineAdapter) SyncQuotaMode(_ context.Context, credential accountdomain.Credential, mode string) (accountdomain.QuotaWindow, error) {
a.modeCalls.Add(1)
if mode != "weekly" {
return accountdomain.QuotaWindow{}, fmt.Errorf("unexpected quota mode %q", mode)
}
now := time.Now().UTC()
return accountdomain.QuotaWindow{
AccountID: credential.ID, Mode: mode, Remaining: 80, Total: 100,
SyncedAt: &now, Source: accountdomain.QuotaSourceUpstream, UpdatedAt: now,
}, nil
}

func (a *sharedWeeklyImagineAdapter) SyncQuotaGroup(_ context.Context, _ accountdomain.Credential, group string) (provider.QuotaGroupSnapshot, error) {
a.groupCalls.Add(1)
if group != accountdomain.QuotaGroupWebImagine {
return provider.QuotaGroupSnapshot{}, fmt.Errorf("unexpected quota group %q", group)
}
return provider.QuotaGroupSnapshot{
Group: group, Modes: accountdomain.WebImagineQuotaModes(), SyncedAt: time.Now().UTC(),
}, nil
}

func (a *imagineQuotaGroupAdapter) Provider() accountdomain.Provider {
return accountdomain.ProviderWeb
}
Expand Down
36 changes: 29 additions & 7 deletions backend/internal/application/account/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3064,9 +3064,9 @@ func (s *Service) RefreshQuotaMode(ctx context.Context, id uint64, mode string)
return accountdomain.QuotaWindow{}, err
}
}
window, ok := quotaWindowByMode(refreshed.Windows, mode)
if !ok {
return accountdomain.QuotaWindow{}, fmt.Errorf("Provider usage 响应缺少 %s 额度", mode)
window, err := s.resolveRefreshedQuotaWindow(ctx, id, mode, refreshed)
if err != nil {
return accountdomain.QuotaWindow{}, err
}
if len(refreshed.Modes) == 0 && refreshed.Credential.Provider == accountdomain.ProviderConsole {
// One Console request refreshes all three authoritative windows. Reconcile
Expand All @@ -3079,6 +3079,13 @@ func (s *Service) RefreshQuotaMode(ctx context.Context, id uint64, mode string)
if err := s.reconcileQuotaRecoveryWindow(ctx, refreshed.Credential.Provider, id, window); err != nil {
return window, err
}
} else if window.Mode == "weekly" {
// The requested Imagine product was availability-only and resolved to
// the paid shared pool. Reconcile the authoritative weekly window too;
// the group reconciliation above only covers product-specific modes.
if err := s.reconcileQuotaRecoveryWindow(ctx, refreshed.Credential.Provider, id, window); err != nil {
return window, err
}
}
return window, nil
}
Expand All @@ -3102,11 +3109,26 @@ func (s *Service) ProbeQuotaMode(ctx context.Context, id uint64, mode string) (a
if !ok {
return accountdomain.QuotaWindow{}, fmt.Errorf("Provider 模式额度探测返回类型无效")
}
window, ok := quotaWindowByMode(refreshed.Windows, mode)
if !ok {
return accountdomain.QuotaWindow{}, fmt.Errorf("Provider usage 响应缺少 %s 额度", mode)
return s.resolveRefreshedQuotaWindow(ctx, id, mode, refreshed)
}

func (s *Service) resolveRefreshedQuotaWindow(ctx context.Context, id uint64, mode string, refreshed quotaRefreshResult) (accountdomain.QuotaWindow, error) {
if window, ok := quotaWindowByMode(refreshed.Windows, mode); ok {
return window, nil
}
return window, nil
credential := refreshed.Credential
paidWebImagine := credential.Provider == accountdomain.ProviderWeb && isWebImagineQuotaMode(mode) &&
(credential.WebTier == accountdomain.WebTierSuper || credential.WebTier == accountdomain.WebTierHeavy)
if paidWebImagine {
weekly, err := s.refreshQuotaMode(ctx, id, "weekly")
if err != nil {
return accountdomain.QuotaWindow{}, err
}
if window, ok := quotaWindowByMode(weekly.Windows, "weekly"); ok {
return window, nil
}
}
return accountdomain.QuotaWindow{}, fmt.Errorf("Provider usage 响应缺少 %s 额度", mode)
}

func (s *Service) refreshQuotaGroup(ctx context.Context, id uint64, group string) (quotaRefreshResult, error) {
Expand Down
16 changes: 13 additions & 3 deletions backend/internal/application/gateway/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ func (s *Service) executeImage(
}
}
quotaKind, _ := s.providers.QuotaKind(route.Provider)
refreshMode, decrementMode := quotaFinalizationModes(effectiveQuotaMode, quotaRefreshGroup)
refreshMode, decrementMode, availabilityMode := quotaFinalizationModes(effectiveQuotaMode, quotaRefreshGroup)
if successful && quotaKind == provider.QuotaRemoteWindow && refreshMode != "" {
if decrementMode != "" && decrementMode != "weekly" {
units := max(1, response.QuotaUnits)
Expand All @@ -342,6 +342,9 @@ func (s *Service) executeImage(
}
}
s.accounts.QueueQuotaRefresh(accountID, refreshMode)
if availabilityMode != "" && availabilityMode != refreshMode {
s.accounts.QueueQuotaRefresh(accountID, availabilityMode)
}
}
if err := budget.run("audit", finalizationAuditBudget, func(stageCtx context.Context) error {
return s.audits.Create(stageCtx, record)
Expand All @@ -359,10 +362,17 @@ func (s *Service) executeImage(
// upstream windows atomically, while the local fence must charge the exact
// window selected for this account so concurrent media requests cannot
// over-allocate during the short refresh delay.
func quotaFinalizationModes(effectiveMode, refreshGroup string) (refreshMode, decrementMode string) {
func quotaFinalizationModes(effectiveMode, refreshGroup string) (refreshMode, decrementMode, availabilityMode string) {
// Availability-only Imagine products on paid Web tiers are governed by the
// shared weekly pool. Refresh its numeric counter and also re-read the
// product group so available=false/nextAvailableAt can install an exact
// product fence that overrides weekly routing.
if effectiveMode == "weekly" {
return effectiveMode, effectiveMode, refreshGroup
}
refreshMode = effectiveMode
if refreshGroup != "" {
refreshMode = refreshGroup
}
return refreshMode, effectiveMode
return refreshMode, effectiveMode, ""
}
10 changes: 7 additions & 3 deletions backend/internal/application/gateway/selector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -745,9 +745,13 @@ func TestSelectorWebCatalogCapabilityStillEnforcesTier(t *testing.T) {
}

func TestImageQuotaFinalizationKeepsEffectiveConsumptionFence(t *testing.T) {
refreshMode, decrementMode := quotaFinalizationModes(account.QuotaModeWebImagePro, account.QuotaGroupWebImagine)
if refreshMode != account.QuotaGroupWebImagine || decrementMode != account.QuotaModeWebImagePro {
t.Fatalf("refresh=%q decrement=%q", refreshMode, decrementMode)
refreshMode, decrementMode, availabilityMode := quotaFinalizationModes(account.QuotaModeWebImagePro, account.QuotaGroupWebImagine)
if refreshMode != account.QuotaGroupWebImagine || decrementMode != account.QuotaModeWebImagePro || availabilityMode != "" {
t.Fatalf("refresh=%q decrement=%q availability=%q", refreshMode, decrementMode, availabilityMode)
}
refreshMode, decrementMode, availabilityMode = quotaFinalizationModes("weekly", account.QuotaGroupWebImagine)
if refreshMode != "weekly" || decrementMode != "weekly" || availabilityMode != account.QuotaGroupWebImagine {
t.Fatalf("shared weekly refresh=%q decrement=%q availability=%q", refreshMode, decrementMode, availabilityMode)
}
}

Expand Down
5 changes: 4 additions & 1 deletion backend/internal/application/gateway/video.go
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ func (s *Service) runVideoJob(parent context.Context, job media.Job, route model
return
}
s.selector.MarkSuccess(context.Background(), lease.Credential)
refreshMode, decrementMode := quotaFinalizationModes(lease.QuotaMode, quotaRefreshGroup)
refreshMode, decrementMode, availabilityMode := quotaFinalizationModes(lease.QuotaMode, quotaRefreshGroup)
if decrementMode != "" && decrementMode != "weekly" {
quotaCtx, quotaCancel := context.WithTimeout(context.Background(), accountStateWriteTimeout)
updated, quotaErr := s.accounts.DecrementQuota(quotaCtx, job.AccountID, decrementMode, 1)
Expand All @@ -757,6 +757,9 @@ func (s *Service) runVideoJob(parent context.Context, job media.Job, route model
}
if quotaKind, _ := s.providers.QuotaKind(route.Provider); quotaKind == provider.QuotaRemoteWindow && refreshMode != "" {
s.accounts.QueueQuotaRefresh(job.AccountID, refreshMode)
if availabilityMode != "" && availabilityMode != refreshMode {
s.accounts.QueueQuotaRefresh(job.AccountID, availabilityMode)
}
}
// 输入回收放在账号状态、计费和审计收尾之后,存储抖动不得延迟关键终态逻辑。
s.releaseVideoInputs(job)
Expand Down
10 changes: 7 additions & 3 deletions backend/internal/application/gateway/video_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ func TestVideoQuotaModeUsesWeb720pProduct(t *testing.T) {
}

func TestVideoQuotaFinalizationKeepsEffectiveConsumptionFence(t *testing.T) {
refreshMode, decrementMode := quotaFinalizationModes(account.QuotaModeWebVideo720p, account.QuotaGroupWebImagine)
if refreshMode != account.QuotaGroupWebImagine || decrementMode != account.QuotaModeWebVideo720p {
t.Fatalf("refresh=%q decrement=%q", refreshMode, decrementMode)
refreshMode, decrementMode, availabilityMode := quotaFinalizationModes(account.QuotaModeWebVideo720p, account.QuotaGroupWebImagine)
if refreshMode != account.QuotaGroupWebImagine || decrementMode != account.QuotaModeWebVideo720p || availabilityMode != "" {
t.Fatalf("refresh=%q decrement=%q availability=%q", refreshMode, decrementMode, availabilityMode)
}
refreshMode, decrementMode, availabilityMode = quotaFinalizationModes("weekly", account.QuotaGroupWebImagine)
if refreshMode != "weekly" || decrementMode != "weekly" || availabilityMode != account.QuotaGroupWebImagine {
t.Fatalf("shared weekly refresh=%q decrement=%q availability=%q", refreshMode, decrementMode, availabilityMode)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -680,11 +680,13 @@ func (r *AccountRepository) getRoutingQuotaWindows(ctx context.Context, provider
if provider != account.ProviderWeb && quotaMode == "" {
return result, nil
}
modes := make([]string, 0, 2)
// Paid Web chat routes are governed by the shared weekly pool. Imagine
// products have independent authoritative windows and must not be hidden by
// a weekly row merely because the same account also has paid chat access.
if provider == account.ProviderWeb && !account.IsWebImagineQuotaMode(quotaMode) {
modes := make([]string, 0, 3)
webImagineMode := provider == account.ProviderWeb && account.IsWebImagineQuotaMode(quotaMode)
// Paid Web routes use the shared weekly pool. Imagine may additionally
// expose a product-specific remainingQueries window (notably for Basic and
// older response shapes); load both and prefer the exact product window
// below, falling back to weekly only for confirmed Super/Heavy accounts.
if provider == account.ProviderWeb {
modes = append(modes, "weekly")
}
if provider == account.ProviderWeb && quotaMode == account.QuotaModeWebImageEdit {
Expand All @@ -711,10 +713,23 @@ func (r *AccountRepository) getRoutingQuotaWindows(ctx context.Context, provider
webTiers[credential.ID] = credential.WebTier
}
for _, row := range rows {
if provider == account.ProviderWeb && quotaMode == account.QuotaModeWebImageEdit {
if row.Mode != webImageEditRoutingQuotaMode(webTiers[row.AccountID]) {
continue
if webImagineMode {
tier := webTiers[row.AccountID]
productMode := quotaMode
if quotaMode == account.QuotaModeWebImageEdit {
productMode = webImageEditRoutingQuotaMode(tier)
}
switch {
case row.Mode == productMode:
// An explicit product window is more precise than the shared pool,
// regardless of query order.
result[row.AccountID] = toRoutingQuotaWindowDomain(row)
case row.Mode == "weekly" && (tier == account.WebTierSuper || tier == account.WebTierHeavy):
if existing, exists := result[row.AccountID]; !exists || existing.Mode != productMode {
result[row.AccountID] = toRoutingQuotaWindowDomain(row)
}
}
continue
}
if _, exists := result[row.AccountID]; !exists {
result[row.AccountID] = toRoutingQuotaWindowDomain(row)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func TestRoutingProjectionMapsWebImageEditQuotaByTier(t *testing.T) {
t.Fatal(createErr)
}
if saveErr := accounts.SaveQuotaWindows(ctx, value.ID, tier, now, []account.QuotaWindow{
{AccountID: value.ID, Mode: "weekly", Remaining: 11, SyncedAt: &now, Source: account.QuotaSourceUpstream},
{AccountID: value.ID, Mode: account.QuotaModeWebImagePro, Remaining: 3, SyncedAt: &now, Source: account.QuotaSourceUpstream},
{AccountID: value.ID, Mode: account.QuotaModeWebImageEdit, Remaining: 7, SyncedAt: &now, Source: account.QuotaSourceUpstream},
}); saveErr != nil {
Expand All @@ -155,6 +156,56 @@ func TestRoutingProjectionMapsWebImageEditQuotaByTier(t *testing.T) {
}
}

func TestRoutingProjectionFallsBackToWeeklyForPaidWebImagine(t *testing.T) {
ctx := context.Background()
database, err := OpenSQLite(ctx, filepath.Join(t.TempDir(), "routing-web-imagine-weekly.db"))
if err != nil {
t.Fatal(err)
}
defer database.Close()
if err := database.InitializeSchema(ctx); err != nil {
t.Fatal(err)
}
accounts := NewAccountRepository(database)
now := time.Now().UTC()
create := func(name string, tier account.WebTier) account.Credential {
value, _, createErr := accounts.UpsertByIdentity(ctx, account.Credential{
Provider: account.ProviderWeb, AuthType: account.AuthTypeSSO, Name: name, SourceKey: name,
EncryptedAccessToken: "encrypted", Enabled: true, AuthStatus: account.AuthStatusActive, WebTier: tier,
})
if createErr != nil {
t.Fatal(createErr)
}
if saveErr := accounts.SaveQuotaWindows(ctx, value.ID, tier, now, []account.QuotaWindow{{
AccountID: value.ID, Mode: "weekly", Remaining: 9, Total: 10,
SyncedAt: &now, Source: account.QuotaSourceUpstream,
}}); saveErr != nil {
t.Fatal(saveErr)
}
return value
}
basic := create("basic-weekly", account.WebTierBasic)
super := create("super-weekly", account.WebTierSuper)
heavy := create("heavy-weekly", account.WebTierHeavy)

bases, err := accounts.ListRoutingAccountBases(ctx, account.ProviderWeb, account.QuotaModeWebImagePro)
if err != nil {
t.Fatal(err)
}
byID := make(map[uint64]account.RoutingAccountBase, len(bases))
for _, base := range bases {
byID[base.Credential.ID] = base
}
if got := byID[basic.ID].QuotaWindow; got != nil {
t.Fatalf("Basic must not inherit paid weekly Imagine quota: %#v", got)
}
for _, value := range []account.Credential{super, heavy} {
if got := byID[value.ID].QuotaWindow; got == nil || got.Mode != "weekly" || got.Remaining != 9 {
t.Fatalf("paid Imagine weekly fallback for %d = %#v", value.ID, got)
}
}
}

func TestGetCredentialMaterialHydratesOneAccountAndMapsNotFound(t *testing.T) {
ctx := context.Background()
database, err := OpenSQLite(ctx, filepath.Join(t.TempDir(), "credential-material.db"))
Expand Down
Loading
Loading