Skip to content

Commit 2b30e3b

Browse files
committed
Reduce scheduler rebuilds on neutral extra updates
1 parent 7455476 commit 2b30e3b

2 files changed

Lines changed: 142 additions & 3 deletions

File tree

backend/internal/repository/account_repo.go

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"encoding/json"
1717
"errors"
1818
"strconv"
19+
"strings"
1920
"time"
2021

2122
dbent "github.com/Wei-Shaw/sub2api/ent"
@@ -50,6 +51,18 @@ type accountRepository struct {
5051
schedulerCache service.SchedulerCache
5152
}
5253

54+
var schedulerNeutralExtraKeyPrefixes = []string{
55+
"codex_primary_",
56+
"codex_secondary_",
57+
"codex_5h_",
58+
"codex_7d_",
59+
}
60+
61+
var schedulerNeutralExtraKeys = map[string]struct{}{
62+
"codex_usage_updated_at": {},
63+
"session_window_utilization": {},
64+
}
65+
5366
// NewAccountRepository 创建账户仓储实例。
5467
// 这是对外暴露的构造函数,返回接口类型以便于依赖注入。
5568
func NewAccountRepository(client *dbent.Client, sqlDB *sql.DB, schedulerCache service.SchedulerCache) service.AccountRepository {
@@ -613,6 +626,29 @@ func (r *accountRepository) syncSchedulerAccountSnapshot(ctx context.Context, ac
613626
}
614627
}
615628

