-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat: 渠道级别限制每分钟最大请求数 #1711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: alpha
Are you sure you want to change the base?
feat: 渠道级别限制每分钟最大请求数 #1711
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ import ( | |||||||||||||||||||||||||||||||||
| "one-api/common" | ||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||
| "sync" | ||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| "github.com/samber/lo" | ||||||||||||||||||||||||||||||||||
| "gorm.io/gorm" | ||||||||||||||||||||||||||||||||||
|
|
@@ -102,6 +103,81 @@ func getChannelQuery(group string, model string, retry int) (*gorm.DB, error) { | |||||||||||||||||||||||||||||||||
| return channelQuery, nil | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| var channelRateLimitStatus sync.Map // 存储每个 Channel 的频率限制状态 | ||||||||||||||||||||||||||||||||||
| var rateLimitMutex sync.Mutex | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| type ChannelRateLimit struct { | ||||||||||||||||||||||||||||||||||
| Count int64 // 使用次数 | ||||||||||||||||||||||||||||||||||
| ResetTime time.Time // 上次重置时间 | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| type ChannelModelKey struct { | ||||||||||||||||||||||||||||||||||
| ChannelID int | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func isRateLimited(channel Channel, channelId int) bool { | ||||||||||||||||||||||||||||||||||
| if (channel.RateLimit != nil && *channel.RateLimit > 0) { | ||||||||||||||||||||||||||||||||||
| if _, ok := checkRateLimit(&channel, channelId); !ok { | ||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| updateRateLimitStatus(channelId) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func checkRateLimit(channel *Channel, channelId int) (*ChannelRateLimit, bool) { | ||||||||||||||||||||||||||||||||||
| now := time.Now() | ||||||||||||||||||||||||||||||||||
| key := ChannelModelKey{ChannelID: channelId} | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| rateLimitMutex.Lock() | ||||||||||||||||||||||||||||||||||
| defer rateLimitMutex.Unlock() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| value, exists := channelRateLimitStatus.Load(key) | ||||||||||||||||||||||||||||||||||
| if !exists { | ||||||||||||||||||||||||||||||||||
| value = &ChannelRateLimit{ | ||||||||||||||||||||||||||||||||||
| Count: 1, | ||||||||||||||||||||||||||||||||||
| ResetTime: now.Add(time.Minute), | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| channelRateLimitStatus.Store(key, value) | ||||||||||||||||||||||||||||||||||
| return value.(*ChannelRateLimit), true | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| rateLimit := value.(*ChannelRateLimit) | ||||||||||||||||||||||||||||||||||
| if now.After(rateLimit.ResetTime) { | ||||||||||||||||||||||||||||||||||
| rateLimit.Count = 1 | ||||||||||||||||||||||||||||||||||
| rateLimit.ResetTime = now.Add(time.Minute) | ||||||||||||||||||||||||||||||||||
| return rateLimit, true | ||||||||||||||||||||||||||||||||||
| } else if int64(*channel.RateLimit) > rateLimit.Count { | ||||||||||||||||||||||||||||||||||
| rateLimit.Count++ | ||||||||||||||||||||||||||||||||||
| return rateLimit, true | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return rateLimit, false | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func updateRateLimitStatus(channelId int) { | ||||||||||||||||||||||||||||||||||
| now := time.Now() | ||||||||||||||||||||||||||||||||||
| key := ChannelModelKey{ChannelID: channelId} | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| rateLimitMutex.Lock() | ||||||||||||||||||||||||||||||||||
| defer rateLimitMutex.Unlock() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| val, _ := channelRateLimitStatus.Load(key) | ||||||||||||||||||||||||||||||||||
| if val == nil { | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| rl := val.(*ChannelRateLimit) | ||||||||||||||||||||||||||||||||||
| if now.After(rl.ResetTime) { | ||||||||||||||||||||||||||||||||||
| rl.Count = 1 | ||||||||||||||||||||||||||||||||||
| rl.ResetTime = now.Add(time.Minute) | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| rl.Count++ | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| channelRateLimitStatus.Store(key, rl) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) { | ||||||||||||||||||||||||||||||||||
| var abilities []Ability | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
@@ -118,28 +194,62 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, | |||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if len(abilities) <= 0 { | ||||||||||||||||||||||||||||||||||
| return nil, errors.New("channel not found"); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| channel := Channel{} | ||||||||||||||||||||||||||||||||||
| if len(abilities) > 0 { | ||||||||||||||||||||||||||||||||||
| // Randomly choose one | ||||||||||||||||||||||||||||||||||
| weightSum := uint(0) | ||||||||||||||||||||||||||||||||||
| for _, ability_ := range abilities { | ||||||||||||||||||||||||||||||||||
| weightSum += ability_.Weight + 10 | ||||||||||||||||||||||||||||||||||
| for len(abilities) > 0 { | ||||||||||||||||||||||||||||||||||
| selectedIndex, err := getRandomWeightedIndex(abilities) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| // Randomly choose one | ||||||||||||||||||||||||||||||||||
| weight := common.GetRandomInt(int(weightSum)) | ||||||||||||||||||||||||||||||||||
| for _, ability_ := range abilities { | ||||||||||||||||||||||||||||||||||
| weight -= int(ability_.Weight) + 10 | ||||||||||||||||||||||||||||||||||
| //log.Printf("weight: %d, ability weight: %d", weight, *ability_.Weight) | ||||||||||||||||||||||||||||||||||
| if weight <= 0 { | ||||||||||||||||||||||||||||||||||
| channel.Id = ability_.ChannelId | ||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| selectedAbility := abilities[selectedIndex] | ||||||||||||||||||||||||||||||||||
| channelPtr, err := GetChannelById(selectedAbility.ChannelId, true) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| if err.Error() != "channel not found" { | ||||||||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| abilities = removeAbility(abilities, selectedIndex) | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+209
to
216
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use errors.Is for gorm.ErrRecordNotFound String compare on error is brittle; treat only “record not found” as skippable. - channelPtr, err := GetChannelById(selectedAbility.ChannelId, true)
- if err != nil {
- if err.Error() != "channel not found" {
- return nil, err
- }
- abilities = removeAbility(abilities, selectedIndex)
- continue
- }
+ channelPtr, err := GetChannelById(selectedAbility.ChannelId, true)
+ if err != nil {
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, err
+ }
+ abilities = removeAbility(abilities, selectedIndex)
+ continue
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| return nil, errors.New("channel not found") | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| channel = *channelPtr | ||||||||||||||||||||||||||||||||||
| if isRateLimited(channel, channel.Id) { | ||||||||||||||||||||||||||||||||||
| abilities = removeAbility(abilities, selectedIndex) | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return channelPtr, nil | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return nil, errors.New("channel not found") | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func getRandomWeightedIndex(abilities []Ability) (int, error) { | ||||||||||||||||||||||||||||||||||
| weightSum := uint(0) | ||||||||||||||||||||||||||||||||||
| for _, ability := range abilities { | ||||||||||||||||||||||||||||||||||
| weightSum += ability.Weight | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if weightSum == 0 { | ||||||||||||||||||||||||||||||||||
| return common.GetRandomInt(len(abilities)), nil | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| err = DB.First(&channel, "id = ?", channel.Id).Error | ||||||||||||||||||||||||||||||||||
| return &channel, err | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| randomWeight := common.GetRandomInt(int(weightSum)) | ||||||||||||||||||||||||||||||||||
| for i, ability := range abilities { | ||||||||||||||||||||||||||||||||||
| randomWeight -= int(ability.Weight) | ||||||||||||||||||||||||||||||||||
| if randomWeight <= 0 { | ||||||||||||||||||||||||||||||||||
| return i, nil | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return -1, errors.New("unable to select a random weighted index") | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func removeAbility(abilities []Ability, index int) []Ability { | ||||||||||||||||||||||||||||||||||
| return append(abilities[:index], abilities[index+1:]...) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func (channel *Channel) AddAbilities() error { | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ type Channel struct { | |
| Tag *string `json:"tag" gorm:"index"` | ||
| Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置 | ||
| ParamOverride *string `json:"param_override" gorm:"type:text"` | ||
| RateLimit *int `json:"rate_limit" gorm:"default:0"` | ||
| // add after v0.8.5 | ||
| ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"` | ||
| } | ||
|
Comment on lines
+47
to
50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add DB migration and constraints for rate_limit Schema change requires migration. Backfill nulls to 0 and add a non-negative constraint to prevent invalid values. Apply one of these migrations (adjust table name if needed):
ALTER TABLE channels ADD COLUMN rate_limit INT NOT NULL DEFAULT 0;
ALTER TABLE channels ADD CONSTRAINT chk_rate_limit_nonneg CHECK (rate_limit >= 0);
ALTER TABLE channels ADD COLUMN rate_limit INT NOT NULL DEFAULT 0;
ALTER TABLE channels ADD CONSTRAINT chk_rate_limit_nonneg CHECK (rate_limit >= 0);
ALTER TABLE channels ADD COLUMN rate_limit INTEGER DEFAULT 0;
UPDATE channels SET rate_limit = 0 WHERE rate_limit IS NULL;
-- (SQLite lacks ADD CONSTRAINT; enforce non-negativity in app code)Also consider returning 0 instead of null in read APIs to simplify clients. 🤖 Prompt for AI Agents |
||
|
|
@@ -397,6 +398,13 @@ func (channel *Channel) GetStatusCodeMapping() string { | |
| return *channel.StatusCodeMapping | ||
| } | ||
|
|
||
| func (channel *Channel) GetRateLimit() int { | ||
| if channel.RateLimit == nil || *channel.RateLimit <= 0 { | ||
| return 0 | ||
| } | ||
| return *channel.RateLimit | ||
| } | ||
|
|
||
| func (channel *Channel) Insert() error { | ||
| var err error | ||
| err = DB.Create(channel).Error | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,11 +7,13 @@ import ( | |
| "one-api/common" | ||
| "one-api/setting" | ||
| "sort" | ||
| "strconv" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/gin-gonic/gin" | ||
| "github.com/go-redis/redis/v8" | ||
| ) | ||
|
|
||
| var group2model2channels map[string]map[string][]int // enabled channel | ||
|
|
@@ -130,26 +132,30 @@ func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel, | |
|
|
||
| channelSyncLock.RLock() | ||
| defer channelSyncLock.RUnlock() | ||
| channels := group2model2channels[group][model] | ||
| channelIds := group2model2channels[group][model] | ||
|
|
||
| if len(channels) == 0 { | ||
| validChannels := make([]*Channel, 0) | ||
| for _, channelId := range channelIds { | ||
| if channel, ok := channelsIDM[channelId]; ok { | ||
| if !isRedisLimited(*channel, channelId) { | ||
| validChannels = append(validChannels, channel) | ||
| } | ||
| } else { | ||
| return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) | ||
| } | ||
| } | ||
|
|
||
| if len(validChannels) == 0 { | ||
| return nil, errors.New("channel not found") | ||
| } | ||
|
|
||
| if len(channels) == 1 { | ||
| if channel, ok := channelsIDM[channels[0]]; ok { | ||
| return channel, nil | ||
| } | ||
| return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channels[0]) | ||
| if len(validChannels) == 1 { | ||
| return validChannels[0], nil | ||
| } | ||
|
|
||
| uniquePriorities := make(map[int]bool) | ||
| for _, channelId := range channels { | ||
| if channel, ok := channelsIDM[channelId]; ok { | ||
| uniquePriorities[int(channel.GetPriority())] = true | ||
| } else { | ||
| return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) | ||
| } | ||
| for _, channel := range validChannels { | ||
| uniquePriorities[int(channel.GetPriority())] = true | ||
| } | ||
| var sortedUniquePriorities []int | ||
| for priority := range uniquePriorities { | ||
|
|
@@ -164,13 +170,9 @@ func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel, | |
|
|
||
| // get the priority for the given retry number | ||
| var targetChannels []*Channel | ||
| for _, channelId := range channels { | ||
| if channel, ok := channelsIDM[channelId]; ok { | ||
| if channel.GetPriority() == targetPriority { | ||
| targetChannels = append(targetChannels, channel) | ||
| } | ||
| } else { | ||
| return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) | ||
| for _, channel := range validChannels { | ||
| if channel.GetPriority() == targetPriority { | ||
| targetChannels = append(targetChannels, channel) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -195,6 +197,53 @@ func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel, | |
| return nil, errors.New("channel not found") | ||
| } | ||
|
|
||
| func isRedisLimited(channel Channel, channelId int) bool { | ||
| if channel.RateLimit != nil && *channel.RateLimit > 0 { | ||
| if !checkRedisLimit(channel, channelId) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func checkRedisLimit(channel Channel, channelId int) bool { | ||
| key := fmt.Sprintf("rate_limit:%d", channelId) | ||
|
|
||
| countStr, err := common.RedisGet(key) | ||
| if err == redis.Nil { | ||
| // Key doesn't exist, set it with expiration | ||
| err = common.RedisSet(key, "1", time.Minute) | ||
| if err != nil { | ||
| common.SysLog(fmt.Sprintf("Error setting rate limit: %v", err)) | ||
| return false | ||
| } | ||
| return true | ||
| } else if err != nil { | ||
| common.SysLog(fmt.Sprintf("Error checking rate limit: %v", err)) | ||
| return false | ||
| } | ||
|
|
||
| count, err := strconv.ParseInt(countStr, 10, 64) | ||
| if err != nil { | ||
| common.SysLog(fmt.Sprintf("Error parsing rate limit count: %v", err)) | ||
| return false | ||
| } | ||
|
|
||
| if count > int64(*channel.RateLimit) { | ||
| return false | ||
| } | ||
|
|
||
| // 增加计数 | ||
| newCount := strconv.FormatInt(count+1, 10) | ||
| err = common.RedisSet(key, newCount, time.Minute) | ||
| if err != nil { | ||
| common.SysLog(fmt.Sprintf("Error incrementing rate limit: %v", err)) | ||
| return false | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
Comment on lines
+200
to
+245
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Fix Redis rate limiter: use atomic INCR + one-time TTL set; correct boundary check Current GET/SET is racy and allows limit+1 per window; also resets TTL on every hit (sliding), diverging from in-memory (fixed). Use INCR and set EXPIRE only when creating the key; compare with <= limit. -func checkRedisLimit(channel Channel, channelId int) bool {
- key := fmt.Sprintf("rate_limit:%d", channelId)
-
- countStr, err := common.RedisGet(key)
- if err == redis.Nil {
- // Key doesn't exist, set it with expiration
- err = common.RedisSet(key, "1", time.Minute)
- if err != nil {
- common.SysLog(fmt.Sprintf("Error setting rate limit: %v", err))
- return false
- }
- return true
- } else if err != nil {
- common.SysLog(fmt.Sprintf("Error checking rate limit: %v", err))
- return false
- }
-
- count, err := strconv.ParseInt(countStr, 10, 64)
- if err != nil {
- common.SysLog(fmt.Sprintf("Error parsing rate limit count: %v", err))
- return false
- }
-
- if count > int64(*channel.RateLimit) {
- return false
- }
-
- // 增加计数
- newCount := strconv.FormatInt(count+1, 10)
- err = common.RedisSet(key, newCount, time.Minute)
- if err != nil {
- common.SysLog(fmt.Sprintf("Error incrementing rate limit: %v", err))
- return false
- }
-
- return true
-}
+func checkRedisLimit(channel Channel, channelId int) bool {
+ key := fmt.Sprintf("rate_limit:%d", channelId)
+ ctx := context.Background()
+
+ // Atomic increment
+ count, err := common.RDB.Incr(ctx, key).Result()
+ if err != nil {
+ common.SysLog(fmt.Sprintf("Error incrementing rate limit: %v", err))
+ return false
+ }
+ // First hit: set TTL once to create a fixed 1-minute window
+ if count == 1 {
+ if err := common.RDB.Expire(ctx, key, time.Minute).Err(); err != nil {
+ common.SysLog(fmt.Sprintf("Error setting rate limit TTL: %v", err))
+ return false
+ }
+ }
+ // Allow if within limit
+ return count <= int64(*channel.RateLimit)
+} |
||
|
|
||
| func CacheGetChannel(id int) (*Channel, error) { | ||
| if !common.MemoryCacheEnabled { | ||
| return GetChannelById(id, true) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Double-count bug: remove updateRateLimitStatus after successful check
checkRateLimit already increments Count. Calling updateRateLimitStatus increments again, reducing the effective limit.
func isRateLimited(channel Channel, channelId int) bool { if (channel.RateLimit != nil && *channel.RateLimit > 0) { if _, ok := checkRateLimit(&channel, channelId); !ok { return true } - updateRateLimitStatus(channelId) } return false }📝 Committable suggestion
🤖 Prompt for AI Agents