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  }