ops_capture_writer_nil_test.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. package handler
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "testing"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/gin-gonic/gin"
  9. "github.com/stretchr/testify/assert"
  10. "github.com/stretchr/testify/require"
  11. )
  12. type blockingOpsResponseWriter struct {
  13. gin.ResponseWriter
  14. writeStarted chan struct{}
  15. writeRelease chan struct{}
  16. }
  17. func (w *blockingOpsResponseWriter) WriteString(s string) (int, error) {
  18. close(w.writeStarted)
  19. <-w.writeRelease
  20. return w.ResponseWriter.WriteString(s)
  21. }
  22. type deterministicOpsCaptureWriterStatePool struct {
  23. states []*opsCaptureWriterState
  24. }
  25. func (p *deterministicOpsCaptureWriterStatePool) Get() any {
  26. if len(p.states) == 0 {
  27. return &opsCaptureWriterState{limit: opsCaptureWriterLimit}
  28. }
  29. last := len(p.states) - 1
  30. state := p.states[last]
  31. p.states = p.states[:last]
  32. return state
  33. }
  34. func (p *deterministicOpsCaptureWriterStatePool) Put(value any) {
  35. if state, ok := value.(*opsCaptureWriterState); ok && state != nil {
  36. p.states = append(p.states, state)
  37. }
  38. }
  39. func TestOpsCaptureWriter_NilInnerWriter_NoPanic(t *testing.T) {
  40. w := &opsCaptureWriter{}
  41. assert.NotPanics(t, func() {
  42. assert.Equal(t, 0, w.Status())
  43. })
  44. assert.NotPanics(t, func() {
  45. assert.Equal(t, -1, w.Size())
  46. })
  47. assert.NotPanics(t, func() {
  48. assert.False(t, w.Written())
  49. })
  50. assert.NotPanics(t, func() {
  51. n, err := w.Write([]byte("test"))
  52. assert.Equal(t, 0, n)
  53. assert.NoError(t, err)
  54. })
  55. assert.NotPanics(t, func() {
  56. n, err := w.WriteString("test")
  57. assert.Equal(t, 0, n)
  58. assert.NoError(t, err)
  59. })
  60. assert.NotPanics(t, func() {
  61. h := w.Header()
  62. assert.NotNil(t, h)
  63. })
  64. assert.NotPanics(t, func() {
  65. w.WriteHeader(200)
  66. })
  67. assert.NotPanics(t, func() {
  68. w.WriteHeaderNow()
  69. })
  70. assert.NotPanics(t, func() {
  71. w.Flush()
  72. })
  73. assert.NotPanics(t, func() {
  74. conn, rw, err := w.Hijack()
  75. assert.Nil(t, conn)
  76. assert.Nil(t, rw)
  77. assert.Error(t, err)
  78. })
  79. assert.NotPanics(t, func() {
  80. ch := w.CloseNotify()
  81. assert.NotNil(t, ch)
  82. })
  83. assert.NotPanics(t, func() {
  84. p := w.Pusher()
  85. assert.Nil(t, p)
  86. })
  87. }
  88. func TestOpsCaptureWriter_CompactKeepaliveRestoresOriginalWriter(t *testing.T) {
  89. gin.SetMode(gin.TestMode)
  90. router := gin.New()
  91. outerStatus := -1
  92. router.Use(func(c *gin.Context) {
  93. c.Next()
  94. outerStatus = c.Writer.Status()
  95. })
  96. router.Use(OpsErrorLoggerMiddleware(nil))
  97. router.GET("/compact", func(c *gin.Context) {
  98. service.MarkOpenAICompactClientStream(c)
  99. stop := service.StartOpenAICompactSSEKeepalive(c, time.Hour)
  100. defer stop()
  101. c.Status(http.StatusOK)
  102. })
  103. recorder := httptest.NewRecorder()
  104. request := httptest.NewRequest(http.MethodGet, "/compact", nil)
  105. require.NotPanics(t, func() {
  106. router.ServeHTTP(recorder, request)
  107. })
  108. require.Equal(t, http.StatusOK, outerStatus)
  109. require.Equal(t, http.StatusOK, recorder.Code)
  110. }
  111. func TestOpsCaptureWriter_StaleLeaseCannotReachReacquiredState(t *testing.T) {
  112. gin.SetMode(gin.TestMode)
  113. pool := &deterministicOpsCaptureWriterStatePool{}
  114. firstRecorder := httptest.NewRecorder()
  115. firstContext, _ := gin.CreateTestContext(firstRecorder)
  116. stale := acquireOpsCaptureWriterFromPool(pool, firstContext.Writer)
  117. releaseOpsCaptureWriter(stale)
  118. secondRecorder := httptest.NewRecorder()
  119. secondContext, _ := gin.CreateTestContext(secondRecorder)
  120. current := acquireOpsCaptureWriterFromPool(pool, secondContext.Writer)
  121. defer releaseOpsCaptureWriter(current)
  122. require.NotSame(t, stale, current)
  123. require.Same(t, stale.state, current.state)
  124. current.WriteHeader(http.StatusInternalServerError)
  125. _, err := current.WriteString("current")
  126. require.NoError(t, err)
  127. require.Equal(t, []byte("current"), current.capturedBytes())
  128. n, err := stale.WriteString("stale")
  129. require.NoError(t, err)
  130. require.Zero(t, n)
  131. require.Nil(t, stale.capturedBytes())
  132. require.Equal(t, []byte("current"), current.capturedBytes())
  133. require.NotContains(t, secondRecorder.Body.String(), "stale")
  134. // Releasing the stale handle must not return an active state to the pool.
  135. releaseOpsCaptureWriter(stale)
  136. thirdRecorder := httptest.NewRecorder()
  137. thirdContext, _ := gin.CreateTestContext(thirdRecorder)
  138. other := acquireOpsCaptureWriterFromPool(pool, thirdContext.Writer)
  139. defer releaseOpsCaptureWriter(other)
  140. require.NotSame(t, current.state, other.state)
  141. }
  142. func TestOpsCaptureWriter_ReleaseWaitsForDelegatedWriteWithoutHoldingStateMutex(t *testing.T) {
  143. gin.SetMode(gin.TestMode)
  144. pool := &deterministicOpsCaptureWriterStatePool{}
  145. recorder := httptest.NewRecorder()
  146. ctx, _ := gin.CreateTestContext(recorder)
  147. inner := &blockingOpsResponseWriter{
  148. ResponseWriter: ctx.Writer,
  149. writeStarted: make(chan struct{}),
  150. writeRelease: make(chan struct{}),
  151. }
  152. w := acquireOpsCaptureWriterFromPool(pool, inner)
  153. writeDone := make(chan struct{})
  154. go func() {
  155. defer close(writeDone)
  156. _, _ = w.WriteString("body")
  157. }()
  158. <-inner.writeStarted
  159. if !w.state.mu.TryLock() {
  160. t.Fatal("state mutex remained held across the delegated network write")
  161. }
  162. w.state.mu.Unlock()
  163. releaseDone := make(chan struct{})
  164. go func() {
  165. releaseOpsCaptureWriter(w)
  166. close(releaseDone)
  167. }()
  168. select {
  169. case <-releaseDone:
  170. t.Fatal("release returned while a delegated write was still active")
  171. case <-time.After(20 * time.Millisecond):
  172. }
  173. require.Empty(t, pool.states)
  174. close(inner.writeRelease)
  175. <-writeDone
  176. select {
  177. case <-releaseDone:
  178. case <-time.After(time.Second):
  179. t.Fatal("release did not finish after the delegated write returned")
  180. }
  181. require.Len(t, pool.states, 1)
  182. }