@@ -18,150 +18,118 @@ module AutoClusterFailoverTests =
1818 SwitchBackDelay = TimeSpan.FromSeconds( 60.0 )
1919 SecondaryCount = 2
2020 }
21+ let noSecondaryAvailable = fun () -> Task.FromResult( None)
22+ let secondaryAvailable index = fun () -> Task.FromResult( Some index)
2123
2224 // ---- Pure state-machine tests ----
2325
2426 [<Tests>]
25- let pureTests =
27+ let stepTests =
2628
2729 testList " AutoClusterFailoverLogic" [
2830
29- // -- shouldProbeSecondaries --
30-
31- test " shouldProbeSecondaries: false when primary is available" {
32- let state = { AutoClusterFailoverLogic.initialState with FailedTimestamp = Some t0 }
33- AutoClusterFailoverLogic.shouldProbeSecondaries t0 config true state
34- |> Expect.isFalse " should not probe when primary is up"
35- }
36-
37- test " shouldProbeSecondaries: false when no failedTimestamp yet" {
38- let state = AutoClusterFailoverLogic.initialState
39- AutoClusterFailoverLogic.shouldProbeSecondaries t0 config false state
40- |> Expect.isFalse " should not probe before first failure recorded"
41- }
42-
43- test " shouldProbeSecondaries: false when within failover delay" {
44- let state = { AutoClusterFailoverLogic.initialState with FailedTimestamp = Some t0 }
45- let now = t0 + TimeSpan.FromSeconds( 29.0 )
46- AutoClusterFailoverLogic.shouldProbeSecondaries now config false state
47- |> Expect.isFalse " should not probe before delay elapsed"
48- }
49-
50- test " shouldProbeSecondaries: true when primary down and delay elapsed" {
51- let state = { AutoClusterFailoverLogic.initialState with FailedTimestamp = Some t0 }
52- let now = t0 + TimeSpan.FromSeconds( 30.0 )
53- AutoClusterFailoverLogic.shouldProbeSecondaries now config false state
54- |> Expect.isTrue " should probe after delay elapsed"
55- }
56-
57- test " shouldProbeSecondaries: false when on secondary" {
58- let state = { AutoClusterFailoverLogic.initialState with Mode = AutoClusterMode.Secondary 0 }
59- AutoClusterFailoverLogic.shouldProbeSecondaries t0 config false state
60- |> Expect.isFalse " should not probe secondaries when already on secondary"
61- }
62-
6331 // -- step: on primary --
6432
65- test " step: primary available clears failedTimestamp" {
66- let state = { AutoClusterFailoverLogic.initialState with FailedTimestamp = Some t0 }
67- let newState , decision =
68- AutoClusterFailoverLogic.step t0 config true None state
69- newState.FailedTimestamp |> Expect.isNone " FailedTimestamp should be cleared"
70- decision |> Expect.equal " should be Noop" AutoClusterDecision.Noop
33+ testTask " step: primary available clears failedTimestamp" {
34+ let state = { AutoClusterFailoverLogic.initialState with PrimaryFailedTimestamp = Some t0 }
35+ let! newState , decision =
36+ AutoClusterFailoverLogic.step t0 config true noSecondaryAvailable state
37+ newState.PrimaryFailedTimestamp |> Expect.isNone " FailedTimestamp should be cleared"
38+ decision |> Expect.equal " should be Noop" AutoClusterDecision.NoAction
7139 }
7240
73- test " step: primary unavailable records failedTimestamp on first failure" {
41+ testTask " step: primary unavailable records failedTimestamp on first failure" {
7442 let state = AutoClusterFailoverLogic.initialState
75- let newState , decision =
76- AutoClusterFailoverLogic.step t0 config false None state
77- newState.FailedTimestamp |> Expect.equal " should record timestamp" ( Some t0)
78- decision |> Expect.equal " should be Noop" AutoClusterDecision.Noop
43+ let! newState , decision =
44+ AutoClusterFailoverLogic.step t0 config false noSecondaryAvailable state
45+ newState.PrimaryFailedTimestamp |> Expect.equal " should record timestamp" ( Some t0)
46+ decision |> Expect.equal " should be Noop" AutoClusterDecision.NoAction
7947 }
8048
81- test " step: does not switch before failover delay elapses" {
82- let state = { AutoClusterFailoverLogic.initialState with FailedTimestamp = Some t0 }
49+ testTask " step: does not switch before failover delay elapses" {
50+ let state = { AutoClusterFailoverLogic.initialState with PrimaryFailedTimestamp = Some t0 }
8351 let now = t0 + TimeSpan.FromSeconds( 29.0 )
84- let newState , decision =
85- AutoClusterFailoverLogic.step now config false ( Some 0 ) state
52+ let! newState , decision =
53+ AutoClusterFailoverLogic.step now config false ( secondaryAvailable 0 ) state
8654 newState.Mode |> Expect.equal " should stay Primary" AutoClusterMode.Primary
87- decision |> Expect.equal " should be Noop" AutoClusterDecision.Noop
55+ decision |> Expect.equal " should be Noop" AutoClusterDecision.NoAction
8856 }
8957
90- test " step: switches to secondary after failover delay" {
91- let state = { AutoClusterFailoverLogic.initialState with FailedTimestamp = Some t0 }
58+ testTask " step: switches to secondary after failover delay" {
59+ let state = { AutoClusterFailoverLogic.initialState with PrimaryFailedTimestamp = Some t0 }
9260 let now = t0 + TimeSpan.FromSeconds( 30.0 )
93- let newState , decision =
94- AutoClusterFailoverLogic.step now config false ( Some 1 ) state
61+ let! newState , decision =
62+ AutoClusterFailoverLogic.step now config false ( secondaryAvailable 1 ) state
9563 newState.Mode |> Expect.equal " should switch to Secondary 1" ( AutoClusterMode.Secondary 1 )
96- newState.FailedTimestamp |> Expect.isNone " FailedTimestamp should be cleared"
64+ newState.PrimaryFailedTimestamp |> Expect.isNone " FailedTimestamp should be cleared"
9765 decision |> Expect.equal " should switch" ( AutoClusterDecision.SwitchToSecondary 1 )
9866 }
9967
100- test " step: no available secondary keeps state unchanged" {
101- let state = { AutoClusterFailoverLogic.initialState with FailedTimestamp = Some t0 }
68+ testTask " step: no available secondary keeps state unchanged" {
69+ let state = { AutoClusterFailoverLogic.initialState with PrimaryFailedTimestamp = Some t0 }
10270 let now = t0 + TimeSpan.FromSeconds( 30.0 )
103- let newState , decision =
104- AutoClusterFailoverLogic.step now config false None state
105- newState.FailedTimestamp |> Expect.equal " should keep timestamp" ( Some t0)
106- decision |> Expect.equal " should be Noop" AutoClusterDecision.Noop
71+ let! newState , decision =
72+ AutoClusterFailoverLogic.step now config false noSecondaryAvailable state
73+ newState.PrimaryFailedTimestamp |> Expect.equal " should keep timestamp" ( Some t0)
74+ decision |> Expect.equal " should be Noop" AutoClusterDecision.NoAction
10775 }
10876
109- test " step: primary becomes available resets failedTimestamp" {
110- let state = { AutoClusterFailoverLogic.initialState with FailedTimestamp = Some t0 }
77+ testTask " step: primary becomes available resets failedTimestamp" {
78+ let state = { AutoClusterFailoverLogic.initialState with PrimaryFailedTimestamp = Some t0 }
11179 let now = t0 + TimeSpan.FromSeconds( 10.0 )
112- let newState , _ =
113- AutoClusterFailoverLogic.step now config true None state
114- newState.FailedTimestamp |> Expect.isNone " FailedTimestamp should be cleared"
80+ let! newState , _ =
81+ AutoClusterFailoverLogic.step now config true noSecondaryAvailable state
82+ newState.PrimaryFailedTimestamp |> Expect.isNone " FailedTimestamp should be cleared"
11583 }
11684
11785 // -- step: on secondary --
11886
119- test " step: on secondary, primary available records recoveredTimestamp" {
87+ testTask " step: on secondary, primary available records recoveredTimestamp" {
12088 let state = { AutoClusterFailoverLogic.initialState with Mode = AutoClusterMode.Secondary 0 }
121- let newState , decision =
122- AutoClusterFailoverLogic.step t0 config true None state
123- newState.RecoveredTimestamp |> Expect.equal " should record timestamp" ( Some t0)
124- decision |> Expect.equal " should be Noop" AutoClusterDecision.Noop
89+ let! newState , decision =
90+ AutoClusterFailoverLogic.step t0 config true noSecondaryAvailable state
91+ newState.PrimaryRecoveredTimestamp |> Expect.equal " should record timestamp" ( Some t0)
92+ decision |> Expect.equal " should be Noop" AutoClusterDecision.NoAction
12593 }
12694
127- test " step: on secondary, does not switch back before switchBackDelay" {
95+ testTask " step: on secondary, does not switch back before switchBackDelay" {
12896 let state = {
12997 Mode = AutoClusterMode.Secondary 0
130- FailedTimestamp = None
131- RecoveredTimestamp = Some t0
98+ PrimaryFailedTimestamp = None
99+ PrimaryRecoveredTimestamp = Some t0
132100 }
133101 let now = t0 + TimeSpan.FromSeconds( 59.0 )
134- let newState , decision =
135- AutoClusterFailoverLogic.step now config true None state
102+ let! newState , decision =
103+ AutoClusterFailoverLogic.step now config true noSecondaryAvailable state
136104 newState.Mode |> Expect.equal " should stay Secondary" ( AutoClusterMode.Secondary 0 )
137- decision |> Expect.equal " should be Noop" AutoClusterDecision.Noop
105+ decision |> Expect.equal " should be Noop" AutoClusterDecision.NoAction
138106 }
139107
140- test " step: on secondary, switches back to primary after switchBackDelay" {
108+ testTask " step: on secondary, switches back to primary after switchBackDelay" {
141109 let state = {
142110 Mode = AutoClusterMode.Secondary 0
143- FailedTimestamp = None
144- RecoveredTimestamp = Some t0
111+ PrimaryFailedTimestamp = None
112+ PrimaryRecoveredTimestamp = Some t0
145113 }
146114 let now = t0 + TimeSpan.FromSeconds( 60.0 )
147- let newState , decision =
148- AutoClusterFailoverLogic.step now config true None state
115+ let! newState , decision =
116+ AutoClusterFailoverLogic.step now config true noSecondaryAvailable state
149117 newState.Mode |> Expect.equal " should be Primary" AutoClusterMode.Primary
150- newState.RecoveredTimestamp |> Expect.isNone " RecoveredTimestamp should be cleared"
118+ newState.PrimaryRecoveredTimestamp |> Expect.isNone " RecoveredTimestamp should be cleared"
151119 decision |> Expect.equal " should switch back" AutoClusterDecision.SwitchToPrimary
152120 }
153121
154- test " step: on secondary, primary goes down again clears recoveredTimestamp" {
122+ testTask " step: on secondary, primary goes down again clears recoveredTimestamp" {
155123 let state = {
156124 Mode = AutoClusterMode.Secondary 0
157- FailedTimestamp = None
158- RecoveredTimestamp = Some t0
125+ PrimaryFailedTimestamp = None
126+ PrimaryRecoveredTimestamp = Some t0
159127 }
160128 let now = t0 + TimeSpan.FromSeconds( 10.0 )
161- let newState , decision =
162- AutoClusterFailoverLogic.step now config false None state
163- newState.RecoveredTimestamp |> Expect.isNone " RecoveredTimestamp should be cleared"
164- decision |> Expect.equal " should be Noop" AutoClusterDecision.Noop
129+ let! newState , decision =
130+ AutoClusterFailoverLogic.step now config false noSecondaryAvailable state
131+ newState.PrimaryRecoveredTimestamp |> Expect.isNone " RecoveredTimestamp should be cleared"
132+ decision |> Expect.equal " should be Noop" AutoClusterDecision.NoAction
165133 }
166134 ]
167135
0 commit comments