github.com/grahambrereton-form3/tilt@v0.10.18/internal/store/store.go (about) 1 package store 2 3 import ( 4 "context" 5 "sync" 6 "time" 7 8 "github.com/davecgh/go-spew/spew" 9 "gopkg.in/d4l3k/messagediff.v1" 10 11 "github.com/windmilleng/tilt/pkg/logger" 12 "github.com/windmilleng/tilt/pkg/model" 13 ) 14 15 // Allow actions to batch together a bit. 16 const actionBatchWindow = time.Millisecond 17 18 // Read-only store 19 type RStore interface { 20 Dispatch(action Action) 21 RLockState() EngineState 22 RUnlockState() 23 } 24 25 // A central state store, modeled after the Reactive programming UX pattern. 26 // Terminology is borrowed liberally from Redux. These docs in particular are helpful: 27 // https://redux.js.org/introduction/threeprinciples 28 // https://redux.js.org/basics 29 type Store struct { 30 state *EngineState 31 subscribers *subscriberList 32 actionQueue *actionQueue 33 actionCh chan []Action 34 mu sync.Mutex 35 stateMu sync.RWMutex 36 reduce Reducer 37 logActions bool 38 39 // TODO(nick): Define Subscribers and Reducers. 40 // The actionChan is an intermediate representation to make the transition easier. 41 } 42 43 func NewStore(reducer Reducer, logActions LogActionsFlag) *Store { 44 return &Store{ 45 state: NewState(), 46 reduce: reducer, 47 actionQueue: &actionQueue{}, 48 actionCh: make(chan []Action), 49 subscribers: &subscriberList{}, 50 logActions: bool(logActions), 51 } 52 } 53 54 // Returns a Store for testing that saves observed actions and makes them available 55 // via the return value `getActions` 56 func NewStoreForTesting() (st *Store, getActions func() []Action) { 57 var mu sync.Mutex 58 actions := []Action{} 59 reducer := Reducer(func(ctx context.Context, s *EngineState, action Action) { 60 mu.Lock() 61 defer mu.Unlock() 62 actions = append(actions, action) 63 64 errorAction, isErrorAction := action.(ErrorAction) 65 if isErrorAction { 66 s.FatalError = errorAction.Error 67 } 68 }) 69 70 getActions = func() []Action { 71 mu.Lock() 72 defer mu.Unlock() 73 return append([]Action{}, actions...) 74 } 75 return NewStore(reducer, false), getActions 76 } 77 78 func (s *Store) AddSubscriber(ctx context.Context, sub Subscriber) { 79 s.subscribers.Add(ctx, sub) 80 } 81 82 func (s *Store) RemoveSubscriber(ctx context.Context, sub Subscriber) error { 83 return s.subscribers.Remove(ctx, sub) 84 } 85 86 // Sends messages to all the subscribers asynchronously. 87 func (s *Store) NotifySubscribers(ctx context.Context) { 88 s.subscribers.NotifyAll(ctx, s) 89 } 90 91 // TODO(nick): Clone the state to ensure it's not mutated. 92 // For now, we use RW locks to simulate the same behavior, but the 93 // onus is on the caller to RUnlockState. 94 func (s *Store) RLockState() EngineState { 95 s.stateMu.RLock() 96 return *(s.state) 97 } 98 99 func (s *Store) RUnlockState() { 100 s.stateMu.RUnlock() 101 } 102 103 func (s *Store) LockMutableStateForTesting() *EngineState { 104 s.stateMu.Lock() 105 return s.state 106 } 107 108 func (s *Store) UnlockMutableState() { 109 s.stateMu.Unlock() 110 } 111 112 func (s *Store) Dispatch(action Action) { 113 s.actionQueue.add(action) 114 go s.drainActions() 115 } 116 117 func (s *Store) Close() { 118 close(s.actionCh) 119 } 120 121 func (s *Store) SetUpSubscribersForTesting(ctx context.Context) { 122 s.subscribers.SetUp(ctx) 123 } 124 125 func (s *Store) Loop(ctx context.Context) error { 126 s.subscribers.SetUp(ctx) 127 defer s.subscribers.TeardownAll(context.Background()) 128 129 for { 130 select { 131 case <-ctx.Done(): 132 return ctx.Err() 133 134 case actions := <-s.actionCh: 135 s.stateMu.Lock() 136 137 for _, action := range actions { 138 var oldState EngineState 139 if s.logActions { 140 oldState = s.cheapCopyState() 141 } 142 143 s.reduce(ctx, s.state, action) 144 145 if s.logActions { 146 newState := s.cheapCopyState() 147 go func() { 148 diff, equal := messagediff.PrettyDiff(oldState, newState) 149 if !equal { 150 logger.Get(ctx).Infof("action %T:\n%s\ncaused state change:\n%s\n", action, spew.Sdump(action), diff) 151 } 152 }() 153 } 154 } 155 156 s.stateMu.Unlock() 157 } 158 159 // Subscribers 160 done, err := s.maybeFinished() 161 if done { 162 return err 163 } 164 s.NotifySubscribers(ctx) 165 } 166 } 167 168 func (s *Store) maybeFinished() (bool, error) { 169 state := s.RLockState() 170 defer s.RUnlockState() 171 172 if state.FatalError == context.Canceled { 173 return true, state.FatalError 174 } 175 176 if state.UserExited { 177 return true, nil 178 } 179 180 if state.FatalError != nil && !state.HUDEnabled { 181 return true, state.FatalError 182 } 183 184 if len(state.ManifestTargets) == 0 { 185 return false, nil 186 } 187 188 finished := !state.WatchFiles && state.InitialBuildsCompleted() 189 190 return finished, nil 191 } 192 193 func (s *Store) drainActions() { 194 time.Sleep(actionBatchWindow) 195 196 // The mutex here ensures that the actions appear on the channel in-order. 197 // Otherwise, two drains can interleave badly. 198 s.mu.Lock() 199 defer s.mu.Unlock() 200 201 actions := s.actionQueue.drain() 202 if len(actions) > 0 { 203 s.actionCh <- actions 204 } 205 } 206 207 type Action interface { 208 Action() 209 } 210 211 type actionQueue struct { 212 actions []Action 213 mu sync.Mutex 214 } 215 216 func (q *actionQueue) add(action Action) { 217 q.mu.Lock() 218 defer q.mu.Unlock() 219 q.actions = append(q.actions, action) 220 } 221 222 func (q *actionQueue) drain() []Action { 223 q.mu.Lock() 224 defer q.mu.Unlock() 225 result := append([]Action{}, q.actions...) 226 q.actions = nil 227 return result 228 } 229 230 type LogActionsFlag bool 231 232 // This does a partial deep copy for the purposes of comparison 233 // i.e., it ensures fields that will be useful in action logging get copied 234 // some fields might not be copied and might still point to the same instance as s.state 235 // and thus might reflect changes that happened as part of the current action or any future action 236 func (s *Store) cheapCopyState() EngineState { 237 ret := *s.state 238 targets := ret.ManifestTargets 239 ret.ManifestTargets = make(map[model.ManifestName]*ManifestTarget) 240 for k, v := range targets { 241 ms := *(v.State) 242 target := &ManifestTarget{ 243 Manifest: v.Manifest, 244 State: &ms, 245 } 246 247 ret.ManifestTargets[k] = target 248 } 249 return ret 250 }