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:
parent
befce8a55c
commit
f069f98b35
77
service/concurrency.go
Normal file
77
service/concurrency.go
Normal 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
|
||||
}
|
||||
59
service/concurrency_test.go
Normal file
59
service/concurrency_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user