github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/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/tilt-dev/tilt/pkg/logger" 12 "github.com/tilt-dev/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 StateMutex() *sync.RWMutex 24 } 25 26 // A central state store, modeled after the Reactive programming UX pattern. 27 // Terminology is borrowed liberally from Redux. These docs in particular are helpful: 28 // https://redux.js.org/introduction/threeprinciples 29 // https://redux.js.org/basics 30 type Store struct { 31 sleeper Sleeper 32 state *EngineState 33 subscribers *subscriberList 34 actionQueue *actionQueue 35 actionCh chan []Action 36 mu sync.Mutex 37 stateMu sync.RWMutex 38 reduce Reducer 39 logActions bool 40 41 // TODO(nick): Define Subscribers and Reducers. 42 // The actionChan is an intermediate representation to make the transition easier. 43 } 44 45 func NewStore(reducer Reducer, logActions LogActionsFlag) *Store { 46 return &Store{ 47 sleeper: DefaultSleeper(), 48 state: NewState(), 49 reduce: reducer, 50 actionQueue: &actionQueue{}, 51 actionCh: make(chan []Action), 52 subscribers: &subscriberList{}, 53 logActions: bool(logActions), 54 } 55 } 56 57 // Returns a Store with a fake reducer that saves observed actions and makes 58 // them available via the return value `getActions`. 59 // 60 // Tests should only use this if they: 61 // 1) want to test the Store itself, or 62 // 2) want to test subscribers with the particular async behavior of a real Store 63 // Otherwise, use NewTestingStore(). 64 func NewStoreWithFakeReducer() (st *Store, getActions func() []Action) { 65 var mu sync.Mutex 66 actions := []Action{} 67 reducer := Reducer(func(ctx context.Context, s *EngineState, action Action) { 68 mu.Lock() 69 defer mu.Unlock() 70 actions = append(actions, action) 71 72 errorAction, isErrorAction := action.(ErrorAction) 73 if isErrorAction { 74 s.FatalError = errorAction.Error 75 } 76 }) 77 78 getActions = func() []Action { 79 mu.Lock() 80 defer mu.Unlock() 81 return append([]Action{}, actions...) 82 } 83 return NewStore(reducer, false), getActions 84 } 85 86 func (s *Store) StateMutex() *sync.RWMutex { 87 return &s.stateMu 88 } 89 90 func (s *Store) AddSubscriber(ctx context.Context, sub Subscriber) error { 91 return s.subscribers.Add(ctx, s, sub) 92 } 93 94 func (s *Store) RemoveSubscriber(ctx context.Context, sub Subscriber) error { 95 return s.subscribers.Remove(ctx, sub) 96 } 97 98 // Sends messages to all the subscribers asynchronously. 99 func (s *Store) NotifySubscribers(ctx context.Context, summary ChangeSummary) { 100 s.subscribers.NotifyAll(ctx, s, summary) 101 } 102 103 // TODO(nick): Clone the state to ensure it's not mutated. 104 // For now, we use RW locks to simulate the same behavior, but the 105 // onus is on the caller to RUnlockState. 106 func (s *Store) RLockState() EngineState { 107 s.stateMu.RLock() 108 return *(s.state) 109 } 110 111 func (s *Store) RUnlockState() { 112 s.stateMu.RUnlock() 113 } 114 115 func (s *Store) LockMutableStateForTesting() *EngineState { 116 s.stateMu.Lock() 117 return s.state 118 } 119 120 func (s *Store) UnlockMutableState() { 121 s.stateMu.Unlock() 122 } 123 124 func (s *Store) Dispatch(action Action) { 125 s.actionQueue.add(action) 126 go s.drainActions() 127 } 128 129 func (s *Store) Close() { 130 close(s.actionCh) 131 } 132 133 func (s *Store) SetUpSubscribersForTesting(ctx context.Context) error { 134 return s.subscribers.SetUp(ctx, s) 135 } 136 137 func (s *Store) Loop(ctx context.Context) error { 138 err := s.subscribers.SetUp(ctx, s) 139 if err != nil { 140 return err 141 } 142 defer s.subscribers.TeardownAll(context.Background()) 143 144 // Set up a defer handler, and make sure to unlock the state 145 // if the control loop is interrupted by a panic. 146 hasStateLock := false 147 defer func() { 148 if hasStateLock { 149 s.stateMu.Unlock() 150 } 151 }() 152 153 for { 154 summary := ChangeSummary{} 155 156 select { 157 case <-ctx.Done(): 158 return ctx.Err() 159 160 case actions := <-s.actionCh: 161 s.stateMu.Lock() 162 hasStateLock = true 163 164 logCheckpoint := s.state.LogStore.Checkpoint() 165 166 for _, action := range actions { 167 var oldState EngineState 168 if s.logActions { 169 oldState = s.cheapCopyState() 170 } 171 172 s.reduce(ctx, s.state, action) 173 174 if summarizer, ok := action.(Summarizer); ok { 175 summarizer.Summarize(&summary) 176 } else { 177 summary.Legacy = true 178 } 179 180 if s.logActions { 181 newState := s.cheapCopyState() 182 action := action 183 go func() { 184 diff, equal := messagediff.PrettyDiff(oldState, newState) 185 if !equal { 186 logger.Get(ctx).Infof("action %T:\n%s\ncaused state change:\n%s\n", action, spew.Sdump(action), diff) 187 } 188 }() 189 } 190 } 191 192 // if one of the actions logged, but didn't report it via Summarizer, 193 // include it in the summary anyway 194 if logCheckpoint != s.state.LogStore.Checkpoint() { 195 summary.Log = true 196 } 197 198 s.stateMu.Unlock() 199 hasStateLock = false 200 } 201 202 // Subscribers 203 done, err := s.maybeFinished() 204 if done { 205 return err 206 } 207 s.NotifySubscribers(ctx, summary) 208 } 209 } 210 211 func (s *Store) maybeFinished() (bool, error) { 212 state := s.RLockState() 213 defer s.RUnlockState() 214 215 if state.FatalError == context.Canceled { 216 return true, state.FatalError 217 } 218 219 if state.UserExited { 220 return true, nil 221 } 222 223 if state.PanicExited != nil { 224 return true, state.PanicExited 225 } 226 227 if state.FatalError != nil && state.TerminalMode != TerminalModeHUD { 228 return true, state.FatalError 229 } 230 231 if state.ExitSignal { 232 return true, state.ExitError 233 } 234 235 return false, nil 236 } 237 238 func (s *Store) drainActions() { 239 s.sleeper.Sleep(context.Background(), actionBatchWindow) 240 241 // The mutex here ensures that the actions appear on the channel in-order. 242 // Otherwise, two drains can interleave badly. 243 s.mu.Lock() 244 defer s.mu.Unlock() 245 246 actions := s.actionQueue.drain() 247 if len(actions) > 0 { 248 s.actionCh <- actions 249 } 250 } 251 252 type Action interface { 253 Action() 254 } 255 256 type actionQueue struct { 257 actions []Action 258 mu sync.Mutex 259 } 260 261 func (q *actionQueue) add(action Action) { 262 q.mu.Lock() 263 defer q.mu.Unlock() 264 q.actions = append(q.actions, action) 265 } 266 267 func (q *actionQueue) drain() []Action { 268 q.mu.Lock() 269 defer q.mu.Unlock() 270 result := append([]Action{}, q.actions...) 271 q.actions = nil 272 return result 273 } 274 275 type LogActionsFlag bool 276 277 // This does a partial deep copy for the purposes of comparison 278 // i.e., it ensures fields that will be useful in action logging get copied 279 // some fields might not be copied and might still point to the same instance as s.state 280 // and thus might reflect changes that happened as part of the current action or any future action 281 func (s *Store) cheapCopyState() EngineState { 282 ret := *s.state 283 targets := ret.ManifestTargets 284 ret.ManifestTargets = make(map[model.ManifestName]*ManifestTarget) 285 for k, v := range targets { 286 ms := *(v.State) 287 target := &ManifestTarget{ 288 Manifest: v.Manifest, 289 State: &ms, 290 } 291 292 ret.ManifestTargets[k] = target 293 } 294 return ret 295 }