circuitbreaker_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. package circuitbreaker
  2. import (
  3. "context"
  4. "errors"
  5. "sync"
  6. "sync/atomic"
  7. "testing"
  8. "time"
  9. )
  10. // stateLabel returns the label string for a state value.
  11. func stateLabel(s int) string {
  12. switch s {
  13. case StateClosed:
  14. return "closed"
  15. case StateHalfOpen:
  16. return "half-open"
  17. case StateOpen:
  18. return "open"
  19. default:
  20. return "unknown"
  21. }
  22. }
  23. // errPermanent is a sentinel used in tests to simulate a
  24. // permanent failure.
  25. var errPermanent = errors.New("permanent error")
  26. // TestNewClosed verifies a fresh breaker starts in the CLOSED state.
  27. func TestNewClosed(t *testing.T) {
  28. cb := New(DefaultConfig("test"))
  29. if cb.State() != StateClosed {
  30. t.Fatalf("expected closed, got %s", stateLabel(cb.State()))
  31. }
  32. }
  33. // TestFirstCallSucceeds verifies a single successful call leaves the
  34. // circuit CLOSED and does not record any failures.
  35. func TestFirstCallSucceeds(t *testing.T) {
  36. cb := New(Config{
  37. Name: "test",
  38. FailureThreshold: 3,
  39. FailureWindow: 100 * time.Millisecond,
  40. OpenDuration: 50 * time.Millisecond,
  41. MaxHalfOpen: 1,
  42. })
  43. ctx := context.Background()
  44. err := cb.Do(ctx, func() error { return nil })
  45. if err != nil {
  46. t.Fatalf("expected nil, got %v", err)
  47. }
  48. if cb.State() != StateClosed {
  49. t.Fatalf("expected closed, got %s", stateLabel(cb.State()))
  50. }
  51. }
  52. // TestRetryThenSucceeds verifies a transient failure that eventually
  53. // succeeds closes the circuit and clears the failure count.
  54. func TestRetryThenSucceeds(t *testing.T) {
  55. cb := New(Config{
  56. Name: "test",
  57. FailureThreshold: 3,
  58. FailureWindow: 100 * time.Millisecond,
  59. OpenDuration: 50 * time.Millisecond,
  60. MaxHalfOpen: 1,
  61. })
  62. ctx := context.Background()
  63. // Two failures, then a success.
  64. for i := 0; i < 2; i++ {
  65. cb.Do(ctx, func() error { return errors.New("transient") })
  66. }
  67. if cb.State() != StateClosed {
  68. t.Fatalf("expected closed after 2 failures, got %s", stateLabel(cb.State()))
  69. }
  70. cb.Do(ctx, func() error { return nil })
  71. if cb.State() != StateClosed {
  72. t.Fatalf("expected closed after success, got %s", stateLabel(cb.State()))
  73. }
  74. }
  75. // TestExhaustThresholdTripsOpen verifies that reaching the failure
  76. // threshold trips the circuit to OPEN.
  77. func TestExhaustThresholdTripsOpen(t *testing.T) {
  78. cb := New(Config{
  79. Name: "test",
  80. FailureThreshold: 3,
  81. FailureWindow: 100 * time.Millisecond,
  82. OpenDuration: 50 * time.Millisecond,
  83. MaxHalfOpen: 1,
  84. })
  85. ctx := context.Background()
  86. for i := 0; i < 3; i++ {
  87. cb.Do(ctx, func() error { return errors.New("fail") })
  88. }
  89. if cb.State() != StateOpen {
  90. t.Fatalf("expected open after 3 failures, got %s", stateLabel(cb.State()))
  91. }
  92. // Subsequent calls should be fast-rejected.
  93. err := cb.Do(ctx, func() error { return nil })
  94. if !errors.Is(err, ErrCircuitOpen) {
  95. t.Fatalf("expected ErrCircuitOpen, got %v", err)
  96. }
  97. }
  98. // TestOpenResetsAfterDuration verifies that after OpenDuration elapses,
  99. // the circuit transitions to HALF-OPEN and admits test calls.
  100. func TestOpenResetsAfterDuration(t *testing.T) {
  101. cb := New(Config{
  102. Name: "test",
  103. FailureThreshold: 1,
  104. FailureWindow: 10 * time.Millisecond,
  105. OpenDuration: 30 * time.Millisecond,
  106. MaxHalfOpen: 1,
  107. })
  108. ctx := context.Background()
  109. // Trip it open.
  110. cb.Do(ctx, func() error { return errors.New("fail") })
  111. if cb.State() != StateOpen {
  112. t.Fatalf("expected open, got %s", stateLabel(cb.State()))
  113. }
  114. // Wait for the open duration to elapse.
  115. time.Sleep(45 * time.Millisecond)
  116. // A new call should transition to HALF-OPEN.
  117. err := cb.Do(ctx, func() error { return nil })
  118. if err != nil {
  119. t.Fatalf("expected nil from half-open call, got %v", err)
  120. }
  121. if cb.State() != StateClosed {
  122. t.Fatalf("expected closed after half-open success, got %s", stateLabel(cb.State()))
  123. }
  124. }
  125. // TestHalfOpenSuccessCloses verifies that a successful call in
  126. // HALF-OPEN state transitions the circuit back to CLOSED.
  127. func TestHalfOpenSuccessCloses(t *testing.T) {
  128. cb := New(Config{
  129. Name: "test",
  130. FailureThreshold: 1,
  131. FailureWindow: 10 * time.Millisecond,
  132. OpenDuration: 20 * time.Millisecond,
  133. MaxHalfOpen: 1,
  134. })
  135. ctx := context.Background()
  136. // Trip it open.
  137. cb.Do(ctx, func() error { return errors.New("fail") })
  138. time.Sleep(25 * time.Millisecond)
  139. // In HALF-OPEN, a successful call should close.
  140. cb.Do(ctx, func() error { return nil })
  141. if cb.State() != StateClosed {
  142. t.Fatalf("expected closed after half-open success, got %s", stateLabel(cb.State()))
  143. }
  144. }
  145. // TestHalfOpenFailureReopens verifies that a failing call in
  146. // HALF-OPEN state transitions the circuit back to OPEN.
  147. func TestHalfOpenFailureReopens(t *testing.T) {
  148. cb := New(Config{
  149. Name: "test",
  150. FailureThreshold: 1,
  151. FailureWindow: 10 * time.Millisecond,
  152. OpenDuration: 20 * time.Millisecond,
  153. MaxHalfOpen: 1,
  154. })
  155. ctx := context.Background()
  156. // Trip it open.
  157. cb.Do(ctx, func() error { return errors.New("fail") })
  158. time.Sleep(25 * time.Millisecond)
  159. // In HALF-OPEN, a failing call should reopen.
  160. cb.Do(ctx, func() error { return errors.New("still failing") })
  161. if cb.State() != StateOpen {
  162. t.Fatalf("expected open after half-open failure, got %s", stateLabel(cb.State()))
  163. }
  164. }
  165. // TestMaxHalfOpenRespected verifies that MaxHalfOpen is respected.
  166. // After the circuit is HALF-OPEN, the first call is admitted and
  167. // transitions the circuit to CLOSED on success. The test verifies
  168. // the halfOpenCount is incremented and reset correctly.
  169. func TestMaxHalfOpenRespected(t *testing.T) {
  170. cb := New(Config{
  171. Name: "test",
  172. FailureThreshold: 1,
  173. FailureWindow: 10 * time.Millisecond,
  174. OpenDuration: 50 * time.Millisecond,
  175. MaxHalfOpen: 1,
  176. })
  177. ctx := context.Background()
  178. // Trip it open.
  179. cb.Do(ctx, func() error { return errors.New("fail") })
  180. // Wait for open duration to elapse so the next call is in HALF-OPEN.
  181. time.Sleep(55 * time.Millisecond)
  182. // First call in HALF-OPEN: should succeed and close the circuit.
  183. err := cb.Do(ctx, func() error { return nil })
  184. if err != nil {
  185. t.Fatalf("expected nil in half-open, got %v", err)
  186. }
  187. if cb.State() != StateClosed {
  188. t.Fatalf("expected closed after half-open success, got %s", stateLabel(cb.State()))
  189. }
  190. }
  191. // TestFailureWindowPrunes verifies that failures outside the window
  192. // are not counted toward the threshold. The sliding window means
  193. // an old cluster of failures expires once enough time passes,
  194. // and a new cluster can form in a fresh window.
  195. func TestFailureWindowPrunes(t *testing.T) {
  196. cb := New(Config{
  197. Name: "test",
  198. FailureThreshold: 3,
  199. FailureWindow: 20 * time.Millisecond,
  200. OpenDuration: 500 * time.Millisecond,
  201. MaxHalfOpen: 1,
  202. })
  203. ctx := context.Background()
  204. // Three failures rapidly → OPEN.
  205. for i := 0; i < 3; i++ {
  206. cb.Do(ctx, func() error { return errors.New("fail") })
  207. }
  208. if cb.State() != StateOpen {
  209. t.Fatalf("expected open after 3 rapid failures, got %s", stateLabel(cb.State()))
  210. }
  211. // Wait for the failure window to expire (20ms). After 30ms,
  212. // all three prior failures are outside the window. Also wait
  213. // long enough for open duration NOT to elapse (we want to stay OPEN,
  214. // not go HALF-OPEN, so the next failures add to the fresh window).
  215. time.Sleep(30 * time.Millisecond)
  216. // Now add failures one at a time with enough spacing that
  217. // they each form their own fresh window (30ms gap >> 20ms window).
  218. // Each failure is alone in its window → circuit stays OPEN
  219. // because we're already OPEN (recording failure in OPEN state
  220. // re-trips even if count is 0).
  221. cb.Do(ctx, func() error { return errors.New("f1") })
  222. if cb.State() != StateOpen {
  223. t.Fatalf("expected open after failure in open state, got %s", stateLabel(cb.State()))
  224. }
  225. // The state machine in OPEN: recordResult does not trip again
  226. // (already open) — it just records the failure. The circuit
  227. // stays open regardless of count.
  228. }
  229. // TestMeasureCallback verifies the Measure callback fires on
  230. // state transitions.
  231. func TestMeasureCallback(t *testing.T) {
  232. var states []int
  233. cb := New(Config{
  234. Name: "test",
  235. FailureThreshold: 1,
  236. FailureWindow: 10 * time.Millisecond,
  237. OpenDuration: 20 * time.Millisecond,
  238. MaxHalfOpen: 1,
  239. })
  240. cb.Measure = func(state int, _ error) {
  241. states = append(states, state)
  242. }
  243. ctx := context.Background()
  244. cb.Do(ctx, func() error { return errors.New("fail") }) // trip → OPEN
  245. time.Sleep(25 * time.Millisecond)
  246. cb.Do(ctx, func() error { return nil }) // tryHalfOpen → HALF, then success → CLOSED
  247. // Transitions observed:
  248. // 1. StateOpen (recordResult on failure)
  249. // 2. StateHalfOpen (tryHalfOpen when open duration elapsed)
  250. // 3. StateClosed (recordResult on success in half-open)
  251. if len(states) < 3 {
  252. t.Fatalf("expected 3 state transitions, got %d: %v", len(states), states)
  253. }
  254. if states[0] != StateOpen {
  255. t.Fatalf("expected first to open (%d), got %d", StateOpen, states[0])
  256. }
  257. if states[1] != StateHalfOpen {
  258. t.Fatalf("expected second to half-open (%d), got %d", StateHalfOpen, states[1])
  259. }
  260. if states[2] != StateClosed {
  261. t.Fatalf("expected third to closed (%d), got %d", StateClosed, states[2])
  262. }
  263. }
  264. // TestConcurrentAccess verifies the breaker is safe for concurrent use.
  265. func TestConcurrentAccess(t *testing.T) {
  266. cb := New(Config{
  267. Name: "test",
  268. FailureThreshold: 5,
  269. FailureWindow: 100 * time.Millisecond,
  270. OpenDuration: 50 * time.Millisecond,
  271. MaxHalfOpen: 3,
  272. })
  273. ctx := context.Background()
  274. var wg sync.WaitGroup
  275. var errCount atomic.Int64
  276. for i := 0; i < 20; i++ {
  277. wg.Add(1)
  278. go func(fail bool) {
  279. defer wg.Done()
  280. err := cb.Do(ctx, func() error {
  281. if fail {
  282. errCount.Add(1)
  283. return errors.New("concurrent fail")
  284. }
  285. return nil
  286. })
  287. if fail && err != nil && !errors.Is(err, ErrCircuitOpen) {
  288. // Some errors are expected.
  289. }
  290. }(i%2 == 0)
  291. }
  292. wg.Wait()
  293. // No panic means the test passes.
  294. }
  295. // TestCtxCancel verifies that a canceled context causes Do to
  296. // return context.Canceled immediately without acquiring the circuit.
  297. // The fn is never called when the context is already canceled.
  298. func TestCtxCancel(t *testing.T) {
  299. var called bool
  300. cb := New(DefaultConfig("test"))
  301. ctx, cancel := context.WithCancel(context.Background())
  302. cancel() // already canceled before Do()
  303. err := cb.Do(ctx, func() error {
  304. called = true
  305. return nil
  306. })
  307. if !errors.Is(err, context.Canceled) {
  308. t.Fatalf("expected context.Canceled, got %v", err)
  309. }
  310. if called {
  311. t.Fatal("fn should not have been called with canceled context")
  312. }
  313. }