1 /** 2 * ae.utils.statequeue 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.utils.statequeue; 15 16 import core.time; 17 18 import ae.net.asockets; 19 import ae.utils.promise; 20 21 /** 22 Let `f(x)` be an expensive operation which changes something to 23 (or towards) state `x`. At most one `f` call may be in progress at any time. 24 This type orchestrates a series of operations that eventually bring 25 the state to some goal, while allowing the goal to change at any time. 26 */ 27 struct StateQueue(State) 28 { 29 private: 30 bool prodPending; 31 32 // Flag to aid the debug invariant. 33 // Normally (oldState == newState) == (currentTransition is null). 34 // One exception to that is when a transition from an invalid state started, 35 // and setCurrentState was called during the transition, 36 // so we're "transitioning" from an invalid to an invalid state. 37 debug bool stateWasReset; 38 39 void enqueueProd() 40 { 41 if (prodPending) 42 return; 43 prodPending = true; 44 socketManager.onNextTick(&prod); 45 } 46 47 void prod() 48 { 49 prodPending = false; 50 if (currentTransition) 51 return; // Will be picked up in onComplete 52 assert(oldState == newState); 53 if (newState == goalState) 54 return; // Already in the goal state 55 // Start a new transition 56 newState = goalState; 57 currentTransition = stateFunc(goalState) 58 .then(&onComplete, &onFail); 59 } 60 61 void onComplete(State resultState) 62 { 63 assert(currentTransition); 64 debug assert(oldState != newState || stateWasReset); 65 debug stateWasReset = false; 66 oldState = newState = resultState; 67 currentTransition = null; 68 69 if (newState == goalState) 70 goalPromise.fulfill(); 71 72 prod(); 73 } 74 75 void onFail(Exception e) 76 { 77 assert(currentTransition); 78 debug assert(oldState != newState || stateWasReset); 79 debug stateWasReset = false; 80 81 // TODO: The logic here may be incomplete. 82 // For now, just notify the application that 83 // the transition failed. 84 goalPromise.reject(e); 85 } 86 87 public: 88 @disable this(); 89 90 /// The asynchronous implementation function which actually changes the state. 91 Promise!State delegate(State) stateFunc; 92 93 /// The state that any current change is moving away from. 94 State oldState; 95 96 /// The state that any current change is moving towards. 97 State newState; 98 99 /// The final state that we want to be in. 100 State goalState; 101 102 /// The promise that will be fulfilled when we reach the goal state. 103 Promise!void goalPromise; 104 105 /// The current state transition. 106 Promise!void currentTransition; 107 108 debug invariant 109 { 110 if (currentTransition) 111 assert(oldState != newState || stateWasReset); 112 else 113 assert(oldState == newState); 114 } 115 116 /// Constructor. 117 this( 118 /// The function implementing the state transition operation. 119 /// Accepts the goal state, and returns a promise which is 120 /// the resulting (ideally but necessarily, the goal) state. 121 Promise!State delegate(State) stateFunc, 122 /// The initial state. 123 State initialState = State.init, 124 ) 125 { 126 this.stateFunc = stateFunc; 127 this.oldState = this.newState = this.goalState = initialState; 128 goalPromise = resolve(); 129 } 130 131 /// Set the goal state. Starts off a transition operation if needed. 132 /// Returns a promise that will be fulfilled when we reach the goal state, 133 /// or rejected if the goal state changes before it is reached. 134 Promise!void setGoal(State state) 135 { 136 if (goalState != state) 137 { 138 if (currentTransition || newState != goalState) 139 goalPromise.reject(new Exception("Goal changed")); 140 goalPromise = new Promise!void; 141 142 this.goalState = state; 143 enqueueProd(); 144 } 145 return goalPromise; 146 } 147 148 /// Can be used to indicate that the state has been changed externally 149 /// (e.g. to some "invalid"/"dirty" state). 150 /// If a transition operation is already in progress, assume that it will 151 /// change the state to the given state instead of its actual goal. 152 void setCurrentState(State state = State.init) 153 { 154 if (currentTransition) 155 { 156 newState = state; 157 debug stateWasReset = true; 158 } 159 else 160 { 161 oldState = newState = state; 162 enqueueProd(); 163 } 164 } 165 } 166 167 // Test changing the goal multiple times per tick 168 debug(ae_unittest) unittest 169 { 170 import ae.utils.promise.timing : sleep; 171 172 int state, workDone; 173 Promise!int changeState(int i) 174 { 175 return sleep(1.msecs).then({ 176 workDone++; 177 state = i; 178 return i; 179 }); 180 } 181 182 auto q = StateQueue!int(&changeState); 183 assert(workDone == 0); 184 185 q.setGoal(1).ignoreResult(); 186 q.setGoal(2).ignoreResult(); 187 socketManager.loop(); 188 assert(state == 2 && workDone == 1); 189 } 190 191 // Test incremental transitions towards the goal 192 debug(ae_unittest) unittest 193 { 194 import ae.utils.promise.timing : sleep; 195 196 int state, workDone; 197 Promise!int changeState(int i) 198 { 199 return sleep(1.msecs).then({ 200 workDone++; 201 auto nextState = state + 1; 202 state = nextState; 203 return nextState; 204 }); 205 } 206 207 auto q = StateQueue!int(&changeState); 208 assert(workDone == 0); 209 210 q.setGoal(3).ignoreResult(); 211 socketManager.loop(); 212 assert(state == 3 && workDone == 3); 213 }