account_traffic_service_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "io"
  6. "net/http"
  7. "strings"
  8. "testing"
  9. "time"
  10. coderws "github.com/coder/websocket"
  11. "github.com/stretchr/testify/require"
  12. )
  13. type trafficCacheStub struct {
  14. AccountTrafficCache
  15. acquired bool
  16. calls, finishes, status int
  17. }
  18. func (s *trafficCacheStub) Acquire(context.Context, AccountTrafficPlan, string) (AccountTrafficAdmission, error) {
  19. s.calls++
  20. return AccountTrafficAdmission{Allowed: s.acquired, RetryAfter: time.Second, Reason: "local budget"}, nil
  21. }
  22. func (s *trafficCacheStub) Finish(_ context.Context, _ AccountTrafficPlan, _ string, status int, _ int64) error {
  23. s.finishes++
  24. s.status = status
  25. return nil
  26. }
  27. func TestAccountTrafficDisabledDoesNotTouchCache(t *testing.T) {
  28. a := &Account{ID: 42, Concurrency: 4}
  29. req, _ := http.NewRequest("POST", "http://localhost/responses", strings.NewReader("{}"))
  30. cache := &trafficCacheStub{}
  31. control := NewAccountTrafficService(cache)
  32. called := 0
  33. resp, err := control.DoHTTP(WithAccountTrafficRequest(req, a), func(r *http.Request) (*http.Response, error) {
  34. called++
  35. return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("ok"))}, nil
  36. })
  37. require.NoError(t, err)
  38. resp.Body.Close()
  39. require.Equal(t, 1, called)
  40. require.Zero(t, cache.calls)
  41. require.Zero(t, cache.finishes)
  42. }
  43. func TestAccountTrafficHoldsLeaseThroughStreamAndClassifiesTerminal429(t *testing.T) {
  44. a := &Account{ID: 42, Concurrency: 4, Extra: map[string]any{AccountTrafficPolicyKey: map[string]any{"adaptive_enabled": true}}}
  45. req, _ := http.NewRequest("POST", "http://localhost/responses", nil)
  46. cache := &trafficCacheStub{acquired: true}
  47. control := NewAccountTrafficService(cache)
  48. resp, err := control.DoHTTP(WithAccountTrafficRequest(req, a), func(r *http.Request) (*http.Response, error) {
  49. return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": []string{"text/event-stream"}}, Body: io.NopCloser(strings.NewReader("data: {\"type\":\"response.failed\",\"response\":{\"error\":{\"code\":\"rate_limit_exceeded\"}}}\n\n"))}, nil
  50. })
  51. require.NoError(t, err)
  52. require.Zero(t, cache.finishes, "response headers alone must not release concurrency")
  53. _, err = io.ReadAll(resp.Body)
  54. require.NoError(t, err)
  55. require.NoError(t, resp.Body.Close())
  56. require.Equal(t, 1, cache.finishes)
  57. require.Equal(t, 429, cache.status)
  58. }
  59. func TestAccountTrafficLocalDenialNeverCallsUpstream(t *testing.T) {
  60. a := &Account{ID: 42, Concurrency: 4, Extra: map[string]any{AccountTrafficPolicyKey: map[string]any{"strict_rpm_enabled": true}}}
  61. req, _ := http.NewRequest("POST", "http://localhost/responses", nil)
  62. cache := &trafficCacheStub{}
  63. control := NewAccountTrafficService(cache)
  64. _, err := control.DoHTTP(WithAccountTrafficRequest(req, a), func(*http.Request) (*http.Response, error) { t.Fatal("must not send"); return nil, nil })
  65. var local *AccountTrafficLimitError
  66. require.ErrorAs(t, err, &local)
  67. var failover *UpstreamFailoverError
  68. require.ErrorAs(t, err, &failover)
  69. require.False(t, failover.ShouldRetryNextAccount())
  70. require.False(t, failover.ShouldReportAccountScheduleFailure())
  71. require.Equal(t, GatewayFailureScopeRequest, failover.Scope)
  72. require.Zero(t, cache.finishes)
  73. _, err = (*AccountTrafficService)(nil).DoHTTP(WithAccountTrafficRequest(req, a), func(*http.Request) (*http.Response, error) {
  74. t.Fatal("missing limiter must not fail open")
  75. return nil, nil
  76. })
  77. require.True(t, errors.As(err, &local))
  78. require.Equal(t, 503, local.Status)
  79. }
  80. func TestAccountTrafficStreamCompletionCanRecoverWithoutReadingEOF(t *testing.T) {
  81. for _, kind := range []string{"response.completed", "message_stop", "response.incomplete"} {
  82. t.Run(kind, func(t *testing.T) {
  83. a := &Account{ID: 42, Concurrency: 8, Extra: map[string]any{AccountTrafficPolicyKey: map[string]any{"adaptive_enabled": true}}}
  84. req, _ := http.NewRequest("POST", "http://localhost/responses", nil)
  85. cache := &trafficCacheStub{acquired: true}
  86. control := NewAccountTrafficService(cache)
  87. resp, err := control.DoHTTP(WithAccountTrafficRequest(req, a), func(*http.Request) (*http.Response, error) {
  88. return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": []string{"text/event-stream"}}, Body: io.NopCloser(strings.NewReader("data: {\"type\":\"" + kind + "\"}\n\n"))}, nil
  89. })
  90. require.NoError(t, err)
  91. buffer := make([]byte, 1000)
  92. _, err = resp.Body.Read(buffer)
  93. require.NoError(t, err)
  94. require.NoError(t, resp.Body.Close())
  95. if kind == "response.incomplete" {
  96. require.Equal(t, 499, cache.status)
  97. } else {
  98. require.Equal(t, 200, cache.status)
  99. }
  100. require.Equal(t, 1, cache.finishes)
  101. })
  102. }
  103. }
  104. func TestAccountTrafficPolicyRejectsInvalidValues(t *testing.T) {
  105. for _, input := range []map[string]any{{"rpm": 0}, {"rpm": 1, "burst": 2}, {"adaptive_mode": "unknown"}, {"strict_rpm_enabled": "true"}, {"unexpected": 1}} {
  106. _, err := ParseAccountTrafficPolicy(map[string]any{AccountTrafficPolicyKey: input})
  107. require.Error(t, err)
  108. }
  109. a := &Account{ID: 42, Concurrency: 4, Extra: map[string]any{AccountTrafficPolicyKey: map[string]any{"adaptive_enabled": true}}}
  110. p, err := AccountTrafficPlanFor(a)
  111. require.NoError(t, err)
  112. require.Zero(t, p.Revision)
  113. }
  114. type trafficFrameStub struct {
  115. payload []byte
  116. writes int
  117. }
  118. func (f *trafficFrameStub) ReadFrame(context.Context) (coderws.MessageType, []byte, error) {
  119. return coderws.MessageText, f.payload, nil
  120. }
  121. func (f *trafficFrameStub) WriteFrame(_ context.Context, _ coderws.MessageType, _ []byte) error {
  122. f.writes++
  123. return nil
  124. }
  125. func (f *trafficFrameStub) Close() error { return nil }
  126. func TestAccountTrafficPassthroughCountsEachTurnAndIgnoresControlFrames(t *testing.T) {
  127. cache := &trafficCacheStub{acquired: true}
  128. inner := &trafficFrameStub{payload: []byte(`{"type":"response.completed"}`)}
  129. a := &Account{ID: 42, Concurrency: 8, Extra: map[string]any{AccountTrafficPolicyKey: map[string]any{"strict_rpm_enabled": true}}}
  130. plan, err := AccountTrafficPlanFor(a)
  131. require.NoError(t, err)
  132. frame := &accountTrafficFrameConn{inner: inner, control: NewAccountTrafficService(cache), plan: plan, ctx: context.Background()}
  133. for i := 0; i < 2; i++ {
  134. writeCtx, cancel := context.WithCancel(context.Background())
  135. require.NoError(t, frame.WriteFrame(writeCtx, coderws.MessageText, []byte(`{"type":"response.create"}`)))
  136. cancel()
  137. require.Equal(t, i+1, cache.calls)
  138. require.Equal(t, i, cache.finishes, "the write deadline is not the active-turn lifetime")
  139. require.NoError(t, frame.WriteFrame(context.Background(), coderws.MessageText, []byte(`{"type":"session.update"}`)))
  140. require.Equal(t, i+1, cache.calls)
  141. _, _, err := frame.ReadFrame(context.Background())
  142. require.NoError(t, err)
  143. require.Equal(t, i+1, cache.finishes)
  144. require.Equal(t, 200, cache.status)
  145. }
  146. require.NoError(t, frame.Close())
  147. require.Equal(t, 2, cache.finishes)
  148. cache.acquired = false
  149. require.Error(t, frame.WriteFrame(context.Background(), coderws.MessageText, []byte(`{"type":"response.create"}`)))
  150. require.Equal(t, 4, inner.writes)
  151. }
  152. func TestAccountTrafficObserveCacheOutageDoesNotRestrictRequests(t *testing.T) {
  153. a := &Account{ID: 42, Concurrency: 8, Extra: map[string]any{AccountTrafficPolicyKey: map[string]any{"adaptive_enabled": true, "adaptive_mode": "observe"}}}
  154. req, _ := http.NewRequest("POST", "http://localhost/responses", nil)
  155. called := false
  156. _, err := (*AccountTrafficService)(nil).DoHTTP(WithAccountTrafficRequest(req, a), func(*http.Request) (*http.Response, error) {
  157. called = true
  158. return &http.Response{StatusCode: 200, Body: http.NoBody}, nil
  159. })
  160. require.NoError(t, err)
  161. require.True(t, called)
  162. }
  163. func TestAccountTrafficLeaseLossOnlyCancelsEnforcedRequests(t *testing.T) {
  164. for _, mode := range []string{"observe", "automatic"} {
  165. t.Run(mode, func(t *testing.T) {
  166. cache := &trafficCacheStub{acquired: true}
  167. policy := DefaultAccountTrafficPolicy()
  168. policy.AdaptiveEnabled, policy.AdaptiveMode = true, mode
  169. ctx, permit, err := NewAccountTrafficService(cache).Begin(context.Background(), AccountTrafficPlan{AccountID: 42, Policy: policy})
  170. require.NoError(t, err)
  171. closed := false
  172. permit.loseLease([]func(){func() { closed = true }})
  173. require.Equal(t, mode == "automatic", closed)
  174. require.Equal(t, mode == "automatic", ctx.Err() != nil)
  175. require.Equal(t, 1, cache.finishes)
  176. permit.Finish(200)
  177. require.ErrorIs(t, ctx.Err(), context.Canceled)
  178. require.Equal(t, 1, cache.finishes, "lost telemetry must not count a later successful completion")
  179. })
  180. }
  181. }
  182. func TestAccountTrafficPassthroughCancellationReleasesTurn(t *testing.T) {
  183. for _, terminal := range []string{`{"type":"response.cancelled"}`, `{"type":"response.canceled"}`, `{"type":"response.done","response":{"status":"cancelled"}}`} {
  184. cache := &trafficCacheStub{acquired: true}
  185. inner := &trafficFrameStub{payload: []byte(terminal)}
  186. policy := DefaultAccountTrafficPolicy()
  187. policy.StrictRPMEnabled = true
  188. frame := &accountTrafficFrameConn{inner: inner, control: NewAccountTrafficService(cache), plan: AccountTrafficPlan{AccountID: 42, Policy: policy}, ctx: context.Background()}
  189. require.NoError(t, frame.WriteFrame(context.Background(), coderws.MessageText, []byte(`{"type":"response.create"}`)))
  190. _, _, err := frame.ReadFrame(context.Background())
  191. require.NoError(t, err)
  192. require.Equal(t, 499, cache.status)
  193. require.NoError(t, frame.WriteFrame(context.Background(), coderws.MessageText, []byte(`{"type":"response.create"}`)), "cancelled turn must not block the next request")
  194. require.NoError(t, frame.Close())
  195. }
  196. }
  197. func TestAccountTrafficIncompleteStreamIsNotRecoveryEvidence(t *testing.T) {
  198. for _, payload := range []string{`{"type":"response.cancelled"}`, `{"type":"response.done","response":{"status":"failed"}}`, `{"type":"error","error":{"code":"invalid_request"}}`} {
  199. cache := &trafficCacheStub{acquired: true}
  200. policy := DefaultAccountTrafficPolicy()
  201. policy.AdaptiveEnabled = true
  202. _, permit, err := NewAccountTrafficService(cache).Begin(context.Background(), AccountTrafficPlan{AccountID: 42, Policy: policy})
  203. require.NoError(t, err)
  204. body := &accountTrafficBody{ReadCloser: io.NopCloser(strings.NewReader("data: " + payload + "\n\ndata: [DONE]\n\n")), permit: permit, status: 200, sse: true}
  205. _, err = io.ReadAll(body)
  206. require.NoError(t, err)
  207. require.NoError(t, body.Close())
  208. require.Equal(t, 499, cache.status)
  209. }
  210. }