istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/status/resourcelock.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package status 16 17 import ( 18 "context" 19 "strconv" 20 "sync" 21 22 "k8s.io/apimachinery/pkg/runtime/schema" 23 24 "istio.io/api/meta/v1alpha1" 25 "istio.io/istio/pkg/config" 26 "istio.io/istio/pkg/util/sets" 27 ) 28 29 // Task to be performed. 30 type Task func(entry cacheEntry) 31 32 // WorkerQueue implements an expandable goroutine pool which executes at most one concurrent routine per target 33 // resource. Multiple calls to Push() will not schedule multiple executions per target resource, but will ensure that 34 // the single execution uses the latest value. 35 type WorkerQueue interface { 36 // Push a task. 37 Push(target Resource, controller *Controller, context any) 38 // Run the loop until a signal on the context 39 Run(ctx context.Context) 40 // Delete a task 41 Delete(target Resource) 42 } 43 44 type cacheEntry struct { 45 // the cacheVale represents the latest version of the resource, including ResourceVersion 46 cacheResource Resource 47 // the perControllerStatus represents the latest version of the ResourceStatus 48 perControllerStatus map[*Controller]any 49 } 50 51 type lockResource struct { 52 schema.GroupVersionResource 53 Namespace string 54 Name string 55 } 56 57 func convert(i Resource) lockResource { 58 return lockResource{ 59 GroupVersionResource: i.GroupVersionResource, 60 Namespace: i.Namespace, 61 Name: i.Name, 62 } 63 } 64 65 type WorkQueue struct { 66 // tasks which are not currently executing but need to run 67 tasks []lockResource 68 // a lock to govern access to data in the cache 69 lock sync.Mutex 70 // for each task, a cacheEntry which can be updated before the task is run so that execution will have latest values 71 cache map[lockResource]cacheEntry 72 73 OnPush func() 74 } 75 76 func (wq *WorkQueue) Push(target Resource, ctl *Controller, progress any) { 77 wq.lock.Lock() 78 key := convert(target) 79 if item, inqueue := wq.cache[key]; inqueue { 80 item.perControllerStatus[ctl] = progress 81 wq.cache[key] = item 82 } else { 83 wq.cache[key] = cacheEntry{ 84 cacheResource: target, 85 perControllerStatus: map[*Controller]any{ctl: progress}, 86 } 87 wq.tasks = append(wq.tasks, key) 88 } 89 wq.lock.Unlock() 90 if wq.OnPush != nil { 91 wq.OnPush() 92 } 93 } 94 95 // Pop returns the first item in the queue not in exclusion, along with it's latest progress 96 func (wq *WorkQueue) Pop(exclusion sets.Set[lockResource]) (target Resource, progress map[*Controller]any) { 97 wq.lock.Lock() 98 defer wq.lock.Unlock() 99 for i := 0; i < len(wq.tasks); i++ { 100 if !exclusion.Contains(wq.tasks[i]) { 101 // remove from tasks 102 t, ok := wq.cache[wq.tasks[i]] 103 wq.tasks = append(wq.tasks[:i], wq.tasks[i+1:]...) 104 if !ok { 105 return Resource{}, nil 106 } 107 return t.cacheResource, t.perControllerStatus 108 } 109 } 110 return Resource{}, nil 111 } 112 113 func (wq *WorkQueue) Length() int { 114 wq.lock.Lock() 115 defer wq.lock.Unlock() 116 return len(wq.tasks) 117 } 118 119 func (wq *WorkQueue) Delete(target Resource) { 120 wq.lock.Lock() 121 defer wq.lock.Unlock() 122 delete(wq.cache, convert(target)) 123 } 124 125 type WorkerPool struct { 126 q WorkQueue 127 // indicates the queue is closing 128 closing bool 129 // the function which will be run for each task in queue 130 write func(*config.Config, any) 131 // the function to retrieve the initial status 132 get func(Resource) *config.Config 133 // current worker routine count 134 workerCount uint 135 // maximum worker routine count 136 maxWorkers uint 137 currentlyWorking sets.Set[lockResource] 138 lock sync.Mutex 139 } 140 141 func NewWorkerPool(write func(*config.Config, any), get func(Resource) *config.Config, maxWorkers uint) WorkerQueue { 142 return &WorkerPool{ 143 write: write, 144 get: get, 145 maxWorkers: maxWorkers, 146 currentlyWorking: sets.New[lockResource](), 147 q: WorkQueue{ 148 tasks: make([]lockResource, 0), 149 cache: make(map[lockResource]cacheEntry), 150 OnPush: nil, 151 }, 152 } 153 } 154 155 func (wp *WorkerPool) Delete(target Resource) { 156 wp.q.Delete(target) 157 } 158 159 func (wp *WorkerPool) Push(target Resource, controller *Controller, context any) { 160 wp.q.Push(target, controller, context) 161 wp.maybeAddWorker() 162 } 163 164 func (wp *WorkerPool) Run(ctx context.Context) { 165 context.AfterFunc(ctx, func() { 166 wp.lock.Lock() 167 wp.closing = true 168 wp.lock.Unlock() 169 }) 170 } 171 172 // maybeAddWorker adds a worker unless we are at maxWorkers. Workers exit when there are no more tasks, except for the 173 // last worker, which stays alive indefinitely. 174 func (wp *WorkerPool) maybeAddWorker() { 175 wp.lock.Lock() 176 if wp.workerCount >= wp.maxWorkers || wp.q.Length() == 0 { 177 wp.lock.Unlock() 178 return 179 } 180 wp.workerCount++ 181 wp.lock.Unlock() 182 go func() { 183 for { 184 wp.lock.Lock() 185 if wp.closing || wp.q.Length() == 0 { 186 wp.workerCount-- 187 wp.lock.Unlock() 188 return 189 } 190 191 target, perControllerWork := wp.q.Pop(wp.currentlyWorking) 192 193 if target == (Resource{}) { 194 // continue or return? 195 // could have been deleted, or could be no items in queue not currently worked on. need a way to differentiate. 196 wp.lock.Unlock() 197 continue 198 } 199 wp.q.Delete(target) 200 wp.currentlyWorking.Insert(convert(target)) 201 wp.lock.Unlock() 202 // work should be done without holding the lock 203 cfg := wp.get(target) 204 if cfg != nil { 205 // Check that generation matches 206 if strconv.FormatInt(cfg.Generation, 10) == target.Generation { 207 x, err := GetOGProvider(cfg.Status) 208 if err == nil { 209 // Not all controllers user generation, so we can ignore errors 210 x.SetObservedGeneration(cfg.Generation) 211 } 212 for c, i := range perControllerWork { 213 // TODO: this does not guarantee controller order. perhaps it should? 214 x = c.fn(x, i) 215 } 216 wp.write(cfg, x) 217 } 218 } 219 wp.lock.Lock() 220 wp.currentlyWorking.Delete(convert(target)) 221 wp.lock.Unlock() 222 } 223 }() 224 } 225 226 type GenerationProvider interface { 227 SetObservedGeneration(int64) 228 Unwrap() any 229 } 230 231 type IstioGenerationProvider struct { 232 *v1alpha1.IstioStatus 233 } 234 235 func (i *IstioGenerationProvider) SetObservedGeneration(in int64) { 236 i.ObservedGeneration = in 237 } 238 239 func (i *IstioGenerationProvider) Unwrap() any { 240 return i.IstioStatus 241 }