openai_proxy_stream_circuit_test.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. package service
  2. import (
  3. "context"
  4. "testing"
  5. "time"
  6. "github.com/stretchr/testify/require"
  7. )
  8. func TestOpenAIProxyStreamCircuitThresholdTTLAndSuccessReset(t *testing.T) {
  9. base := time.Unix(1_800_000_000, 0)
  10. circuit := newOpenAIProxyStreamCircuit(openAIProxyStreamCircuitSettings{
  11. failureThreshold: 2,
  12. failureWindow: time.Minute,
  13. quarantineTTL: 10 * time.Minute,
  14. maxEntries: 16,
  15. })
  16. tripped, _ := circuit.recordFailure(1, base)
  17. require.False(t, tripped)
  18. require.False(t, circuit.isBlocked(1, base))
  19. require.True(t, circuit.recordSuccess(1))
  20. tripped, _ = circuit.recordFailure(1, base.Add(10*time.Second))
  21. require.False(t, tripped, "success must clear the previous failure observation")
  22. tripped, until := circuit.recordFailure(1, base.Add(20*time.Second))
  23. require.True(t, tripped)
  24. require.Equal(t, base.Add(20*time.Second+10*time.Minute), until)
  25. require.True(t, circuit.isBlocked(1, until.Add(-time.Nanosecond)))
  26. require.False(t, circuit.isBlocked(1, until), "TTL expiry must re-admit the proxy")
  27. tripped, _ = circuit.recordFailure(2, base)
  28. require.False(t, tripped)
  29. tripped, _ = circuit.recordFailure(2, base.Add(2*time.Minute))
  30. require.False(t, tripped, "failures outside the window must not accumulate")
  31. }
  32. func TestOpenAIProxyStreamCircuitCollapsesBurstFailures(t *testing.T) {
  33. base := time.Unix(1_800_000_000, 0)
  34. circuit := newOpenAIProxyStreamCircuit(openAIProxyStreamCircuitSettings{
  35. failureThreshold: 2,
  36. failureWindow: time.Minute,
  37. quarantineTTL: 10 * time.Minute,
  38. collapseInterval: 3 * time.Second,
  39. maxEntries: 16,
  40. })
  41. // One HTTP/2 connection loss kills several multiplexed streams at once:
  42. // the near-simultaneous reports must count as a single failure event.
  43. tripped, _ := circuit.recordFailure(1, base)
  44. require.False(t, tripped)
  45. tripped, _ = circuit.recordFailure(1, base.Add(time.Second))
  46. require.False(t, tripped, "burst failures inside the collapse interval must merge")
  47. tripped, _ = circuit.recordFailure(1, base.Add(2*time.Second))
  48. require.False(t, tripped, "burst failures inside the collapse interval must merge")
  49. require.False(t, circuit.isBlocked(1, base.Add(2*time.Second)))
  50. // A second, distinct incident past the collapse interval still trips.
  51. tripped, _ = circuit.recordFailure(1, base.Add(5*time.Second))
  52. require.True(t, tripped)
  53. require.True(t, circuit.isBlocked(1, base.Add(5*time.Second)))
  54. }
  55. func TestOpenAIProxyStreamCircuitDisabled(t *testing.T) {
  56. base := time.Unix(1_800_000_000, 0)
  57. circuit := newOpenAIProxyStreamCircuit(openAIProxyStreamCircuitSettings{
  58. disabled: true,
  59. failureThreshold: 1,
  60. failureWindow: time.Minute,
  61. quarantineTTL: 10 * time.Minute,
  62. maxEntries: 16,
  63. })
  64. tripped, _ := circuit.recordFailure(1, base)
  65. require.False(t, tripped)
  66. require.False(t, circuit.isBlocked(1, base))
  67. require.Equal(t, 0, circuit.activeBlockCount(base))
  68. }
  69. func TestOpenAIProxyStreamCircuitActiveBlockCount(t *testing.T) {
  70. base := time.Unix(1_800_000_000, 0)
  71. circuit := newOpenAIProxyStreamCircuit(openAIProxyStreamCircuitSettings{
  72. failureThreshold: 1,
  73. failureWindow: time.Minute,
  74. quarantineTTL: 10 * time.Minute,
  75. maxEntries: 16,
  76. })
  77. require.Equal(t, 0, circuit.activeBlockCount(base))
  78. tripped, until := circuit.recordFailure(1, base)
  79. require.True(t, tripped)
  80. circuit.recordFailure(2, base) // second proxy also tripped (threshold 1)
  81. require.Equal(t, 2, circuit.activeBlockCount(base.Add(time.Second)))
  82. require.Equal(t, 0, circuit.activeBlockCount(until), "expired quarantines must not count")
  83. }
  84. func TestOpenAIProxyStreamQuarantineBypassContext(t *testing.T) {
  85. proxyID := int64(7)
  86. account := &Account{ID: 1, Platform: PlatformOpenAI, ProxyID: &proxyID}
  87. svc := &OpenAIGatewayService{}
  88. svc.openaiProxyStreamCircuit = newOpenAIProxyStreamCircuit(openAIProxyStreamCircuitSettings{
  89. failureThreshold: 1,
  90. failureWindow: time.Minute,
  91. quarantineTTL: 10 * time.Minute,
  92. maxEntries: 16,
  93. })
  94. svc.openaiProxyStreamCircuit.recordFailure(proxyID, time.Now())
  95. ctx := context.Background()
  96. require.True(t, svc.isOpenAIProxyStreamQuarantined(ctx, account))
  97. require.False(t, svc.isOpenAIProxyStreamQuarantined(withOpenAIProxyStreamQuarantineBypass(ctx), account))
  98. }
  99. func TestOpenAIProxyStreamCircuitBoundsEntries(t *testing.T) {
  100. base := time.Unix(1_800_000_000, 0)
  101. circuit := newOpenAIProxyStreamCircuit(openAIProxyStreamCircuitSettings{
  102. failureThreshold: 1,
  103. failureWindow: time.Minute,
  104. quarantineTTL: 10 * time.Minute,
  105. maxEntries: 2,
  106. })
  107. circuit.recordFailure(1, base)
  108. circuit.recordFailure(2, base.Add(time.Second))
  109. circuit.recordFailure(3, base.Add(2*time.Second))
  110. circuit.mu.Lock()
  111. defer circuit.mu.Unlock()
  112. require.Len(t, circuit.entries, 2)
  113. _, oldestRetained := circuit.entries[1]
  114. require.False(t, oldestRetained, "the oldest entry must be evicted at the bound")
  115. }