Skip to content

Commit c1d46d2

Browse files
fix(proxy): clear stale session affinity after failures
1 parent fc6d59b commit c1d46d2

4 files changed

Lines changed: 47 additions & 0 deletions

File tree

auth/session_affinity_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,27 @@ func TestWaitForSessionAvailableRespectsExcludeSet(t *testing.T) {
123123
t.Fatalf("proxyURL = %q, want empty fallback proxy", proxyURL)
124124
}
125125
}
126+
127+
func TestUnbindSessionAffinityRemovesMatchingBinding(t *testing.T) {
128+
store := &Store{
129+
accounts: []*Account{
130+
{DBID: 1, AccessToken: "tok-1"},
131+
{DBID: 2, AccessToken: "tok-2"},
132+
},
133+
maxConcurrency: 1,
134+
}
135+
store.bindSessionAffinity("session-1", store.accounts[1], "http://proxy-2")
136+
137+
store.UnbindSessionAffinity("session-1", 2)
138+
139+
acc, proxyURL := store.NextForSession("session-1", nil)
140+
if acc == nil {
141+
t.Fatal("expected fallback account")
142+
}
143+
if acc.DBID != 1 {
144+
t.Fatalf("account DBID = %d, want %d", acc.DBID, 1)
145+
}
146+
if proxyURL != "" {
147+
t.Fatalf("proxyURL = %q, want empty fallback proxy", proxyURL)
148+
}
149+
}

auth/store.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,23 @@ func (s *Store) bindSessionAffinity(key string, account *Account, proxyURL strin
13131313
s.sessionMu.Unlock()
13141314
}
13151315

1316+
// UnbindSessionAffinity removes a session binding when it still points to the failed account.
1317+
func (s *Store) UnbindSessionAffinity(key string, accountID int64) {
1318+
if s == nil || accountID == 0 {
1319+
return
1320+
}
1321+
key = strings.TrimSpace(key)
1322+
if key == "" {
1323+
return
1324+
}
1325+
1326+
s.sessionMu.Lock()
1327+
if binding, ok := s.sessionBindings[key]; ok && binding.accountID == accountID {
1328+
delete(s.sessionBindings, key)
1329+
}
1330+
s.sessionMu.Unlock()
1331+
}
1332+
13161333
// NextForSession 优先复用已绑定的账号和代理,失败时回退到普通选号。
13171334
func (s *Store) NextForSession(key string, exclude map[int64]bool) (*Account, string) {
13181335
if s == nil {

proxy/handler.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ func (h *Handler) Responses(c *gin.Context) {
497497
h.store.ReportRequestFailure(account, kind, time.Duration(durationMs)*time.Millisecond)
498498
}
499499
h.store.Release(account)
500+
h.store.UnbindSessionAffinity(sessionID, account.ID())
500501
excludeAccounts[account.ID()] = true
501502

502503
// 不可重试的结构化错误直接返回
@@ -520,6 +521,7 @@ func (h *Handler) Responses(c *gin.Context) {
520521
errBody, _ := io.ReadAll(resp.Body)
521522
resp.Body.Close()
522523
h.store.Release(account)
524+
h.store.UnbindSessionAffinity(sessionID, account.ID())
523525
excludeAccounts[account.ID()] = true
524526

525527
log.Printf("上游返回错误 (attempt %d, status %d): %s", attempt+1, resp.StatusCode, string(errBody))
@@ -866,6 +868,7 @@ func (h *Handler) ChatCompletions(c *gin.Context) {
866868
h.store.ReportRequestFailure(account, kind, time.Duration(durationMs)*time.Millisecond)
867869
}
868870
h.store.Release(account)
871+
h.store.UnbindSessionAffinity(sessionID, account.ID())
869872
excludeAccounts[account.ID()] = true
870873

871874
// 不可重试的结构化错误直接返回
@@ -889,6 +892,7 @@ func (h *Handler) ChatCompletions(c *gin.Context) {
889892
errBody, _ := io.ReadAll(resp.Body)
890893
resp.Body.Close()
891894
h.store.Release(account)
895+
h.store.UnbindSessionAffinity(sessionID, account.ID())
892896
excludeAccounts[account.ID()] = true
893897

894898
log.Printf("上游返回错误 (attempt %d, status %d): %s", attempt+1, resp.StatusCode, string(errBody))

proxy/handler_anthropic.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ func (h *Handler) Messages(c *gin.Context) {
165165
h.store.ReportRequestFailure(account, kind, time.Duration(durationMs)*time.Millisecond)
166166
}
167167
h.store.Release(account)
168+
h.store.UnbindSessionAffinity(sessionID, account.ID())
168169
excludeAccounts[account.ID()] = true
169170

170171
if !IsRetryableError(reqErr) && classifyTransportFailure(reqErr) == "" {
@@ -187,6 +188,7 @@ func (h *Handler) Messages(c *gin.Context) {
187188
errBody, _ := io.ReadAll(resp.Body)
188189
resp.Body.Close()
189190
h.store.Release(account)
191+
h.store.UnbindSessionAffinity(sessionID, account.ID())
190192
excludeAccounts[account.ID()] = true
191193

192194
log.Printf("上游返回错误 (attempt %d, status %d, /v1/messages): %s", attempt+1, resp.StatusCode, string(errBody))

0 commit comments

Comments
 (0)