github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/rollout-service/pkg/service/dispatcher.go (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 17 package service 18 19 import ( 20 "context" 21 "sync" 22 "time" 23 24 "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 25 26 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 27 "github.com/cenkalti/backoff/v4" 28 "github.com/freiheit-com/kuberpult/pkg/setup" 29 "github.com/freiheit-com/kuberpult/services/rollout-service/pkg/versions" 30 ) 31 32 type knownRevision struct { 33 revision string 34 version *versions.VersionInfo 35 } 36 37 // The dispatcher is responsible for enriching argo events with version data from kuberpult. It also maintains a backlog of applications where adding this data failed. 38 // The backlog is retried frequently so that missing data eventually can be resolved. 39 type Dispatcher struct { 40 sink ArgoEventProcessor 41 versionClient versions.VersionClient 42 mx sync.Mutex 43 known map[Key]*knownRevision 44 unknown map[Key]*v1alpha1.ApplicationWatchEvent 45 unknownCh chan struct{} 46 backoff backoff.BackOff 47 } 48 49 func NewDispatcher(sink ArgoEventProcessor, vc versions.VersionClient) *Dispatcher { 50 bo := backoff.NewExponentialBackOff() 51 bo.MaxElapsedTime = 0 52 bo.MaxInterval = 5 * time.Minute 53 rs := &Dispatcher{ 54 mx: sync.Mutex{}, 55 sink: sink, 56 versionClient: vc, 57 known: map[Key]*knownRevision{}, 58 unknown: map[Key]*v1alpha1.ApplicationWatchEvent{}, 59 unknownCh: make(chan struct{}, 1), 60 backoff: bo, 61 } 62 return rs 63 } 64 65 func (r *Dispatcher) Dispatch(ctx context.Context, k Key, ev *v1alpha1.ApplicationWatchEvent) { 66 vs := r.tryResolve(ctx, k, ev) 67 if vs != nil { 68 r.sendEvent(ctx, k, vs, ev) 69 } 70 } 71 72 func (r *Dispatcher) tryResolve(ctx context.Context, k Key, ev *v1alpha1.ApplicationWatchEvent) *versions.VersionInfo { 73 r.mx.Lock() 74 defer r.mx.Unlock() 75 ddSpan, ctx := tracer.StartSpanFromContext(ctx, "tryResolve") 76 defer ddSpan.Finish() 77 revision := ev.Application.Status.Sync.Revision 78 ddSpan.SetTag("argoSyncRevision", revision) 79 // 0. Check if this is the delete event, if yes then we can delete the entry right away 80 if ev.Type == "DELETED" { 81 version := &versions.ZeroVersion 82 r.known[k] = &knownRevision{ 83 revision: revision, 84 version: version, 85 } 86 delete(r.unknown, k) 87 return version 88 } 89 // 1. Check if the revision has not changed 90 if vi := r.known[k]; vi != nil && vi.revision == revision { 91 delete(r.unknown, k) 92 return vi.version 93 } 94 // 2. Check if the versions client knows this version already 95 if version, err := r.versionClient.GetVersion(ctx, revision, k.Environment, k.Application); err == nil { 96 r.known[k] = &knownRevision{ 97 revision: revision, 98 version: version, 99 } 100 delete(r.unknown, k) 101 return version 102 } 103 // 3. Put this in the unknown queue and trigger the channel 104 r.unknown[k] = ev 105 select { 106 case r.unknownCh <- struct{}{}: 107 default: 108 } 109 return nil 110 } 111 112 func (r *Dispatcher) sendEvent(ctx context.Context, k Key, version *versions.VersionInfo, ev *v1alpha1.ApplicationWatchEvent) { 113 r.sink.ProcessArgoEvent(ctx, ArgoEvent{ 114 Application: k.Application, 115 Environment: k.Environment, 116 SyncStatusCode: ev.Application.Status.Sync.Status, 117 HealthStatusCode: ev.Application.Status.Health.Status, 118 OperationState: ev.Application.Status.OperationState, 119 Version: version, 120 }) 121 } 122 123 func (r *Dispatcher) Work(ctx context.Context, hlth *setup.HealthReporter) error { 124 hlth.ReportReady("dispatching") 125 bo := backoff.WithContext(r.backoff, ctx) 126 errored := false 127 for { 128 if errored { 129 errored = false 130 select { 131 case <-ctx.Done(): 132 return nil 133 case <-r.unknownCh: 134 case <-time.After(bo.NextBackOff()): 135 } 136 } else { 137 bo.Reset() 138 select { 139 case <-ctx.Done(): 140 return nil 141 case <-r.unknownCh: 142 } 143 } 144 145 keys := r.getUnknownKeys() 146 for _, k := range keys { 147 ev := r.getUnknown(k) 148 if ev == nil { 149 // The application was found in the meantime -> it's not unknown anymore 150 continue 151 } 152 revision := ev.Application.Status.Sync.Revision 153 version, err := r.versionClient.GetVersion(ctx, revision, k.Environment, k.Application) 154 if err != nil { 155 errored = true 156 continue 157 } 158 r.foundUnknown(ctx, k, ev, version) 159 } 160 } 161 } 162 163 func (r *Dispatcher) getUnknownKeys() []Key { 164 r.mx.Lock() 165 defer r.mx.Unlock() 166 keys := make([]Key, 0, len(r.unknown)) 167 for k := range r.unknown { 168 keys = append(keys, k) 169 } 170 return keys 171 } 172 func (r *Dispatcher) getUnknown(k Key) *v1alpha1.ApplicationWatchEvent { 173 r.mx.Lock() 174 defer r.mx.Unlock() 175 return r.unknown[k] 176 } 177 178 func (r *Dispatcher) foundUnknown(ctx context.Context, k Key, ev1 *v1alpha1.ApplicationWatchEvent, version *versions.VersionInfo) { 179 r.mx.Lock() 180 defer r.mx.Unlock() 181 // We need to recheck here if a new event was observed while we were waiting. 182 ev2 := r.unknown[k] 183 if ev2 == nil { 184 // Yes, there was a new event AND its version was resolved from cache. That means we don't need to do anything anymore. 185 return 186 } 187 revision1 := ev1.Application.Status.Sync.Revision 188 revision2 := ev2.Application.Status.Sync.Revision 189 if revision1 != revision2 { 190 // There was a new event AND its revision is different. We need to discard our version because it's for the wrong revision. 191 return 192 } 193 delete(r.unknown, k) 194 r.sendEvent(ctx, k, version, ev2) 195 }