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  }