Skip to content

Commit 42d48a7

Browse files
committed
fix(admin): async plan-type probe on reset, avoid blocking batch resets
Reset endpoints no longer wait for the upstream Codex probe — it runs in a background goroutine with its own 15s timeout and detached context, so BatchResetStatus returns immediately and N accounts probe in parallel instead of serially. Also fix mojibake in the persist-plan_type log.
1 parent cfc1d0d commit 42d48a7

3 files changed

Lines changed: 37 additions & 20 deletions

File tree

admin/handler.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2714,18 +2714,17 @@ func (h *Handler) BatchResetStatus(c *gin.Context) {
27142714
})
27152715
}
27162716

2717-
func (h *Handler) syncAccountPlanAfterReset(parent context.Context, acc *auth.Account) {
2717+
func (h *Handler) syncAccountPlanAfterReset(_ context.Context, acc *auth.Account) {
27182718
if h == nil || h.syncAccountPlanOnReset == nil || acc == nil {
27192719
return
27202720
}
2721-
if parent == nil {
2722-
parent = context.Background()
2723-
}
2724-
ctx, cancel := context.WithTimeout(parent, 15*time.Second)
2725-
defer cancel()
2726-
if err := h.syncAccountPlanOnReset(ctx, acc); err != nil {
2727-
log.Printf("[account %d] sync Codex plan type after reset failed: %v", acc.DBID, err)
2728-
}
2721+
go func() {
2722+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
2723+
defer cancel()
2724+
if err := h.syncAccountPlanOnReset(ctx, acc); err != nil {
2725+
log.Printf("[账号 %d] 重置后同步 Codex plan type 失败: %v", acc.DBID, err)
2726+
}
2727+
}()
27292728
}
27302729

27312730
func (h *Handler) syncSingleAccountPlanOnReset(ctx context.Context, acc *auth.Account) error {

admin/handler_test.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,16 @@ func TestResetAccountStatusSyncsPlanMetadata(t *testing.T) {
155155
account.SetUsageSnapshot(88, time.Now().Add(time.Hour))
156156
store.AddAccount(account)
157157

158-
var called bool
158+
called := make(chan int64, 1)
159159
handler := &Handler{
160160
store: store,
161161
syncAccountPlanOnReset: func(_ context.Context, acc *auth.Account) error {
162-
called = true
163-
if acc == nil || acc.DBID != 42 {
164-
t.Fatalf("sync account = %#v, want DBID 42", acc)
162+
if acc == nil {
163+
t.Errorf("sync account is nil")
164+
called <- -1
165+
return nil
165166
}
167+
called <- acc.DBID
166168
return nil
167169
},
168170
}
@@ -177,7 +179,12 @@ func TestResetAccountStatusSyncsPlanMetadata(t *testing.T) {
177179
if recorder.Code != http.StatusOK {
178180
t.Fatalf("status = %d, want %d, body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
179181
}
180-
if !called {
182+
select {
183+
case id := <-called:
184+
if id != 42 {
185+
t.Fatalf("sync DBID = %d, want 42", id)
186+
}
187+
case <-time.After(2 * time.Second):
181188
t.Fatal("expected reset to sync plan metadata")
182189
}
183190
if _, ok := account.GetUsagePercent7d(); ok {
@@ -192,11 +199,11 @@ func TestBatchResetStatusSyncsEachResolvedAccount(t *testing.T) {
192199
store.AddAccount(&auth.Account{DBID: 11, AccessToken: "at-11", PlanType: "free"})
193200
store.AddAccount(&auth.Account{DBID: 22, AccessToken: "at-22", PlanType: "plus"})
194201

195-
var gotIDs []int64
202+
gotIDs := make(chan int64, 2)
196203
handler := &Handler{
197204
store: store,
198205
syncAccountPlanOnReset: func(_ context.Context, acc *auth.Account) error {
199-
gotIDs = append(gotIDs, acc.DBID)
206+
gotIDs <- acc.DBID
200207
if acc.DBID == 22 {
201208
return errors.New("temporary upstream failure")
202209
}
@@ -214,16 +221,27 @@ func TestBatchResetStatusSyncsEachResolvedAccount(t *testing.T) {
214221
if recorder.Code != http.StatusOK {
215222
t.Fatalf("status = %d, want %d, body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
216223
}
217-
if fmt.Sprint(gotIDs) != "[11 22]" {
218-
t.Fatalf("synced ids = %v, want [11 22]", gotIDs)
219-
}
220224
var payload map[string]interface{}
221225
if err := json.Unmarshal(recorder.Body.Bytes(), &payload); err != nil {
222226
t.Fatalf("decode response: %v", err)
223227
}
224228
if payload["success"] != float64(2) || payload["failed"] != float64(1) {
225229
t.Fatalf("payload = %#v, want success=2 failed=1", payload)
226230
}
231+
232+
collected := make(map[int64]bool)
233+
deadline := time.After(2 * time.Second)
234+
for len(collected) < 2 {
235+
select {
236+
case id := <-gotIDs:
237+
collected[id] = true
238+
case <-deadline:
239+
t.Fatalf("synced ids = %v, want {11,22}", collected)
240+
}
241+
}
242+
if !collected[11] || !collected[22] {
243+
t.Fatalf("synced ids = %v, want both 11 and 22", collected)
244+
}
227245
}
228246

229247
func TestCreateAPIKeyPersistsQuotaAndExpiration(t *testing.T) {

auth/store.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4009,7 +4009,7 @@ func (s *Store) UpdateAccountPlanType(acc *Account, planType string) bool {
40094009
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
40104010
defer cancel()
40114011
if err := s.db.UpdateCredentials(ctx, acc.DBID, map[string]interface{}{"plan_type": plan}); err != nil {
4012-
log.Printf("[璐﹀彿 %d] 鎸佷箙鍖?plan_type 澶辫触: %v", acc.DBID, err)
4012+
log.Printf("[账号 %d] 持久化 plan_type 失败: %v", acc.DBID, err)
40134013
}
40144014
return changed
40154015
}

0 commit comments

Comments
 (0)