629+
func (r *accountRepository) patchSchedulerAccountExtra(ctx context.Context, accountID int64, updates map[string]any) {
630+
if r == nil || r.schedulerCache == nil || accountID <= 0 || len(updates) == 0 {
631+
return
632+
}
633+
account, err := r.schedulerCache.GetAccount(ctx, accountID)
634+
if err != nil {
635+
logger.LegacyPrintf("repository.account", "[Scheduler] patch account extra read failed: id=%d err=%v", accountID, err)
636+
return
637+
}
638+
if account == nil {
639+
return
640+
}
641+
if account.Extra == nil {
642+
account.Extra = make(map[string]any, len(updates))
643+
}
644+
for key, value := range updates {
645+
account.Extra[key] = value
646+
}
647+
if err := r.schedulerCache.SetAccount(ctx, account); err != nil {
648+
logger.LegacyPrintf("repository.account", "[Scheduler] patch account extra write failed: id=%d err=%v", accountID, err)
649+
}
650+
}
651+
616652
func (r *accountRepository) syncSchedulerAccountSnapshots(ctx context.Context, accountIDs []int64) {
617653
if r == nil || r.schedulerCache == nil || len(accountIDs) == 0 {
618654
return
@@ -1185,12 +1221,47 @@ func (r *accountRepository) UpdateExtra(ctx context.Context, id int64, updates m
11851221
if affected == 0 {
11861222
return service.ErrAccountNotFound
11871223
}
1188-
if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventAccountChanged, &id, nil, nil); err != nil {
1189-
logger.LegacyPrintf("repository.account", "[SchedulerOutbox] enqueue extra update failed: account=%d err=%v", id, err)
1224+
1225+
if shouldEnqueueSchedulerOutboxForExtraUpdates(updates) {
1226+
if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventAccountChanged, &id, nil, nil); err != nil {
1227+
logger.LegacyPrintf("repository.account", "[SchedulerOutbox] enqueue extra update failed: account=%d err=%v", id, err)
1228+
}
1229+
} else {
1230+
// 观测型 extra 字段不需要触发 bucket 重建,但尽量把单账号缓存补到最新,
1231+
// 让 sticky session / GetAccount 命中缓存时也能读到最新快照。
1232+
r.patchSchedulerAccountExtra(ctx, id, updates)
11901233
}
11911234
return nil
11921235
}
11931236

1237+
func shouldEnqueueSchedulerOutboxForExtraUpdates(updates map[string]any) bool {
1238+
if len(updates) == 0 {
1239+
return false
1240+
}
1241+
for key := range updates {
1242+
if !isSchedulerNeutralExtraKey(key) {
1243+
return true
1244+
}
1245+
}
1246+
return false
1247+
}
1248+
1249+
func isSchedulerNeutralExtraKey(key string) bool {
1250+
key = strings.TrimSpace(key)
1251+
if key == "" {
1252+
return false
1253+
}
1254+
if _, ok := schedulerNeutralExtraKeys[key]; ok {
1255+
return true
1256+
}
1257+
for _, prefix := range schedulerNeutralExtraKeyPrefixes {
1258+
if strings.HasPrefix(key, prefix) {
1259+
return true
1260+
}
1261+
}
1262+
return false
1263+
}
1264+
11941265
func (r *accountRepository) BulkUpdate(ctx context.Context, ids []int64, updates service.AccountBulkUpdate) (int64, error) {
11951266
if len(ids) == 0 {
11961267
return 0, nil

backend/internal/repository/account_repo_integration_test.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type AccountRepoSuite struct {
2323

2424
type schedulerCacheRecorder struct {
2525
setAccounts []*service.Account
26+
accounts map[int64]*service.Account
2627
}
2728

2829
func (s *schedulerCacheRecorder) GetSnapshot(ctx context.Context, bucket service.SchedulerBucket) ([]*service.Account, bool, error) {
@@ -34,11 +35,20 @@ func (s *schedulerCacheRecorder) SetSnapshot(ctx context.Context, bucket service
3435
}
3536

3637
func (s *schedulerCacheRecorder) GetAccount(ctx context.Context, accountID int64) (*service.Account, error) {
37-
return nil, nil
38+
if s.accounts == nil {
39+
return nil, nil
40+
}
41+
return s.accounts[accountID], nil
3842
}
3943

4044
func (s *schedulerCacheRecorder) SetAccount(ctx context.Context, account *service.Account) error {
4145
s.setAccounts = append(s.setAccounts, account)
46+
if s.accounts == nil {
47+
s.accounts = make(map[int64]*service.Account)
48+
}
49+
if account != nil {
50+
s.accounts[account.ID] = account
51+
}
4252
return nil
4353
}
4454

@@ -623,6 +633,64 @@ func (s *AccountRepoSuite) TestUpdateExtra_NilExtra() {
623633
s.Require().Equal("val", got.Extra["key"])
624634
}
625635

636+
func (s *AccountRepoSuite) TestUpdateExtra_SchedulerNeutralSkipsOutboxAndPatchesCache() {
637+
account := mustCreateAccount(s.T(), s.client, &service.Account{
638+
Name: "acc-extra-neutral",
639+
Platform: service.PlatformOpenAI,
640+
Extra: map[string]any{"codex_usage_updated_at": "old"},
641+
})
642+
cacheRecorder := &schedulerCacheRecorder{
643+
accounts: map[int64]*service.Account{
644+
account.ID: {
645+
ID: account.ID,
646+
Platform: account.Platform,
647+
Extra: map[string]any{
648+
"codex_usage_updated_at": "old",
649+
},
650+
},
651+
},
652+
}
653+
s.repo.schedulerCache = cacheRecorder
654+
655+
updates := map[string]any{
656+
"codex_usage_updated_at": "2026-03-11T10:00:00Z",
657+
"codex_5h_used_percent": 88.5,
658+
"session_window_utilization": 0.42,
659+
}
660+
s.Require().NoError(s.repo.UpdateExtra(s.ctx, account.ID, updates))
661+
662+
got, err := s.repo.GetByID(s.ctx, account.ID)
663+
s.Require().NoError(err)
664+
s.Require().Equal("2026-03-11T10:00:00Z", got.Extra["codex_usage_updated_at"])
665+
s.Require().Equal(88.5, got.Extra["codex_5h_used_percent"])
666+
s.Require().Equal(0.42, got.Extra["session_window_utilization"])
667+
668+
var outboxCount int
669+
s.Require().NoError(scanSingleRow(s.ctx, s.repo.sql, "SELECT COUNT(*) FROM scheduler_outbox", nil, &outboxCount))
670+
s.Require().Zero(outboxCount)
671+
s.Require().Len(cacheRecorder.setAccounts, 1)
672+
s.Require().NotNil(cacheRecorder.accounts[account.ID])
673+
s.Require().Equal("2026-03-11T10:00:00Z", cacheRecorder.accounts[account.ID].Extra["codex_usage_updated_at"])
674+
}
675+
676+
func (s *AccountRepoSuite) TestUpdateExtra_SchedulerRelevantStillEnqueuesOutbox() {
677+
account := mustCreateAccount(s.T(), s.client, &service.Account{
678+
Name: "acc-extra-mixed",
679+
Platform: service.PlatformAntigravity,
680+
Extra: map[string]any{},
681+
})
682+
683+
updates := map[string]any{
684+
"mixed_scheduling": true,
685+
"codex_usage_updated_at": "2026-03-11T10:00:00Z",
686+
}
687+
s.Require().NoError(s.repo.UpdateExtra(s.ctx, account.ID, updates))
688+
689+
var outboxCount int
690+
s.Require().NoError(scanSingleRow(s.ctx, s.repo.sql, "SELECT COUNT(*) FROM scheduler_outbox", nil, &outboxCount))
691+
s.Require().Equal(1, outboxCount)
692+
}
693+
626694
// --- GetByCRSAccountID ---
627695

628696
func (s *AccountRepoSuite) TestGetByCRSAccountID() {

0 commit comments

Comments
 (0)