fix: harden concurrent quota and status updates

This commit is contained in:
CaIon
2026-08-11 22:03:47 +08:00
parent 50e5377ea5
commit ccd535ef8e
14 changed files with 702 additions and 203 deletions
+22 -11
View File
@@ -346,11 +346,21 @@ func (channel *Channel) Save() error {
return DB.Save(channel).Error
}
func (channel *Channel) SaveWithoutKey() error {
// saveStatusState persists only the fields owned by the channel status flow.
// Keeping this allowlist here prevents a stale channel snapshot from
// overwriting credentials, accounting counters, or channel configuration.
func (channel *Channel) saveStatusState() error {
if channel.Id == 0 {
return errors.New("channel ID is 0")
}
return DB.Omit("key").Save(channel).Error
updates := map[string]any{
"status": channel.Status,
"other_info": channel.OtherInfo,
}
if channel.ChannelInfo.IsMultiKey {
updates["channel_info"] = channel.ChannelInfo
}
return DB.Model(&Channel{}).Where("id = ?", channel.Id).Updates(updates).Error
}
func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool, sortOptions ...ChannelSortOptions) ([]*Channel, error) {
@@ -713,19 +723,24 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri
if common.MemoryCacheEnabled {
channelStatusLock.Lock()
defer channelStatusLock.Unlock()
}
// ChannelInfo stores both multi-key status and the polling cursor. Hold the
// same per-channel lock from the first read through persistence so neither
// writer can save a stale JSON snapshot over the other.
pollingLock := GetChannelPollingLock(channelId)
pollingLock.Lock()
defer pollingLock.Unlock()
if common.MemoryCacheEnabled {
channelCache, _ := CacheGetChannel(channelId)
if channelCache == nil {
return false
}
if channelCache.ChannelInfo.IsMultiKey {
// Use per-channel lock to prevent concurrent map read/write with GetNextEnabledKey
beforeStatus := channelCache.Status
pollingLock := GetChannelPollingLock(channelId)
pollingLock.Lock()
// 如果是多Key模式,更新缓存中的状态
handlerMultiKeyUpdate(channelCache, usingKey, status, reason)
pollingLock.Unlock()
if beforeStatus != channelCache.Status {
CacheUpdateChannelStatus(channelId, channelCache.Status)
}
@@ -759,11 +774,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri
if channel.ChannelInfo.IsMultiKey {
beforeStatus := channel.Status
// Protect map writes with the same per-channel lock used by readers
pollingLock := GetChannelPollingLock(channelId)
pollingLock.Lock()
handlerMultiKeyUpdate(channel, usingKey, status, reason)
pollingLock.Unlock()
if beforeStatus != channel.Status {
shouldUpdateAbilities = true
}
@@ -775,7 +786,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri
channel.Status = status
shouldUpdateAbilities = true
}
err = channel.SaveWithoutKey()
err = channel.saveStatusState()
if err != nil {
common.SysLog(fmt.Sprintf("failed to update channel status: channel_id=%d, status=%d, error=%v", channel.Id, status, err))
return false