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 }