feat: sync deployed v1.0.0-rc.38-custom.42
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
1 parent
aa6c3f50f4
commit
ceddfd3d6b
828 files changed
+122253
-13702
No files matched your search
@@ -0,0 +1,28 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
)
|
||||
|
||||
// GetAdvancedCustomPreset returns fresh defaults.
|
||||
func GetAdvancedCustomPreset(channelType int) *dto.AdvancedCustomConfig {
|
||||
if channelType != constant.ChannelTypeVLLM && channelType != constant.ChannelTypeSGLang {
|
||||
return nil
|
||||
}
|
||||
config := &dto.AdvancedCustomConfig{}
|
||||
for _, path := range []string{"/v1/chat/completions", "/v1/completions", "/v1/responses", "/v1/embeddings", "/v1/messages", dto.AdvancedCustomModelListPath} {
|
||||
config.Routes = append(config.Routes, dto.AdvancedCustomRoute{
|
||||
IncomingPath: path, UpstreamPath: path, Converter: "none",
|
||||
Auth: &dto.AdvancedCustomRouteAuth{Type: dto.AdvancedCustomAuthTypeHeader, Name: "Authorization", Value: "Bearer {api_key}"},
|
||||
})
|
||||
}
|
||||
if channelType == constant.ChannelTypeSGLang {
|
||||
for _, path := range []string{"/v1/rerank", "/rerank"} {
|
||||
config.Routes = append(config.Routes, dto.AdvancedCustomRoute{
|
||||
IncomingPath: path, UpstreamPath: "/v1/rerank", Converter: dto.AdvancedCustomConverterSGLangRerank,
|
||||
})
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
+1
-1
@@ -75,7 +75,7 @@ func ChannelType2APIType(channelType int) (int, bool) {
|
||||
apiType = constant.APITypeReplicate
|
||||
case constant.ChannelTypeCodex:
|
||||
apiType = constant.APITypeCodex
|
||||
case constant.ChannelTypeAdvancedCustom:
|
||||
case constant.ChannelTypeAdvancedCustom, constant.ChannelTypeVLLM, constant.ChannelTypeSGLang:
|
||||
apiType = constant.APITypeAdvancedCustom
|
||||
case constant.ChannelTypeSub2API:
|
||||
apiType = constant.APITypeSub2API
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/abema/go-mp4"
|
||||
"github.com/go-audio/aiff"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
// 它不再依赖外部的 ffmpeg 或 ffprobe 程序。
|
||||
func GetAudioDuration(ctx context.Context, f io.ReadSeeker, ext string) (duration float64, err error) {
|
||||
SysLog(fmt.Sprintf("GetAudioDuration: ext=%s", ext))
|
||||
ext = strings.ToLower(ext)
|
||||
// 根据文件扩展名选择解析器
|
||||
switch ext {
|
||||
case ".mp3":
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// minimalWAV builds a valid 16-bit PCM mono WAV with the given number of sample frames.
|
||||
func minimalWAV(sampleRate uint32, frames int) []byte {
|
||||
const numChannels = 1
|
||||
const bitsPerSample = 16
|
||||
dataSize := frames * numChannels * (bitsPerSample / 8)
|
||||
buf := &bytes.Buffer{}
|
||||
buf.WriteString("RIFF")
|
||||
binary.Write(buf, binary.LittleEndian, uint32(36+dataSize))
|
||||
buf.WriteString("WAVE")
|
||||
buf.WriteString("fmt ")
|
||||
binary.Write(buf, binary.LittleEndian, uint32(16))
|
||||
binary.Write(buf, binary.LittleEndian, uint16(1)) // PCM
|
||||
binary.Write(buf, binary.LittleEndian, uint16(numChannels))
|
||||
binary.Write(buf, binary.LittleEndian, sampleRate)
|
||||
byteRate := sampleRate * numChannels * (bitsPerSample / 8)
|
||||
binary.Write(buf, binary.LittleEndian, byteRate)
|
||||
binary.Write(buf, binary.LittleEndian, uint16(numChannels*(bitsPerSample/8)))
|
||||
binary.Write(buf, binary.LittleEndian, uint16(bitsPerSample))
|
||||
buf.WriteString("data")
|
||||
binary.Write(buf, binary.LittleEndian, uint32(dataSize))
|
||||
buf.Write(make([]byte, dataSize))
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func TestGetAudioDurationCaseInsensitiveExt(t *testing.T) {
|
||||
wav := minimalWAV(8000, 8000) // 1 second
|
||||
|
||||
for _, ext := range []string{".wav", ".WAV", ".Wav"} {
|
||||
d, err := GetAudioDuration(context.Background(), bytes.NewReader(wav), ext)
|
||||
if err != nil {
|
||||
t.Fatalf("ext %q: unexpected error: %v", ext, err)
|
||||
}
|
||||
if d < 0.99 || d > 1.01 {
|
||||
t.Fatalf("ext %q: expected ~1s duration, got %v", ext, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAudioDurationUppercaseNotUnsupported(t *testing.T) {
|
||||
_, err := GetAudioDuration(context.Background(), bytes.NewReader([]byte("not audio")), ".MP3")
|
||||
if err != nil && strings.Contains(err.Error(), "unsupported audio format") {
|
||||
t.Fatalf("uppercase extension rejected as unsupported: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
|
||||
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI}
|
||||
case constant.ChannelTypeXai:
|
||||
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI, constant.EndpointTypeOpenAIResponse}
|
||||
case constant.ChannelTypeVLLM, constant.ChannelTypeSGLang:
|
||||
endpointTypes = GetAdvancedCustomPreset(channelType).SupportedEndpointTypesForModel(modelName)
|
||||
case constant.ChannelTypeSora:
|
||||
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIVideo}
|
||||
case constant.ChannelTypeSub2API, constant.ChannelTypeNewAPI:
|
||||
|
||||
+4
-2
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
type RawMessage = json.RawMessage
|
||||
|
||||
// hostJSONCodec is the single place where the host chooses its JSON engine.
|
||||
// Swap the implementation here (for example to sonic.ConfigStd) and every
|
||||
// common.* and kitutil.* JSON helper, including relaykit DTO (un)marshalling,
|
||||
@@ -73,11 +75,11 @@ func IndentJson(data []byte) ([]byte, error) {
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func GetJsonType(data json.RawMessage) string {
|
||||
func GetJsonType(data RawMessage) string {
|
||||
return kitutil.GetJsonType(data)
|
||||
}
|
||||
|
||||
// JsonRawMessageToString returns JSON strings as their decoded value and other JSON values as raw text.
|
||||
func JsonRawMessageToString(data json.RawMessage) string {
|
||||
func JsonRawMessageToString(data RawMessage) string {
|
||||
return kitutil.JsonRawMessageToString(data)
|
||||
}
|
||||
+23
-4
@@ -2,6 +2,9 @@ package common
|
||||
|
||||
import "strings"
|
||||
|
||||
// ZImagePromptExtendMultiplier is the legacy Ali image request surcharge.
|
||||
const ZImagePromptExtendMultiplier = 2
|
||||
|
||||
var (
|
||||
// OpenAIResponseOnlyModels is a list of models that are only available for OpenAI responses.
|
||||
OpenAIResponseOnlyModels = []string{
|
||||
@@ -12,7 +15,20 @@ var (
|
||||
ImageGenerationModels = []string{
|
||||
"dall-e-3",
|
||||
"dall-e-2",
|
||||
"gpt-image-1",
|
||||
"prefix:dall-e", // Deprecated upstream models; retained for compatible routes.
|
||||
"gpt-image-",
|
||||
"qwen-image",
|
||||
"z-image",
|
||||
"wan2.7-image-pro",
|
||||
"wan2.7-image",
|
||||
"wan2.6-image",
|
||||
"wan2.6-t2i",
|
||||
"wan2.5-t2i-preview",
|
||||
"wan2.2-t2i-flash",
|
||||
"wan2.2-t2i-plus",
|
||||
"wanx2.1-t2i-turbo",
|
||||
"wanx2.1-t2i-plus",
|
||||
"wanx2.0-t2i-turbo",
|
||||
"prefix:imagen-",
|
||||
"flux-",
|
||||
"flux.1-",
|
||||
@@ -38,10 +54,13 @@ func IsOpenAIResponseOnlyModel(modelName string) bool {
|
||||
func IsImageGenerationModel(modelName string) bool {
|
||||
modelName = strings.ToLower(modelName)
|
||||
for _, m := range ImageGenerationModels {
|
||||
if strings.Contains(modelName, m) {
|
||||
return true
|
||||
if prefix, ok := strings.CutPrefix(m, "prefix:"); ok {
|
||||
if strings.HasPrefix(modelName, prefix) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(m, "prefix:") && strings.HasPrefix(modelName, strings.TrimPrefix(m, "prefix:")) {
|
||||
if strings.Contains(modelName, m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package common_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWanEndpointsDistinguishImagesFromVideos(t *testing.T) {
|
||||
for _, name := range []string{
|
||||
"wan2.7-image-pro", "wan2.7-image", "wan2.6-image", "wan2.6-t2i",
|
||||
"wan2.5-t2i-preview", "wan2.2-t2i-flash", "wan2.2-t2i-plus",
|
||||
"wanx2.1-t2i-turbo", "wanx2.1-t2i-plus", "wanx2.0-t2i-turbo",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Contains(t, common.GetEndpointTypesByChannelType(constant.ChannelTypeAli, name), constant.EndpointTypeImageGeneration)
|
||||
})
|
||||
}
|
||||
for _, name := range []string{
|
||||
"wanx2.1-t2v-plus", "wanx2.1-t2v-turbo", "wanx2.1-i2v-plus", "wanx2.1-i2v-turbo",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.NotContains(t, common.GetEndpointTypesByChannelType(constant.ChannelTypeAli, name), constant.EndpointTypeImageGeneration)
|
||||
})
|
||||
}
|
||||
}
|
||||
+186
-45
@@ -1,70 +1,211 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// rateLimitRequest stores one accepted request in a key's sliding window.
|
||||
type rateLimitRequest struct {
|
||||
next *rateLimitRequest
|
||||
timestamp int64
|
||||
}
|
||||
|
||||
// rateLimitQueue keeps accepted requests ordered from oldest to newest.
|
||||
type rateLimitQueue struct {
|
||||
head *rateLimitRequest
|
||||
tail *rateLimitRequest
|
||||
length int
|
||||
}
|
||||
|
||||
// append adds an accepted request without preallocating for the configured limit.
|
||||
func (q *rateLimitQueue) append(timestamp int64) {
|
||||
request := &rateLimitRequest{timestamp: timestamp}
|
||||
if q.tail == nil {
|
||||
q.head = request
|
||||
q.tail = request
|
||||
} else {
|
||||
q.tail.next = request
|
||||
q.tail = request
|
||||
}
|
||||
q.length++
|
||||
}
|
||||
|
||||
// removeExpired releases every expired request at the front of the queue.
|
||||
func (q *rateLimitQueue) removeExpired(now int64, duration int64) {
|
||||
if q.head == nil || now-q.head.timestamp < duration {
|
||||
return
|
||||
}
|
||||
|
||||
// Requests are time ordered, so an expired tail means the whole queue expired.
|
||||
if now-q.tail.timestamp >= duration {
|
||||
q.clear()
|
||||
return
|
||||
}
|
||||
|
||||
for now-q.head.timestamp >= duration {
|
||||
expired := q.head
|
||||
q.head = expired.next
|
||||
expired.next = nil
|
||||
q.length--
|
||||
}
|
||||
}
|
||||
|
||||
// clear releases the whole request chain when its key expires.
|
||||
func (q *rateLimitQueue) clear() {
|
||||
q.head = nil
|
||||
q.tail = nil
|
||||
q.length = 0
|
||||
}
|
||||
|
||||
// rateLimitEntry is both a rate-limit bucket and a node in the key-level LRU.
|
||||
type rateLimitEntry struct {
|
||||
lastActive time.Time
|
||||
element *list.Element
|
||||
requests rateLimitQueue
|
||||
key string
|
||||
}
|
||||
|
||||
// InMemoryRateLimiter implements a sliding-window limiter with idle-key eviction.
|
||||
// Reservations are tracked apart from the LRU so an in-flight request survives
|
||||
// its key being evicted while it is still running.
|
||||
type InMemoryRateLimiter struct {
|
||||
store map[string]*[]int64
|
||||
store map[string]*rateLimitEntry
|
||||
lru *list.List
|
||||
reservations map[string]int
|
||||
mutex sync.Mutex
|
||||
expirationDuration time.Duration
|
||||
}
|
||||
|
||||
// Init initializes the limiter once. Repeated calls leave the first configuration unchanged.
|
||||
func (l *InMemoryRateLimiter) Init(expirationDuration time.Duration) {
|
||||
if l.store == nil {
|
||||
l.mutex.Lock()
|
||||
if l.store == nil {
|
||||
l.store = make(map[string]*[]int64)
|
||||
l.expirationDuration = expirationDuration
|
||||
if expirationDuration > 0 {
|
||||
go l.clearExpiredItems()
|
||||
}
|
||||
}
|
||||
l.mutex.Unlock()
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.store != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.store = make(map[string]*rateLimitEntry)
|
||||
l.lru = list.New()
|
||||
l.reservations = make(map[string]int)
|
||||
l.expirationDuration = expirationDuration
|
||||
if expirationDuration > 0 {
|
||||
go l.clearExpiredItems(time.NewTicker(expirationDuration).C)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *InMemoryRateLimiter) clearExpiredItems() {
|
||||
for {
|
||||
time.Sleep(l.expirationDuration)
|
||||
l.mutex.Lock()
|
||||
now := time.Now().Unix()
|
||||
for key := range l.store {
|
||||
queue := l.store[key]
|
||||
size := len(*queue)
|
||||
if size == 0 || now-(*queue)[size-1] > int64(l.expirationDuration.Seconds()) {
|
||||
delete(l.store, key)
|
||||
}
|
||||
}
|
||||
l.mutex.Unlock()
|
||||
// clearExpiredItems periodically removes expired entries from the LRU tail.
|
||||
func (l *InMemoryRateLimiter) clearExpiredItems(ticks <-chan time.Time) {
|
||||
for now := range ticks {
|
||||
l.deleteExpiredEntries(now)
|
||||
}
|
||||
}
|
||||
|
||||
// deleteExpiredEntries walks only the oldest LRU entries and stops at the first active key.
|
||||
func (l *InMemoryRateLimiter) deleteExpiredEntries(now time.Time) {
|
||||
if l.expirationDuration <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
for {
|
||||
oldest := l.lru.Back()
|
||||
if oldest == nil {
|
||||
return
|
||||
}
|
||||
|
||||
entry := oldest.Value.(*rateLimitEntry)
|
||||
if now.Sub(entry.lastActive) < l.expirationDuration {
|
||||
return
|
||||
}
|
||||
|
||||
delete(l.store, entry.key)
|
||||
l.lru.Remove(oldest)
|
||||
entry.element = nil
|
||||
entry.requests.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// touchEntry returns the key's bucket, creating it on demand, and marks it as
|
||||
// the most recently active key. The caller must hold the mutex.
|
||||
func (l *InMemoryRateLimiter) touchEntry(key string, now time.Time) *rateLimitEntry {
|
||||
entry, ok := l.store[key]
|
||||
if !ok {
|
||||
entry = &rateLimitEntry{
|
||||
key: key,
|
||||
lastActive: now,
|
||||
}
|
||||
entry.element = l.lru.PushFront(entry)
|
||||
l.store[key] = entry
|
||||
return entry
|
||||
}
|
||||
entry.lastActive = now
|
||||
l.lru.MoveToFront(entry.element)
|
||||
return entry
|
||||
}
|
||||
|
||||
// Request parameter duration's unit is seconds
|
||||
func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration int64) bool {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
// [old <-- new]
|
||||
queue, ok := l.store[key]
|
||||
now := time.Now().Unix()
|
||||
if ok {
|
||||
if len(*queue) < maxRequestNum {
|
||||
*queue = append(*queue, now)
|
||||
return true
|
||||
} else {
|
||||
if now-(*queue)[0] >= duration {
|
||||
*queue = (*queue)[1:]
|
||||
*queue = append(*queue, now)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s := make([]int64, 0, maxRequestNum)
|
||||
l.store[key] = &s
|
||||
*(l.store[key]) = append(*(l.store[key]), now)
|
||||
|
||||
now := time.Now()
|
||||
entry := l.touchEntry(key, now)
|
||||
entry.requests.removeExpired(now.Unix(), duration)
|
||||
|
||||
allowed := entry.requests.length < maxRequestNum
|
||||
if allowed {
|
||||
entry.requests.append(now.Unix())
|
||||
}
|
||||
return true
|
||||
|
||||
return allowed
|
||||
}
|
||||
|
||||
// RateLimitReservation counts an active request towards admission until its
|
||||
// outcome is known. Failed requests release the slot without consuming quota.
|
||||
type RateLimitReservation struct {
|
||||
limiter *InMemoryRateLimiter
|
||||
key string
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// Reserve admits a request when the key's accepted requests plus its in-flight
|
||||
// reservations stay below maxRequests. It returns nil when the key is saturated.
|
||||
func (l *InMemoryRateLimiter) Reserve(key string, maxRequests int, duration int64) *RateLimitReservation {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
entry := l.touchEntry(key, now)
|
||||
entry.requests.removeExpired(now.Unix(), duration)
|
||||
if entry.requests.length+l.reservations[key] >= maxRequests {
|
||||
return nil
|
||||
}
|
||||
l.reservations[key]++
|
||||
return &RateLimitReservation{limiter: l, key: key}
|
||||
}
|
||||
|
||||
// Complete releases the reservation. A successful outcome records the request
|
||||
// in the key's sliding window; a failed one only frees the slot.
|
||||
func (r *RateLimitReservation) Complete(success bool) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.once.Do(func() {
|
||||
l := r.limiter
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
l.reservations[r.key]--
|
||||
if l.reservations[r.key] == 0 {
|
||||
delete(l.reservations, r.key)
|
||||
}
|
||||
if success {
|
||||
now := time.Now()
|
||||
l.touchEntry(r.key, now).requests.append(now.Unix())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInMemoryRateLimiterAllocatesTimestampsOnDemand(t *testing.T) {
|
||||
var limiter InMemoryRateLimiter
|
||||
limiter.Init(0)
|
||||
|
||||
require.True(t, limiter.Request("client", 1_000_000, 60))
|
||||
|
||||
entry := limiter.store["client"]
|
||||
require.NotNil(t, entry)
|
||||
assert.Equal(t, 1, entry.requests.length)
|
||||
assert.Same(t, entry.requests.head, entry.requests.tail)
|
||||
assert.Nil(t, entry.requests.head.next)
|
||||
|
||||
limiter.deleteExpiredEntries(time.Now().Add(time.Hour))
|
||||
assert.Contains(t, limiter.store, "client")
|
||||
}
|
||||
|
||||
func TestInMemoryRateLimiterEnforcesWindowBoundary(t *testing.T) {
|
||||
var queue rateLimitQueue
|
||||
queue.append(100)
|
||||
queue.append(100)
|
||||
|
||||
queue.removeExpired(159, 60)
|
||||
assert.Equal(t, 2, queue.length)
|
||||
|
||||
queue.removeExpired(160, 60)
|
||||
assert.Zero(t, queue.length)
|
||||
assert.Nil(t, queue.head)
|
||||
assert.Nil(t, queue.tail)
|
||||
}
|
||||
|
||||
func TestInMemoryRateLimiterRemovesAllExpiredRequestsPerCall(t *testing.T) {
|
||||
var queue rateLimitQueue
|
||||
for range 5 {
|
||||
queue.append(100)
|
||||
}
|
||||
queue.append(110)
|
||||
|
||||
queue.removeExpired(110, 10)
|
||||
assert.Equal(t, 1, queue.length)
|
||||
require.NotNil(t, queue.head)
|
||||
assert.EqualValues(t, 110, queue.head.timestamp)
|
||||
assert.Same(t, queue.head, queue.tail)
|
||||
}
|
||||
|
||||
func TestInMemoryRateLimiterKeepsNonExpiredRequests(t *testing.T) {
|
||||
var queue rateLimitQueue
|
||||
for range 3 {
|
||||
queue.append(100)
|
||||
}
|
||||
|
||||
queue.removeExpired(109, 10)
|
||||
assert.Equal(t, 3, queue.length)
|
||||
assert.NotNil(t, queue.head)
|
||||
assert.NotNil(t, queue.tail)
|
||||
}
|
||||
|
||||
func TestInMemoryRateLimiterEvictsOnlyExpiredLRUTail(t *testing.T) {
|
||||
var limiter InMemoryRateLimiter
|
||||
limiter.Init(0)
|
||||
limiter.expirationDuration = 10 * time.Second
|
||||
|
||||
require.True(t, limiter.Request("active", 1, 100))
|
||||
require.True(t, limiter.Request("idle", 1, 100))
|
||||
assert.False(t, limiter.Request("active", 1, 100))
|
||||
|
||||
assert.Equal(t, "active", limiter.lru.Front().Value.(*rateLimitEntry).key)
|
||||
assert.Equal(t, "idle", limiter.lru.Back().Value.(*rateLimitEntry).key)
|
||||
|
||||
idleEntry := limiter.store["idle"]
|
||||
now := time.Now()
|
||||
idleEntry.lastActive = now.Add(-10 * time.Second)
|
||||
limiter.store["active"].lastActive = now
|
||||
limiter.deleteExpiredEntries(now)
|
||||
|
||||
assert.NotContains(t, limiter.store, "idle")
|
||||
assert.Contains(t, limiter.store, "active")
|
||||
assert.Equal(t, 1, limiter.lru.Len())
|
||||
assert.Nil(t, idleEntry.element)
|
||||
assert.Nil(t, idleEntry.requests.head)
|
||||
assert.Nil(t, idleEntry.requests.tail)
|
||||
assert.Zero(t, idleEntry.requests.length)
|
||||
}
|
||||
|
||||
func TestInMemoryRateLimiterCleanupTicksEvictExpiredKeys(t *testing.T) {
|
||||
var limiter InMemoryRateLimiter
|
||||
limiter.Init(0)
|
||||
limiter.expirationDuration = 10 * time.Second
|
||||
require.True(t, limiter.Request("idle", 1, 100))
|
||||
|
||||
now := time.Now()
|
||||
limiter.store["idle"].lastActive = now.Add(-10 * time.Second)
|
||||
ticks := make(chan time.Time, 1)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
limiter.clearExpiredItems(ticks)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
ticks <- now
|
||||
close(ticks)
|
||||
<-done
|
||||
|
||||
assert.Empty(t, limiter.store)
|
||||
assert.Zero(t, limiter.lru.Len())
|
||||
}
|
||||
|
||||
func TestInMemoryRateLimiterInitKeepsFirstConfiguration(t *testing.T) {
|
||||
var limiter InMemoryRateLimiter
|
||||
limiter.Init(time.Hour)
|
||||
limiter.Init(0)
|
||||
|
||||
assert.Equal(t, time.Hour, limiter.expirationDuration)
|
||||
require.True(t, limiter.Request("client", 1, 60))
|
||||
limiter.deleteExpiredEntries(time.Now().Add(time.Hour))
|
||||
assert.NotContains(t, limiter.store, "client")
|
||||
}
|
||||
|
||||
func TestInMemoryRateLimiterConcurrentInitializationAndRequests(t *testing.T) {
|
||||
var limiter InMemoryRateLimiter
|
||||
var allowed atomic.Int64
|
||||
var waitGroup sync.WaitGroup
|
||||
|
||||
for range 100 {
|
||||
waitGroup.Add(1)
|
||||
go func() {
|
||||
defer waitGroup.Done()
|
||||
limiter.Init(0)
|
||||
if limiter.Request("client", 10, 60) {
|
||||
allowed.Add(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
waitGroup.Wait()
|
||||
assert.EqualValues(t, 10, allowed.Load())
|
||||
assert.Len(t, limiter.store, 1)
|
||||
assert.Equal(t, 10, limiter.store["client"].requests.length)
|
||||
}
|
||||
Reference in new issue
Block a user