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 }