feat: add concurrency guard service

Add Redis-backed user and channel concurrency slots plus user RPM checks with fail-open behavior.
This commit is contained in:
zizi 2026-05-20 14:27:18 +08:00
parent befce8a55c
commit f069f98b35
2 changed files with 136 additions and 0 deletions

77
service/concurrency.go Normal file
View File

@ -0,0 +1,77 @@
package service
import (
"context"
"strconv"
"time"
"github.com/QuantumNous/new-api/common"
)
const (
userConcurrencyKeyPrefix = "user:concurrency:"
channelConcurrencyKeyPrefix = "channel:concurrency:"
userRPMKeyPrefix = "user:rpm:"
concurrencyTTL = 5 * time.Minute
rpmTTL = 2 * time.Minute
)
func AcquireUserConcurrency(userId, limit int) (bool, func()) {
return acquireConcurrency(userConcurrencyKeyPrefix+strconv.Itoa(userId), limit)
}
func AcquireChannelConcurrency(channelId, limit int) (bool, func()) {
return acquireConcurrency(channelConcurrencyKeyPrefix+strconv.Itoa(channelId), limit)
}
func CheckUserRPM(userId, limit int) bool {
if limit <= 0 || !redisAvailable() {
return true
}
key := userRPMKeyPrefix + strconv.Itoa(userId) + ":" + time.Now().Format("200601021504")
ctx := context.Background()
count, err := common.RDB.Incr(ctx, key).Result()
if err != nil {
return true
}
if err = common.RDB.Expire(ctx, key, rpmTTL).Err(); err != nil {
return true
}
return count <= int64(limit)
}
func acquireConcurrency(key string, limit int) (bool, func()) {
release := func() {}
if limit <= 0 || !redisAvailable() {
return true, release
}
ctx := context.Background()
count, err := common.RDB.Incr(ctx, key).Result()
if err != nil {
return true, release
}
release = func() {
if redisAvailable() {
_ = common.RDB.Decr(context.Background(), key).Err()
}
}
if err = common.RDB.Expire(ctx, key, concurrencyTTL).Err(); err != nil {
return true, release
}
if count > int64(limit) {
release()
return false, func() {}
}
return true, release
}
func redisAvailable() bool {
return common.RedisEnabled && common.RDB != nil
}

View File

@ -0,0 +1,59 @@
package service
import (
"strconv"
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/stretchr/testify/require"
)
func TestAcquireConcurrencyAllowsWhenLimitDisabled(t *testing.T) {
previousRedisEnabled := common.RedisEnabled
previousRDB := common.RDB
common.RedisEnabled = true
common.RDB = nil
t.Cleanup(func() {
common.RedisEnabled = previousRedisEnabled
common.RDB = previousRDB
})
allowed, release := AcquireUserConcurrency(1, 0)
require.True(t, allowed)
require.NotNil(t, release)
release()
allowed, release = AcquireChannelConcurrency(1, -1)
require.True(t, allowed)
require.NotNil(t, release)
release()
require.True(t, CheckUserRPM(1, 0))
}
func TestAcquireConcurrencyAllowsWhenRedisUnavailable(t *testing.T) {
for _, redisEnabled := range []bool{false, true} {
t.Run("redis_enabled_"+strconv.FormatBool(redisEnabled), func(t *testing.T) {
previousRedisEnabled := common.RedisEnabled
previousRDB := common.RDB
common.RedisEnabled = redisEnabled
common.RDB = nil
t.Cleanup(func() {
common.RedisEnabled = previousRedisEnabled
common.RDB = previousRDB
})
allowed, release := AcquireUserConcurrency(1, 1)
require.True(t, allowed)
require.NotNil(t, release)
release()
allowed, release = AcquireChannelConcurrency(1, 1)
require.True(t, allowed)
require.NotNil(t, release)
release()
require.True(t, CheckUserRPM(1, 1))
})
}
}