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  }