| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- package service
- import (
- "context"
- "errors"
- "fmt"
- "testing"
- "time"
- "github.com/stretchr/testify/require"
- )
- func TestOpenAIWSStateStore_BindGetDeleteResponseAccount(t *testing.T) {
- cache := &stubGatewayCache{}
- store := NewOpenAIWSStateStore(cache)
- ctx := context.Background()
- groupID := int64(7)
- require.NoError(t, store.BindResponseAccount(ctx, groupID, "resp_abc", 101, time.Minute))
- accountID, err := store.GetResponseAccount(ctx, groupID, "resp_abc")
- require.NoError(t, err)
- require.Equal(t, int64(101), accountID)
- require.NoError(t, store.DeleteResponseAccount(ctx, groupID, "resp_abc"))
- accountID, err = store.GetResponseAccount(ctx, groupID, "resp_abc")
- require.NoError(t, err)
- require.Zero(t, accountID)
- }
- func TestOpenAIWSStateStore_HTTPResponseOwnerPersistsAcrossStoreInstances(t *testing.T) {
- cache := &stubGatewayCache{}
- ctx := context.Background()
- groupID := int64(8)
- writer := NewOpenAIWSStateStore(cache)
- require.NoError(t, writer.BindHTTPResponseOwner(ctx, groupID, "resp_owned", 201, 301, time.Minute))
- userID, apiKeyID, found, err := writer.GetHTTPResponseOwner(ctx, groupID, "resp_owned")
- require.NoError(t, err)
- require.True(t, found)
- require.Equal(t, int64(201), userID)
- require.Equal(t, int64(301), apiKeyID)
- reader := NewOpenAIWSStateStore(cache)
- userID, apiKeyID, found, err = reader.GetHTTPResponseOwner(ctx, groupID, "resp_owned")
- require.NoError(t, err)
- require.True(t, found)
- require.Equal(t, int64(201), userID)
- require.Equal(t, int64(301), apiKeyID)
- }
- func TestOpenAIWSStateStore_ResponseConnTTL(t *testing.T) {
- store := NewOpenAIWSStateStore(nil)
- store.BindResponseConn("resp_conn", "conn_1", 30*time.Millisecond)
- connID, ok := store.GetResponseConn("resp_conn")
- require.True(t, ok)
- require.Equal(t, "conn_1", connID)
- time.Sleep(60 * time.Millisecond)
- _, ok = store.GetResponseConn("resp_conn")
- require.False(t, ok)
- }
- func TestOpenAIWSStateStore_SessionTurnStateTTL(t *testing.T) {
- store := NewOpenAIWSStateStore(nil)
- store.BindSessionTurnState(9, "session_hash_1", "turn_state_1", 30*time.Millisecond)
- state, ok := store.GetSessionTurnState(9, "session_hash_1")
- require.True(t, ok)
- require.Equal(t, "turn_state_1", state)
- // group 隔离
- _, ok = store.GetSessionTurnState(10, "session_hash_1")
- require.False(t, ok)
- time.Sleep(60 * time.Millisecond)
- _, ok = store.GetSessionTurnState(9, "session_hash_1")
- require.False(t, ok)
- }
- func TestOpenAIWSStateStore_SessionConnTTL(t *testing.T) {
- store := NewOpenAIWSStateStore(nil)
- store.BindSessionConn(9, "session_hash_conn_1", "conn_1", 30*time.Millisecond)
- connID, ok := store.GetSessionConn(9, "session_hash_conn_1")
- require.True(t, ok)
- require.Equal(t, "conn_1", connID)
- // group 隔离
- _, ok = store.GetSessionConn(10, "session_hash_conn_1")
- require.False(t, ok)
- time.Sleep(60 * time.Millisecond)
- _, ok = store.GetSessionConn(9, "session_hash_conn_1")
- require.False(t, ok)
- }
- func TestOpenAIWSStateStore_GetResponseAccount_NoStaleAfterCacheMiss(t *testing.T) {
- cache := &stubGatewayCache{sessionBindings: map[string]int64{}}
- store := NewOpenAIWSStateStore(cache)
- ctx := context.Background()
- groupID := int64(17)
- responseID := "resp_cache_stale"
- cacheKey := openAIWSResponseAccountCacheKey(responseID)
- cache.sessionBindings[cacheKey] = 501
- accountID, err := store.GetResponseAccount(ctx, groupID, responseID)
- require.NoError(t, err)
- require.Equal(t, int64(501), accountID)
- delete(cache.sessionBindings, cacheKey)
- accountID, err = store.GetResponseAccount(ctx, groupID, responseID)
- require.NoError(t, err)
- require.Zero(t, accountID, "上游缓存失效后不应继续命中本地陈旧映射")
- }
- func TestOpenAIWSStateStore_MaybeCleanupRemovesExpiredIncrementally(t *testing.T) {
- raw := NewOpenAIWSStateStore(nil)
- store, ok := raw.(*defaultOpenAIWSStateStore)
- require.True(t, ok)
- expiredAt := time.Now().Add(-time.Minute)
- total := 2048
- store.responseToConnMu.Lock()
- for i := 0; i < total; i++ {
- store.responseToConn[fmt.Sprintf("resp_%d", i)] = openAIWSConnBinding{
- connID: "conn_incremental",
- expiresAt: expiredAt,
- }
- }
- store.responseToConnMu.Unlock()
- store.lastCleanupUnixNano.Store(time.Now().Add(-2 * openAIWSStateStoreCleanupInterval).UnixNano())
- store.maybeCleanup()
- store.responseToConnMu.RLock()
- remainingAfterFirst := len(store.responseToConn)
- store.responseToConnMu.RUnlock()
- require.Less(t, remainingAfterFirst, total, "单轮 cleanup 应至少有进展")
- require.Greater(t, remainingAfterFirst, 0, "增量清理不要求单轮清空全部键")
- for i := 0; i < 8; i++ {
- store.lastCleanupUnixNano.Store(time.Now().Add(-2 * openAIWSStateStoreCleanupInterval).UnixNano())
- store.maybeCleanup()
- }
- store.responseToConnMu.RLock()
- remaining := len(store.responseToConn)
- store.responseToConnMu.RUnlock()
- require.Zero(t, remaining, "多轮 cleanup 后应逐步清空全部过期键")
- }
- func TestEnsureBindingCapacity_EvictsOneWhenMapIsFull(t *testing.T) {
- bindings := map[string]int{
- "a": 1,
- "b": 2,
- }
- ensureBindingCapacity(bindings, "c", 2)
- bindings["c"] = 3
- require.Len(t, bindings, 2)
- require.Equal(t, 3, bindings["c"])
- }
- func TestEnsureBindingCapacity_DoesNotEvictWhenUpdatingExistingKey(t *testing.T) {
- bindings := map[string]int{
- "a": 1,
- "b": 2,
- }
- ensureBindingCapacity(bindings, "a", 2)
- bindings["a"] = 9
- require.Len(t, bindings, 2)
- require.Equal(t, 9, bindings["a"])
- }
- type openAIWSStateStoreTimeoutProbeCache struct {
- setHasDeadline bool
- getHasDeadline bool
- deleteHasDeadline bool
- setDeadlineDelta time.Duration
- getDeadlineDelta time.Duration
- delDeadlineDelta time.Duration
- }
- func (c *openAIWSStateStoreTimeoutProbeCache) GetSessionAccountID(ctx context.Context, _ int64, _ string) (int64, error) {
- if deadline, ok := ctx.Deadline(); ok {
- c.getHasDeadline = true
- c.getDeadlineDelta = time.Until(deadline)
- }
- return 123, nil
- }
- func (c *openAIWSStateStoreTimeoutProbeCache) SetSessionAccountID(ctx context.Context, _ int64, _ string, _ int64, _ time.Duration) error {
- if deadline, ok := ctx.Deadline(); ok {
- c.setHasDeadline = true
- c.setDeadlineDelta = time.Until(deadline)
- }
- return errors.New("set failed")
- }
- func (c *openAIWSStateStoreTimeoutProbeCache) RefreshSessionTTL(context.Context, int64, string, time.Duration) error {
- return nil
- }
- func (c *openAIWSStateStoreTimeoutProbeCache) DeleteSessionAccountID(ctx context.Context, _ int64, _ string) error {
- if deadline, ok := ctx.Deadline(); ok {
- c.deleteHasDeadline = true
- c.delDeadlineDelta = time.Until(deadline)
- }
- return nil
- }
- func (c *openAIWSStateStoreTimeoutProbeCache) SetGrokVideoPendingBilling(_ context.Context, _ string, _ []byte, _ time.Duration) error {
- return nil
- }
- func (c *openAIWSStateStoreTimeoutProbeCache) GetGrokVideoPendingBilling(_ context.Context, _ string) ([]byte, error) {
- return nil, nil
- }
- func (c *openAIWSStateStoreTimeoutProbeCache) ClaimGrokVideoBilled(_ context.Context, _ string, _ time.Duration) (bool, error) {
- return true, nil
- }
- func (c *openAIWSStateStoreTimeoutProbeCache) ReleaseGrokVideoBilled(_ context.Context, _ string) error {
- return nil
- }
- func (c *openAIWSStateStoreTimeoutProbeCache) SetReasoningContent(_ context.Context, _ string, _ string, _ time.Duration) error {
- return nil
- }
- func (c *openAIWSStateStoreTimeoutProbeCache) GetReasoningContent(_ context.Context, _ string) (string, error) {
- return "", ErrReasoningContentNotFound
- }
- func TestOpenAIWSStateStore_RedisOpsUseShortTimeout(t *testing.T) {
- probe := &openAIWSStateStoreTimeoutProbeCache{}
- store := NewOpenAIWSStateStore(probe)
- ctx := context.Background()
- groupID := int64(5)
- err := store.BindResponseAccount(ctx, groupID, "resp_timeout_probe", 11, time.Minute)
- require.Error(t, err)
- accountID, getErr := store.GetResponseAccount(ctx, groupID, "resp_timeout_probe")
- require.NoError(t, getErr)
- require.Equal(t, int64(11), accountID, "本地缓存命中应优先返回已绑定账号")
- require.NoError(t, store.DeleteResponseAccount(ctx, groupID, "resp_timeout_probe"))
- require.True(t, probe.setHasDeadline, "SetSessionAccountID 应携带独立超时上下文")
- require.True(t, probe.deleteHasDeadline, "DeleteSessionAccountID 应携带独立超时上下文")
- require.False(t, probe.getHasDeadline, "GetSessionAccountID 本用例应由本地缓存命中,不触发 Redis 读取")
- require.Greater(t, probe.setDeadlineDelta, 2*time.Second)
- require.LessOrEqual(t, probe.setDeadlineDelta, 3*time.Second)
- require.Greater(t, probe.delDeadlineDelta, 2*time.Second)
- require.LessOrEqual(t, probe.delDeadlineDelta, 3*time.Second)
- probe2 := &openAIWSStateStoreTimeoutProbeCache{}
- store2 := NewOpenAIWSStateStore(probe2)
- accountID2, err2 := store2.GetResponseAccount(ctx, groupID, "resp_cache_only")
- require.NoError(t, err2)
- require.Equal(t, int64(123), accountID2)
- require.True(t, probe2.getHasDeadline, "GetSessionAccountID 在缓存未命中时应携带独立超时上下文")
- require.Greater(t, probe2.getDeadlineDelta, 2*time.Second)
- require.LessOrEqual(t, probe2.getDeadlineDelta, 3*time.Second)
- }
- func TestWithOpenAIWSStateStoreRedisTimeout_WithParentContext(t *testing.T) {
- ctx, cancel := withOpenAIWSStateStoreRedisTimeout(context.Background())
- defer cancel()
- require.NotNil(t, ctx)
- _, ok := ctx.Deadline()
- require.True(t, ok, "应附加短超时")
- }
|