state.lua 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. --[[
  2. test_state.lua — Watchdog state machine unit tests
  3. Run: lua5.1 tests/test_state.lua
  4. ]]
  5. package.path = package.path .. ";./src/?.lua"
  6. local M = require("balancerlite.state")
  7. local state = M.state
  8. local function assert_eq(a, b, msg)
  9. if a ~= b then
  10. error(((msg or "") .. ": expected " .. tostring(b) .. ", got " .. tostring(a)), 2)
  11. end
  12. end
  13. local TBASE = 1000000000
  14. local function feed_ok(s, wan_id, n, base)
  15. base = base or TBASE
  16. for i = 1, (n or 1) do
  17. M.feed(s, wan_id, {ok=true, rtt_ms=5, ts=base+i})
  18. end
  19. end
  20. local function feed_fail(s, wan_id, n, base)
  21. base = base or TBASE
  22. for i = 1, (n or 1) do
  23. M.feed(s, wan_id, {ok=false, ts=base+i})
  24. end
  25. end
  26. print("=== State machine tests ===")
  27. -- TEST 1: INIT → WAN_A_PRIMARY
  28. local S = M.new({ down_threshold=3, up_threshold=3, flap_threshold=99,
  29. flap_window_s=300, flap_recovery=300 })
  30. assert_eq(S.cur_state, state.INIT, "initial state")
  31. feed_ok(S, "wan-a", 3)
  32. local ev = M.advance(S)
  33. assert_eq(S.cur_state, state.WAN_A_PRIMARY, "wan-a up → WAN_A_PRIMARY")
  34. assert_eq(ev.type, "state.init", "event type")
  35. print(" TEST 1: INIT→WAN_A_PRIMARY: OK")
  36. -- TEST 2: Failover wan-a → wan-b
  37. S = M.new({ down_threshold=3, up_threshold=3, flap_threshold=99,
  38. flap_window_s=300, flap_recovery=300 })
  39. feed_ok(S, "wan-a", 3); M.advance(S) -- WAN_A_PRIMARY
  40. assert_eq(S.cur_state, state.WAN_A_PRIMARY, "setup: wan-a primary")
  41. feed_ok(S, "wan-b", 3); M.advance(S) -- wan-b is also up
  42. feed_fail(S, "wan-a", 3)
  43. ev = M.advance(S)
  44. assert_eq(S.cur_state, state.SWITCHING_TO_B, "wan-a down + wan-b up → SWITCHING_TO_B")
  45. assert_eq(ev.type, "failover.switch", "event type")
  46. ev = M.advance(S)
  47. assert_eq(S.cur_state, state.WAN_B_PRIMARY, "confirmed → WAN_B_PRIMARY")
  48. print(" TEST 2: wan-a→wan-b failover: OK")
  49. -- TEST 3: BOTH_DOWN when both fail
  50. S = M.new({ down_threshold=2, up_threshold=2, flap_threshold=99,
  51. flap_window_s=300, flap_recovery=300 })
  52. feed_ok(S, "wan-a", 2); M.advance(S)
  53. feed_fail(S, "wan-a", 2); feed_fail(S, "wan-b", 2)
  54. ev = M.advance(S)
  55. assert_eq(S.cur_state, state.BOTH_DOWN, "both fail → BOTH_DOWN")
  56. assert_eq(ev.type, "state.both_down", "event type")
  57. print(" TEST 3: BOTH_DOWN: OK")
  58. -- TEST 4: Recovery from BOTH_DOWN → WAN_B_PRIMARY (direct, no SWITCHING)
  59. S = M.new({ down_threshold=2, up_threshold=2, flap_threshold=99,
  60. flap_window_s=300, flap_recovery=300 })
  61. feed_ok(S, "wan-a", 2); M.advance(S)
  62. feed_fail(S, "wan-a", 2); feed_fail(S, "wan-b", 2)
  63. M.advance(S) -- BOTH_DOWN
  64. assert_eq(S.cur_state, state.BOTH_DOWN, "setup: BOTH_DOWN")
  65. feed_ok(S, "wan-b", 2)
  66. ev = M.advance(S)
  67. assert_eq(S.cur_state, state.WAN_B_PRIMARY, "wan-b recovers → WAN_B_PRIMARY")
  68. assert_eq(ev.type, "state.recovered", "event is state.recovered")
  69. print(" TEST 4: BOTH_DOWN recovery: OK")
  70. -- TEST 5: Hysteresis (up_thr > down_thr)
  71. S = M.new({ down_threshold=2, up_threshold=5, flap_threshold=99,
  72. flap_window_s=300, flap_recovery=300 })
  73. feed_ok(S, "wan-a", 5); M.advance(S)
  74. assert_eq(S.cur_state, state.WAN_A_PRIMARY, "setup: wan-a primary")
  75. feed_fail(S, "wan-a", 2); feed_fail(S, "wan-b", 2)
  76. ev = M.advance(S)
  77. assert_eq(S.cur_state, state.BOTH_DOWN, "both fail (2 fails each) → BOTH_DOWN")
  78. print(" TEST 5: hysteresis (up=5, down=2): OK")
  79. -- TEST 6: DEGRADED on flap (3 switches needed)
  80. -- Each switch: stable → failure+ok → SWITCHING → advance → stable (records switch).
  81. -- 3 switches = 3 advance() calls from stable states that changed = DEGRADED.
  82. S = M.new({ down_threshold=2, up_threshold=2, flap_threshold=3,
  83. flap_window_s=300, flap_recovery=300 })
  84. feed_ok(S, "wan-a", 2); M.advance(S) -- → WAN_A_PRIMARY [switch 1]
  85. assert_eq(S.cur_state, state.WAN_A_PRIMARY)
  86. -- Switch 1: A→B
  87. feed_fail(S, "wan-a", 2); feed_ok(S, "wan-b", 2)
  88. M.advance(S) -- → SWITCHING_TO_B
  89. M.advance(S) -- → WAN_B_PRIMARY [switch 2]
  90. -- Switch 2: B→A
  91. feed_fail(S, "wan-b", 2); feed_ok(S, "wan-a", 2)
  92. M.advance(S) -- → SWITCHING_TO_A
  93. M.advance(S) -- → WAN_A_PRIMARY [switch 3]
  94. -- Switch 3: A→B — 3rd switch → DEGRADED
  95. feed_fail(S, "wan-a", 2); feed_ok(S, "wan-b", 2)
  96. M.advance(S) -- → SWITCHING_TO_B
  97. ev = M.advance(S) -- → DEGRADED (nswitches=3 >= flap_threshold=3)
  98. assert_eq(S.cur_state, state.DEGRADED, "3 prior switches → DEGRADED")
  99. assert_eq(ev.type, "flap.alert", "event is flap.alert")
  100. print(" TEST 6: flap → DEGRADED: OK")
  101. -- TEST 7: DEGRADED persists until flap_recovery (use flap_threshold=2 so BOTH_DOWN
  102. -- doesn't keep accumulating; BOTH_DOWN clears switch_times, so only count stable switches)
  103. S = M.new({ down_threshold=2, up_threshold=2, flap_threshold=2,
  104. flap_window_s=300, flap_recovery=300 })
  105. feed_ok(S, "wan-a", 2); M.advance(S) -- → WAN_A_PRIMARY
  106. feed_fail(S, "wan-a", 2); feed_ok(S, "wan-b", 2)
  107. M.advance(S); M.advance(S) -- → WAN_B_PRIMARY [switch 1]
  108. assert_eq(S.cur_state, state.WAN_B_PRIMARY)
  109. -- Switch 2: B→A → DEGRADED (flap_threshold=2, 2nd switch triggers DEGRADED)
  110. feed_fail(S, "wan-b", 2); feed_ok(S, "wan-a", 2)
  111. M.advance(S); -- → SWITCHING_TO_A
  112. ev = M.advance(S) -- → WAN_A_PRIMARY [switch 2] → DEGRADED
  113. assert_eq(S.cur_state, state.DEGRADED, "2 switches → DEGRADED")
  114. assert_eq(ev.type, "flap.alert", "event is flap.alert")
  115. ev = M.advance(S) -- no new switches, no calm period → stays DEGRADED
  116. assert_eq(S.cur_state, state.DEGRADED, "still DEGRADED (no calm period)")
  117. print(" TEST 7: DEGRADED persists until calm: OK")
  118. -- TEST 8: Switch-back to preferred (A recovers while on B)
  119. S = M.new({ down_threshold=2, up_threshold=2, flap_threshold=99,
  120. flap_window_s=300, flap_recovery=300 })
  121. feed_ok(S, "wan-a", 2); M.advance(S) -- WAN_A_PRIMARY
  122. feed_fail(S, "wan-a", 2); feed_ok(S, "wan-b", 2)
  123. M.advance(S) -- → SWITCHING_TO_B
  124. M.advance(S) -- → WAN_B_PRIMARY (now on wan-b)
  125. assert_eq(S.cur_state, state.WAN_B_PRIMARY, "on wan-b")
  126. feed_ok(S, "wan-a", 2) -- wan-a recovers
  127. ev = M.advance(S)
  128. assert_eq(S.cur_state, state.SWITCHING_TO_A, "wan-a recovered → SWITCHING_TO_A")
  129. ev = M.advance(S)
  130. assert_eq(S.cur_state, state.WAN_A_PRIMARY, "confirmed → WAN_A_PRIMARY")
  131. print(" TEST 8: switch-back to preferred: OK")
  132. print("PASS: state")