github.com/tilt-dev/tilt@v0.36.0/internal/store/subscriber.go (about)

     1  package store
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/tilt-dev/tilt/pkg/logger"
    12  )
    13  
    14  const MaxBackoff = time.Second * 15
    15  
    16  // A subscriber is notified whenever the state changes.
    17  //
    18  // Subscribers do not need to be thread-safe. The Store will only
    19  // call OnChange for a given subscriber when the last call completes.
    20  //
    21  // Subscribers are only allowed to read state. If they want to
    22  // modify state, they should call store.Dispatch().
    23  //
    24  // If OnChange returns an error, the store will requeue the change summary and
    25  // retry after a backoff period.
    26  //
    27  // Over time, we want to port all subscribers to use controller-runtime's
    28  // Reconciler interface. In the intermediate period, we expect this interface
    29  // will evolve to support all the features of Reconciler.
    30  //
    31  // https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile
    32  type Subscriber interface {
    33  	OnChange(ctx context.Context, st RStore, summary ChangeSummary) error
    34  }
    35  
    36  // Some subscribers need to do SetUp or TearDown.
    37  //
    38  // Both hold the subscriber lock, so should return quickly.
    39  //
    40  // SetUp and TearDown are called in serial.
    41  // SetUp is called in FIFO order while TearDown is LIFO so that
    42  // inter-subscriber dependencies are respected.
    43  type SetUpper interface {
    44  	// Initialize the subscriber.
    45  	//
    46  	// Any errors will trigger an ErrorAction.
    47  	SetUp(ctx context.Context, st RStore) error
    48  }
    49  type TearDowner interface {
    50  	TearDown(ctx context.Context)
    51  }
    52  
    53  // Convenience interface for subscriber fulfilling both SetUpper and TearDowner
    54  type SubscriberLifecycle interface {
    55  	SetUpper
    56  	TearDowner
    57  }
    58  
    59  type subscriberList struct {
    60  	subscribers []*subscriberEntry
    61  	setup       bool
    62  	mu          sync.Mutex
    63  }
    64  
    65  func (l *subscriberList) Add(ctx context.Context, st RStore, s Subscriber) error {
    66  	l.mu.Lock()
    67  	defer l.mu.Unlock()
    68  
    69  	e := &subscriberEntry{
    70  		subscriber: s,
    71  	}
    72  	l.subscribers = append(l.subscribers, e)
    73  	if l.setup {
    74  		// the rest of the subscriberList has already been set up, so set up this subscriber directly
    75  		return e.maybeSetUp(ctx, st)
    76  	}
    77  	return nil
    78  }
    79  
    80  func (l *subscriberList) Remove(ctx context.Context, s Subscriber) error {
    81  	l.mu.Lock()
    82  	defer l.mu.Unlock()
    83  
    84  	for i, current := range l.subscribers {
    85  		if s == current.subscriber {
    86  			l.subscribers = append(l.subscribers[:i], l.subscribers[i+1:]...)
    87  			if l.setup {
    88  				current.maybeTeardown(ctx)
    89  			}
    90  			return nil
    91  		}
    92  	}
    93  
    94  	return fmt.Errorf("Subscriber not found: %T: %+v", s, s)
    95  }
    96  
    97  func (l *subscriberList) SetUp(ctx context.Context, st RStore) error {
    98  	l.mu.Lock()
    99  	subscribers := append([]*subscriberEntry{}, l.subscribers...)
   100  	l.setup = true
   101  	l.mu.Unlock()
   102  
   103  	for _, s := range subscribers {
   104  		err := s.maybeSetUp(ctx, st)
   105  		if err != nil {
   106  			return err
   107  		}
   108  	}
   109  	return nil
   110  }
   111  
   112  // TeardownAll removes subscribes in the reverse order as they were subscribed.
   113  func (l *subscriberList) TeardownAll(ctx context.Context) {
   114  	l.mu.Lock()
   115  	subscribers := append([]*subscriberEntry{}, l.subscribers...)
   116  	l.setup = false
   117  	l.mu.Unlock()
   118  
   119  	for i := len(subscribers) - 1; i >= 0; i-- {
   120  		subscribers[i].maybeTeardown(ctx)
   121  	}
   122  }
   123  
   124  func (l *subscriberList) NotifyAll(ctx context.Context, store *Store, summary ChangeSummary) {
   125  	l.mu.Lock()
   126  	subscribers := append([]*subscriberEntry{}, l.subscribers...)
   127  	l.mu.Unlock()
   128  
   129  	for _, s := range subscribers {
   130  		isPending := s.claimPending(summary)
   131  		if isPending {
   132  			SafeGo(store, func() {
   133  				s.notify(ctx, store)
   134  			})
   135  		}
   136  	}
   137  }
   138  
   139  type subscriberEntry struct {
   140  	subscriber Subscriber
   141  
   142  	// At any given time, there are at most two goroutines
   143  	// notifying the subscriber: a pending goroutine and an active goroutine.
   144  	pendingChange *ChangeSummary
   145  
   146  	// The active mutex is held by the goroutine currently notifying the
   147  	// subscriber. It may be held for a long time if the subscriber
   148  	// takes a long time.
   149  	activeMu sync.Mutex
   150  
   151  	// The state mutex is just for updating the hasPending/hasActive state.
   152  	// It should never be held a long time.
   153  	stateMu sync.Mutex
   154  }
   155  
   156  // Returns true if this is the pending goroutine.
   157  // Returns false to do nothing.
   158  // If there's a pending change, we merge the passed summary.
   159  func (e *subscriberEntry) claimPending(s ChangeSummary) bool {
   160  	e.stateMu.Lock()
   161  	defer e.stateMu.Unlock()
   162  
   163  	if e.pendingChange != nil {
   164  		e.pendingChange.Add(s)
   165  		return false
   166  	}
   167  	e.pendingChange = &ChangeSummary{}
   168  	e.pendingChange.Add(s)
   169  	return true
   170  }
   171  
   172  func (e *subscriberEntry) movePendingToActive() *ChangeSummary {
   173  	e.stateMu.Lock()
   174  	defer e.stateMu.Unlock()
   175  
   176  	activeChange := e.pendingChange
   177  	e.pendingChange = nil
   178  	return activeChange
   179  }
   180  
   181  // returns a string identifying the subscriber's type using its package + type name
   182  // e.g. "engine/uiresource.Subscriber"
   183  func subscriberName(sub Subscriber) string {
   184  	typ := reflect.TypeOf(sub)
   185  	if typ.Kind() == reflect.Ptr {
   186  		typ = typ.Elem()
   187  	}
   188  	return fmt.Sprintf("%s.%s", strings.TrimPrefix(typ.PkgPath(), "github.com/tilt-dev/tilt/internal/"), typ.Name())
   189  }
   190  
   191  func (e *subscriberEntry) notify(ctx context.Context, store *Store) {
   192  	e.activeMu.Lock()
   193  	defer e.activeMu.Unlock()
   194  
   195  	activeChange := e.movePendingToActive()
   196  	err := e.subscriber.OnChange(ctx, store, *activeChange)
   197  	if err == nil {
   198  		// Success! Finish immediately.
   199  		return
   200  	}
   201  
   202  	if ctx.Err() != nil {
   203  		// context finished
   204  		return
   205  	}
   206  
   207  	// Backoff on error
   208  	backoff := activeChange.LastBackoff * 2
   209  	if backoff == 0 {
   210  		backoff = time.Second
   211  		logger.Get(ctx).Debugf("Problem processing change. Subscriber: %s. Backing off %v. Error: %v", subscriberName(e.subscriber), backoff, err)
   212  	} else if backoff > MaxBackoff {
   213  		backoff = MaxBackoff
   214  		logger.Get(ctx).Errorf("Problem processing change. Subscriber: %s. Backing off %v. Error: %v", subscriberName(e.subscriber), backoff, err)
   215  	}
   216  	store.sleeper.Sleep(ctx, backoff)
   217  
   218  	activeChange.LastBackoff = backoff
   219  
   220  	// Requeue the active change.
   221  	isPending := e.claimPending(*activeChange)
   222  	if isPending {
   223  		SafeGo(store, func() {
   224  			e.notify(ctx, store)
   225  		})
   226  	}
   227  }
   228  
   229  func (e *subscriberEntry) maybeSetUp(ctx context.Context, st RStore) error {
   230  	s, ok := e.subscriber.(SetUpper)
   231  	if ok {
   232  		e.activeMu.Lock()
   233  		defer e.activeMu.Unlock()
   234  		return s.SetUp(ctx, st)
   235  	}
   236  	return nil
   237  }
   238  
   239  func (e *subscriberEntry) maybeTeardown(ctx context.Context) {
   240  	s, ok := e.subscriber.(TearDowner)
   241  	if ok {
   242  		e.activeMu.Lock()
   243  		defer e.activeMu.Unlock()
   244  		s.TearDown(ctx)
   245  	}
   246  }