github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/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  		s := s
   131  		isPending := s.claimPending(summary)
   132  		if isPending {
   133  			SafeGo(store, func() {
   134  				s.notify(ctx, store)
   135  			})
   136  		}
   137  	}
   138  }
   139  
   140  type subscriberEntry struct {
   141  	subscriber Subscriber
   142  
   143  	// At any given time, there are at most two goroutines
   144  	// notifying the subscriber: a pending goroutine and an active goroutine.
   145  	pendingChange *ChangeSummary
   146  
   147  	// The active mutex is held by the goroutine currently notifying the
   148  	// subscriber. It may be held for a long time if the subscriber
   149  	// takes a long time.
   150  	activeMu sync.Mutex
   151  
   152  	// The state mutex is just for updating the hasPending/hasActive state.
   153  	// It should never be held a long time.
   154  	stateMu sync.Mutex
   155  }
   156  
   157  // Returns true if this is the pending goroutine.
   158  // Returns false to do nothing.
   159  // If there's a pending change, we merge the passed summary.
   160  func (e *subscriberEntry) claimPending(s ChangeSummary) bool {
   161  	e.stateMu.Lock()
   162  	defer e.stateMu.Unlock()
   163  
   164  	if e.pendingChange != nil {
   165  		e.pendingChange.Add(s)
   166  		return false
   167  	}
   168  	e.pendingChange = &ChangeSummary{}
   169  	e.pendingChange.Add(s)
   170  	return true
   171  }
   172  
   173  func (e *subscriberEntry) movePendingToActive() *ChangeSummary {
   174  	e.stateMu.Lock()
   175  	defer e.stateMu.Unlock()
   176  
   177  	activeChange := e.pendingChange
   178  	e.pendingChange = nil
   179  	return activeChange
   180  }
   181  
   182  // returns a string identifying the subscriber's type using its package + type name
   183  // e.g. "engine/uiresource.Subscriber"
   184  func subscriberName(sub Subscriber) string {
   185  	typ := reflect.TypeOf(sub)
   186  	if typ.Kind() == reflect.Ptr {
   187  		typ = typ.Elem()
   188  	}
   189  	return fmt.Sprintf("%s.%s", strings.TrimPrefix(typ.PkgPath(), "github.com/tilt-dev/tilt/internal/"), typ.Name())
   190  }
   191  
   192  func (e *subscriberEntry) notify(ctx context.Context, store *Store) {
   193  	e.activeMu.Lock()
   194  	defer e.activeMu.Unlock()
   195  
   196  	activeChange := e.movePendingToActive()
   197  	err := e.subscriber.OnChange(ctx, store, *activeChange)
   198  	if err == nil {
   199  		// Success! Finish immediately.
   200  		return
   201  	}
   202  
   203  	if ctx.Err() != nil {
   204  		// context finished
   205  		return
   206  	}
   207  
   208  	// Backoff on error
   209  	backoff := activeChange.LastBackoff * 2
   210  	if backoff == 0 {
   211  		backoff = time.Second
   212  		logger.Get(ctx).Debugf("Problem processing change. Subscriber: %s. Backing off %v. Error: %v", subscriberName(e.subscriber), backoff, err)
   213  	} else if backoff > MaxBackoff {
   214  		backoff = MaxBackoff
   215  		logger.Get(ctx).Errorf("Problem processing change. Subscriber: %s. Backing off %v. Error: %v", subscriberName(e.subscriber), backoff, err)
   216  	}
   217  	store.sleeper.Sleep(ctx, backoff)
   218  
   219  	activeChange.LastBackoff = backoff
   220  
   221  	// Requeue the active change.
   222  	isPending := e.claimPending(*activeChange)
   223  	if isPending {
   224  		SafeGo(store, func() {
   225  			e.notify(ctx, store)
   226  		})
   227  	}
   228  }
   229  
   230  func (e *subscriberEntry) maybeSetUp(ctx context.Context, st RStore) error {
   231  	s, ok := e.subscriber.(SetUpper)
   232  	if ok {
   233  		e.activeMu.Lock()
   234  		defer e.activeMu.Unlock()
   235  		return s.SetUp(ctx, st)
   236  	}
   237  	return nil
   238  }
   239  
   240  func (e *subscriberEntry) maybeTeardown(ctx context.Context) {
   241  	s, ok := e.subscriber.(TearDowner)
   242  	if ok {
   243  		e.activeMu.Lock()
   244  		defer e.activeMu.Unlock()
   245  		s.TearDown(ctx)
   246  	}
   247  }