openai_ws_state_store_test.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "testing"
  7. "time"
  8. "github.com/stretchr/testify/require"
  9. )
  10. func TestOpenAIWSStateStore_BindGetDeleteResponseAccount(t *testing.T) {
  11. cache := &stubGatewayCache{}
  12. store := NewOpenAIWSStateStore(cache)
  13. ctx := context.Background()
  14. groupID := int64(7)
  15. require.NoError(t, store.BindResponseAccount(ctx, groupID, "resp_abc", 101, time.Minute))
  16. accountID, err := store.GetResponseAccount(ctx, groupID, "resp_abc")
  17. require.NoError(t, err)
  18. require.Equal(t, int64(101), accountID)
  19. require.NoError(t, store.DeleteResponseAccount(ctx, groupID, "resp_abc"))
  20. accountID, err = store.GetResponseAccount(ctx, groupID, "resp_abc")
  21. require.NoError(t, err)
  22. require.Zero(t, accountID)
  23. }
  24. func TestOpenAIWSStateStore_HTTPResponseOwnerPersistsAcrossStoreInstances(t *testing.T) {
  25. cache := &stubGatewayCache{}
  26. ctx := context.Background()
  27. groupID := int64(8)
  28. writer := NewOpenAIWSStateStore(cache)
  29. require.NoError(t, writer.BindHTTPResponseOwner(ctx, groupID, "resp_owned", 201, 301, time.Minute))
  30. userID, apiKeyID, found, err := writer.GetHTTPResponseOwner(ctx, groupID, "resp_owned")
  31. require.NoError(t, err)
  32. require.True(t, found)
  33. require.Equal(t, int64(201), userID)
  34. require.Equal(t, int64(301), apiKeyID)
  35. reader := NewOpenAIWSStateStore(cache)
  36. userID, apiKeyID, found, err = reader.GetHTTPResponseOwner(ctx, groupID, "resp_owned")
  37. require.NoError(t, err)
  38. require.True(t, found)
  39. require.Equal(t, int64(201), userID)
  40. require.Equal(t, int64(301), apiKeyID)
  41. }
  42. func TestOpenAIWSStateStore_ResponseConnTTL(t *testing.T) {
  43. store := NewOpenAIWSStateStore(nil)
  44. store.BindResponseConn("resp_conn", "conn_1", 30*time.Millisecond)
  45. connID, ok := store.GetResponseConn("resp_conn")
  46. require.True(t, ok)
  47. require.Equal(t, "conn_1", connID)
  48. time.Sleep(60 * time.Millisecond)
  49. _, ok = store.GetResponseConn("resp_conn")
  50. require.False(t, ok)
  51. }
  52. func TestOpenAIWSStateStore_SessionTurnStateTTL(t *testing.T) {
  53. store := NewOpenAIWSStateStore(nil)
  54. store.BindSessionTurnState(9, "session_hash_1", "turn_state_1", 30*time.Millisecond)
  55. state, ok := store.GetSessionTurnState(9, "session_hash_1")
  56. require.True(t, ok)
  57. require.Equal(t, "turn_state_1", state)
  58. // group 隔离
  59. _, ok = store.GetSessionTurnState(10, "session_hash_1")
  60. require.False(t, ok)
  61. time.Sleep(60 * time.Millisecond)
  62. _, ok = store.GetSessionTurnState(9, "session_hash_1")
  63. require.False(t, ok)
  64. }
  65. func TestOpenAIWSStateStore_SessionConnTTL(t *testing.T) {
  66. store := NewOpenAIWSStateStore(nil)
  67. store.BindSessionConn(9, "session_hash_conn_1", "conn_1", 30*time.Millisecond)
  68. connID, ok := store.GetSessionConn(9, "session_hash_conn_1")
  69. require.True(t, ok)
  70. require.Equal(t, "conn_1", connID)
  71. // group 隔离
  72. _, ok = store.GetSessionConn(10, "session_hash_conn_1")
  73. require.False(t, ok)
  74. time.Sleep(60 * time.Millisecond)
  75. _, ok = store.GetSessionConn(9, "session_hash_conn_1")
  76. require.False(t, ok)
  77. }
  78. func TestOpenAIWSStateStore_GetResponseAccount_NoStaleAfterCacheMiss(t *testing.T) {
  79. cache := &stubGatewayCache{sessionBindings: map[string]int64{}}
  80. store := NewOpenAIWSStateStore(cache)
  81. ctx := context.Background()
  82. groupID := int64(17)
  83. responseID := "resp_cache_stale"
  84. cacheKey := openAIWSResponseAccountCacheKey(responseID)
  85. cache.sessionBindings[cacheKey] = 501
  86. accountID, err := store.GetResponseAccount(ctx, groupID, responseID)
  87. require.NoError(t, err)
  88. require.Equal(t, int64(501), accountID)
  89. delete(cache.sessionBindings, cacheKey)
  90. accountID, err = store.GetResponseAccount(ctx, groupID, responseID)
  91. require.NoError(t, err)
  92. require.Zero(t, accountID, "上游缓存失效后不应继续命中本地陈旧映射")
  93. }
  94. func TestOpenAIWSStateStore_MaybeCleanupRemovesExpiredIncrementally(t *testing.T) {
  95. raw := NewOpenAIWSStateStore(nil)
  96. store, ok := raw.(*defaultOpenAIWSStateStore)
  97. require.True(t, ok)
  98. expiredAt := time.Now().Add(-time.Minute)
  99. total := 2048
  100. store.responseToConnMu.Lock()
  101. for i := 0; i < total; i++ {
  102. store.responseToConn[fmt.Sprintf("resp_%d", i)] = openAIWSConnBinding{
  103. connID: "conn_incremental",
  104. expiresAt: expiredAt,
  105. }
  106. }
  107. store.responseToConnMu.Unlock()
  108. store.lastCleanupUnixNano.Store(time.Now().Add(-2 * openAIWSStateStoreCleanupInterval).UnixNano())
  109. store.maybeCleanup()
  110. store.responseToConnMu.RLock()
  111. remainingAfterFirst := len(store.responseToConn)
  112. store.responseToConnMu.RUnlock()
  113. require.Less(t, remainingAfterFirst, total, "单轮 cleanup 应至少有进展")
  114. require.Greater(t, remainingAfterFirst, 0, "增量清理不要求单轮清空全部键")
  115. for i := 0; i < 8; i++ {
  116. store.lastCleanupUnixNano.Store(time.Now().Add(-2 * openAIWSStateStoreCleanupInterval).UnixNano())
  117. store.maybeCleanup()
  118. }
  119. store.responseToConnMu.RLock()
  120. remaining := len(store.responseToConn)
  121. store.responseToConnMu.RUnlock()
  122. require.Zero(t, remaining, "多轮 cleanup 后应逐步清空全部过期键")
  123. }
  124. func TestEnsureBindingCapacity_EvictsOneWhenMapIsFull(t *testing.T) {
  125. bindings := map[string]int{
  126. "a": 1,
  127. "b": 2,
  128. }
  129. ensureBindingCapacity(bindings, "c", 2)
  130. bindings["c"] = 3
  131. require.Len(t, bindings, 2)
  132. require.Equal(t, 3, bindings["c"])
  133. }
  134. func TestEnsureBindingCapacity_DoesNotEvictWhenUpdatingExistingKey(t *testing.T) {
  135. bindings := map[string]int{
  136. "a": 1,
  137. "b": 2,
  138. }
  139. ensureBindingCapacity(bindings, "a", 2)
  140. bindings["a"] = 9
  141. require.Len(t, bindings, 2)
  142. require.Equal(t, 9, bindings["a"])
  143. }
  144. type openAIWSStateStoreTimeoutProbeCache struct {
  145. setHasDeadline bool
  146. getHasDeadline bool
  147. deleteHasDeadline bool
  148. setDeadlineDelta time.Duration
  149. getDeadlineDelta time.Duration
  150. delDeadlineDelta time.Duration
  151. }
  152. func (c *openAIWSStateStoreTimeoutProbeCache) GetSessionAccountID(ctx context.Context, _ int64, _ string) (int64, error) {
  153. if deadline, ok := ctx.Deadline(); ok {
  154. c.getHasDeadline = true
  155. c.getDeadlineDelta = time.Until(deadline)
  156. }
  157. return 123, nil
  158. }
  159. func (c *openAIWSStateStoreTimeoutProbeCache) SetSessionAccountID(ctx context.Context, _ int64, _ string, _ int64, _ time.Duration) error {
  160. if deadline, ok := ctx.Deadline(); ok {
  161. c.setHasDeadline = true
  162. c.setDeadlineDelta = time.Until(deadline)
  163. }
  164. return errors.New("set failed")
  165. }
  166. func (c *openAIWSStateStoreTimeoutProbeCache) RefreshSessionTTL(context.Context, int64, string, time.Duration) error {
  167. return nil
  168. }
  169. func (c *openAIWSStateStoreTimeoutProbeCache) DeleteSessionAccountID(ctx context.Context, _ int64, _ string) error {
  170. if deadline, ok := ctx.Deadline(); ok {
  171. c.deleteHasDeadline = true
  172. c.delDeadlineDelta = time.Until(deadline)
  173. }
  174. return nil
  175. }
  176. func (c *openAIWSStateStoreTimeoutProbeCache) SetGrokVideoPendingBilling(_ context.Context, _ string, _ []byte, _ time.Duration) error {
  177. return nil
  178. }
  179. func (c *openAIWSStateStoreTimeoutProbeCache) GetGrokVideoPendingBilling(_ context.Context, _ string) ([]byte, error) {
  180. return nil, nil
  181. }
  182. func (c *openAIWSStateStoreTimeoutProbeCache) ClaimGrokVideoBilled(_ context.Context, _ string, _ time.Duration) (bool, error) {
  183. return true, nil
  184. }
  185. func (c *openAIWSStateStoreTimeoutProbeCache) ReleaseGrokVideoBilled(_ context.Context, _ string) error {
  186. return nil
  187. }
  188. func (c *openAIWSStateStoreTimeoutProbeCache) SetReasoningContent(_ context.Context, _ string, _ string, _ time.Duration) error {
  189. return nil
  190. }
  191. func (c *openAIWSStateStoreTimeoutProbeCache) GetReasoningContent(_ context.Context, _ string) (string, error) {
  192. return "", ErrReasoningContentNotFound
  193. }
  194. func TestOpenAIWSStateStore_RedisOpsUseShortTimeout(t *testing.T) {
  195. probe := &openAIWSStateStoreTimeoutProbeCache{}
  196. store := NewOpenAIWSStateStore(probe)
  197. ctx := context.Background()
  198. groupID := int64(5)
  199. err := store.BindResponseAccount(ctx, groupID, "resp_timeout_probe", 11, time.Minute)
  200. require.Error(t, err)
  201. accountID, getErr := store.GetResponseAccount(ctx, groupID, "resp_timeout_probe")
  202. require.NoError(t, getErr)
  203. require.Equal(t, int64(11), accountID, "本地缓存命中应优先返回已绑定账号")
  204. require.NoError(t, store.DeleteResponseAccount(ctx, groupID, "resp_timeout_probe"))
  205. require.True(t, probe.setHasDeadline, "SetSessionAccountID 应携带独立超时上下文")
  206. require.True(t, probe.deleteHasDeadline, "DeleteSessionAccountID 应携带独立超时上下文")
  207. require.False(t, probe.getHasDeadline, "GetSessionAccountID 本用例应由本地缓存命中,不触发 Redis 读取")
  208. require.Greater(t, probe.setDeadlineDelta, 2*time.Second)
  209. require.LessOrEqual(t, probe.setDeadlineDelta, 3*time.Second)
  210. require.Greater(t, probe.delDeadlineDelta, 2*time.Second)
  211. require.LessOrEqual(t, probe.delDeadlineDelta, 3*time.Second)
  212. probe2 := &openAIWSStateStoreTimeoutProbeCache{}
  213. store2 := NewOpenAIWSStateStore(probe2)
  214. accountID2, err2 := store2.GetResponseAccount(ctx, groupID, "resp_cache_only")
  215. require.NoError(t, err2)
  216. require.Equal(t, int64(123), accountID2)
  217. require.True(t, probe2.getHasDeadline, "GetSessionAccountID 在缓存未命中时应携带独立超时上下文")
  218. require.Greater(t, probe2.getDeadlineDelta, 2*time.Second)
  219. require.LessOrEqual(t, probe2.getDeadlineDelta, 3*time.Second)
  220. }
  221. func TestWithOpenAIWSStateStoreRedisTimeout_WithParentContext(t *testing.T) {
  222. ctx, cancel := withOpenAIWSStateStoreRedisTimeout(context.Background())
  223. defer cancel()
  224. require.NotNil(t, ctx)
  225. _, ok := ctx.Deadline()
  226. require.True(t, ok, "应附加短超时")
  227. }