Make webhook-based scale race-free (#1477)
* Make webhook-based scale operation asynchronous This prevents race condition in the webhook-based autoscaler when it received another webhook event while processing another webhook event and both ended up scaling up the same horizontal runner autoscaler. Ref #1321 * Fix typos * Update rather than Patch HRA to avoid race among webhook-based autoscaler servers * Batch capacity reservation updates for efficient use of apiserver * Fix potential never-ending HRA update conflicts in batch update * Extract batchScaler out of webhook-based autoscaler for testability * Fix log levels and batch scaler hang on start * Correlate webhook event with scale trigger amount in logs * Fix log message
This commit is contained in:
		
							parent
							
								
									84d16c1c12
								
							
						
					
					
						commit
						e2c8163b8c
					
				|  | @ -72,6 +72,7 @@ func main() { | ||||||
| 		enableLeaderElection bool | 		enableLeaderElection bool | ||||||
| 		syncPeriod           time.Duration | 		syncPeriod           time.Duration | ||||||
| 		logLevel             string | 		logLevel             string | ||||||
|  | 		queueLimit           int | ||||||
| 
 | 
 | ||||||
| 		ghClient *github.Client | 		ghClient *github.Client | ||||||
| 	) | 	) | ||||||
|  | @ -92,6 +93,7 @@ func main() { | ||||||
| 		"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") | 		"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") | ||||||
| 	flag.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, "Determines the minimum frequency at which K8s resources managed by this controller are reconciled. When you use autoscaling, set to a lower value like 10 minute, because this corresponds to the minimum time to react on demand change") | 	flag.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, "Determines the minimum frequency at which K8s resources managed by this controller are reconciled. When you use autoscaling, set to a lower value like 10 minute, because this corresponds to the minimum time to react on demand change") | ||||||
| 	flag.StringVar(&logLevel, "log-level", logging.LogLevelDebug, `The verbosity of the logging. Valid values are "debug", "info", "warn", "error". Defaults to "debug".`) | 	flag.StringVar(&logLevel, "log-level", logging.LogLevelDebug, `The verbosity of the logging. Valid values are "debug", "info", "warn", "error". Defaults to "debug".`) | ||||||
|  | 	flag.IntVar(&queueLimit, "queue-limit", controllers.DefaultQueueLimit, `The maximum length of the scale operation queue. The scale opration is enqueued per every matching webhook event, and the server returns a 500 HTTP status when the queue was already full on enqueue attempt.`) | ||||||
| 	flag.StringVar(&webhookSecretToken, "github-webhook-secret-token", "", "The personal access token of GitHub.") | 	flag.StringVar(&webhookSecretToken, "github-webhook-secret-token", "", "The personal access token of GitHub.") | ||||||
| 	flag.StringVar(&c.Token, "github-token", c.Token, "The personal access token of GitHub.") | 	flag.StringVar(&c.Token, "github-token", c.Token, "The personal access token of GitHub.") | ||||||
| 	flag.Int64Var(&c.AppID, "github-app-id", c.AppID, "The application ID of GitHub App.") | 	flag.Int64Var(&c.AppID, "github-app-id", c.AppID, "The application ID of GitHub App.") | ||||||
|  | @ -164,6 +166,7 @@ func main() { | ||||||
| 		SecretKeyBytes: []byte(webhookSecretToken), | 		SecretKeyBytes: []byte(webhookSecretToken), | ||||||
| 		Namespace:      watchNamespace, | 		Namespace:      watchNamespace, | ||||||
| 		GitHubClient:   ghClient, | 		GitHubClient:   ghClient, | ||||||
|  | 		QueueLimit:     queueLimit, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err = hraGitHubWebhook.SetupWithManager(mgr); err != nil { | 	if err = hraGitHubWebhook.SetupWithManager(mgr); err != nil { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,207 @@ | ||||||
|  | package controllers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1" | ||||||
|  | 	"github.com/go-logr/logr" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type batchScaler struct { | ||||||
|  | 	Ctx      context.Context | ||||||
|  | 	Client   client.Client | ||||||
|  | 	Log      logr.Logger | ||||||
|  | 	interval time.Duration | ||||||
|  | 
 | ||||||
|  | 	queue       chan *ScaleTarget | ||||||
|  | 	workerStart sync.Once | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newBatchScaler(ctx context.Context, client client.Client, log logr.Logger) *batchScaler { | ||||||
|  | 	return &batchScaler{ | ||||||
|  | 		Ctx:      ctx, | ||||||
|  | 		Client:   client, | ||||||
|  | 		Log:      log, | ||||||
|  | 		interval: 3 * time.Second, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type batchScaleOperation struct { | ||||||
|  | 	namespacedName types.NamespacedName | ||||||
|  | 	scaleOps       []scaleOperation | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type scaleOperation struct { | ||||||
|  | 	trigger v1alpha1.ScaleUpTrigger | ||||||
|  | 	log     logr.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Add the scale target to the unbounded queue, blocking until the target is successfully added to the queue.
 | ||||||
|  | // All the targets in the queue are dequeued every 3 seconds, grouped by the HRA, and applied.
 | ||||||
|  | // In a happy path, batchScaler update each HRA only once, even though the HRA had two or more associated webhook events in the 3 seconds interval,
 | ||||||
|  | // which results in less K8s API calls and less HRA update conflicts in case your ARC installation receives a lot of webhook events
 | ||||||
|  | func (s *batchScaler) Add(st *ScaleTarget) { | ||||||
|  | 	if st == nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	s.workerStart.Do(func() { | ||||||
|  | 		var expBackoff = []time.Duration{time.Second, 2 * time.Second, 4 * time.Second, 8 * time.Second, 16 * time.Second} | ||||||
|  | 
 | ||||||
|  | 		s.queue = make(chan *ScaleTarget) | ||||||
|  | 
 | ||||||
|  | 		log := s.Log | ||||||
|  | 
 | ||||||
|  | 		go func() { | ||||||
|  | 			log.Info("Starting batch worker") | ||||||
|  | 			defer log.Info("Stopped batch worker") | ||||||
|  | 
 | ||||||
|  | 			for { | ||||||
|  | 				select { | ||||||
|  | 				case <-s.Ctx.Done(): | ||||||
|  | 					return | ||||||
|  | 				default: | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				log.V(2).Info("Batch worker is dequeueing operations") | ||||||
|  | 
 | ||||||
|  | 				batches := map[types.NamespacedName]batchScaleOperation{} | ||||||
|  | 				after := time.After(s.interval) | ||||||
|  | 				var ops uint | ||||||
|  | 
 | ||||||
|  | 			batch: | ||||||
|  | 				for { | ||||||
|  | 					select { | ||||||
|  | 					case <-after: | ||||||
|  | 						after = nil | ||||||
|  | 						break batch | ||||||
|  | 					case st := <-s.queue: | ||||||
|  | 						nsName := types.NamespacedName{ | ||||||
|  | 							Namespace: st.HorizontalRunnerAutoscaler.Namespace, | ||||||
|  | 							Name:      st.HorizontalRunnerAutoscaler.Name, | ||||||
|  | 						} | ||||||
|  | 						b, ok := batches[nsName] | ||||||
|  | 						if !ok { | ||||||
|  | 							b = batchScaleOperation{ | ||||||
|  | 								namespacedName: nsName, | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 						b.scaleOps = append(b.scaleOps, scaleOperation{ | ||||||
|  | 							log:     *st.log, | ||||||
|  | 							trigger: st.ScaleUpTrigger, | ||||||
|  | 						}) | ||||||
|  | 						batches[nsName] = b | ||||||
|  | 						ops++ | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				log.V(2).Info("Batch worker dequeued operations", "ops", ops, "batches", len(batches)) | ||||||
|  | 
 | ||||||
|  | 			retry: | ||||||
|  | 				for i := 0; ; i++ { | ||||||
|  | 					failed := map[types.NamespacedName]batchScaleOperation{} | ||||||
|  | 
 | ||||||
|  | 					for nsName, b := range batches { | ||||||
|  | 						b := b | ||||||
|  | 						if err := s.batchScale(context.Background(), b); err != nil { | ||||||
|  | 							log.V(2).Info("Failed to scale due to error", "error", err) | ||||||
|  | 							failed[nsName] = b | ||||||
|  | 						} else { | ||||||
|  | 							log.V(2).Info("Successfully ran batch scale", "hra", b.namespacedName) | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					if len(failed) == 0 { | ||||||
|  | 						break retry | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					batches = failed | ||||||
|  | 
 | ||||||
|  | 					delay := 16 * time.Second | ||||||
|  | 					if i < len(expBackoff) { | ||||||
|  | 						delay = expBackoff[i] | ||||||
|  | 					} | ||||||
|  | 					time.Sleep(delay) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	s.queue <- st | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *batchScaler) batchScale(ctx context.Context, batch batchScaleOperation) error { | ||||||
|  | 	var hra v1alpha1.HorizontalRunnerAutoscaler | ||||||
|  | 
 | ||||||
|  | 	if err := s.Client.Get(ctx, batch.namespacedName, &hra); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	copy := hra.DeepCopy() | ||||||
|  | 
 | ||||||
|  | 	copy.Spec.CapacityReservations = getValidCapacityReservations(copy) | ||||||
|  | 
 | ||||||
|  | 	var added, completed int | ||||||
|  | 
 | ||||||
|  | 	for _, scale := range batch.scaleOps { | ||||||
|  | 		amount := 1 | ||||||
|  | 
 | ||||||
|  | 		if scale.trigger.Amount != 0 { | ||||||
|  | 			amount = scale.trigger.Amount | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		scale.log.V(2).Info("Adding capacity reservation", "amount", amount) | ||||||
|  | 
 | ||||||
|  | 		if amount > 0 { | ||||||
|  | 			now := time.Now() | ||||||
|  | 			copy.Spec.CapacityReservations = append(copy.Spec.CapacityReservations, v1alpha1.CapacityReservation{ | ||||||
|  | 				EffectiveTime:  metav1.Time{Time: now}, | ||||||
|  | 				ExpirationTime: metav1.Time{Time: now.Add(scale.trigger.Duration.Duration)}, | ||||||
|  | 				Replicas:       amount, | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			added += amount | ||||||
|  | 		} else if amount < 0 { | ||||||
|  | 			var reservations []v1alpha1.CapacityReservation | ||||||
|  | 
 | ||||||
|  | 			var found bool | ||||||
|  | 
 | ||||||
|  | 			for _, r := range copy.Spec.CapacityReservations { | ||||||
|  | 				if !found && r.Replicas+amount == 0 { | ||||||
|  | 					found = true | ||||||
|  | 				} else { | ||||||
|  | 					reservations = append(reservations, r) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			copy.Spec.CapacityReservations = reservations | ||||||
|  | 
 | ||||||
|  | 			completed += amount | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	before := len(hra.Spec.CapacityReservations) | ||||||
|  | 	expired := before - len(copy.Spec.CapacityReservations) | ||||||
|  | 	after := len(copy.Spec.CapacityReservations) | ||||||
|  | 
 | ||||||
|  | 	s.Log.V(1).Info( | ||||||
|  | 		fmt.Sprintf("Updating hra %s for capacityReservations update", hra.Name), | ||||||
|  | 		"before", before, | ||||||
|  | 		"expired", expired, | ||||||
|  | 		"added", added, | ||||||
|  | 		"completed", completed, | ||||||
|  | 		"after", after, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	if err := s.Client.Update(ctx, copy); err != nil { | ||||||
|  | 		return fmt.Errorf("updating horizontalrunnerautoscaler to add capacity reservation: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -23,9 +23,9 @@ import ( | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |  | ||||||
| 	"k8s.io/apimachinery/pkg/types" | 	"k8s.io/apimachinery/pkg/types" | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/reconcile" | 	"sigs.k8s.io/controller-runtime/pkg/reconcile" | ||||||
| 
 | 
 | ||||||
|  | @ -46,6 +46,8 @@ const ( | ||||||
| 
 | 
 | ||||||
| 	keyPrefixEnterprise = "enterprises/" | 	keyPrefixEnterprise = "enterprises/" | ||||||
| 	keyRunnerGroup      = "/group/" | 	keyRunnerGroup      = "/group/" | ||||||
|  | 
 | ||||||
|  | 	DefaultQueueLimit = 100 | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // HorizontalRunnerAutoscalerGitHubWebhook autoscales a HorizontalRunnerAutoscaler and the RunnerDeployment on each
 | // HorizontalRunnerAutoscalerGitHubWebhook autoscales a HorizontalRunnerAutoscaler and the RunnerDeployment on each
 | ||||||
|  | @ -68,6 +70,15 @@ type HorizontalRunnerAutoscalerGitHubWebhook struct { | ||||||
| 	// Set to empty for letting it watch for all namespaces.
 | 	// Set to empty for letting it watch for all namespaces.
 | ||||||
| 	Namespace string | 	Namespace string | ||||||
| 	Name      string | 	Name      string | ||||||
|  | 
 | ||||||
|  | 	// QueueLimit is the maximum length of the bounded queue of scale targets and their associated operations
 | ||||||
|  | 	// A scale target is enqueued on each retrieval of each eligible webhook event, so that it is processed asynchronously.
 | ||||||
|  | 	QueueLimit int | ||||||
|  | 
 | ||||||
|  | 	worker      *worker | ||||||
|  | 	workerInit  sync.Once | ||||||
|  | 	workerStart sync.Once | ||||||
|  | 	batchCh     chan *ScaleTarget | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Reconcile(_ context.Context, request reconcile.Request) (reconcile.Result, error) { | func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Reconcile(_ context.Context, request reconcile.Request) (reconcile.Result, error) { | ||||||
|  | @ -312,9 +323,19 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := autoscaler.tryScale(context.TODO(), target); err != nil { | 	autoscaler.workerInit.Do(func() { | ||||||
| 		log.Error(err, "could not scale up") | 		batchScaler := newBatchScaler(context.Background(), autoscaler.Client, autoscaler.Log) | ||||||
| 
 | 
 | ||||||
|  | 		queueLimit := autoscaler.QueueLimit | ||||||
|  | 		if queueLimit == 0 { | ||||||
|  | 			queueLimit = DefaultQueueLimit | ||||||
|  | 		} | ||||||
|  | 		autoscaler.worker = newWorker(context.Background(), queueLimit, batchScaler.Add) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	target.log = &log | ||||||
|  | 	if ok := autoscaler.worker.Add(target); !ok { | ||||||
|  | 		log.Error(err, "Could not scale up due to queue full") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -383,6 +404,8 @@ func matchTriggerConditionAgainstEvent(types []string, eventAction *string) bool | ||||||
| type ScaleTarget struct { | type ScaleTarget struct { | ||||||
| 	v1alpha1.HorizontalRunnerAutoscaler | 	v1alpha1.HorizontalRunnerAutoscaler | ||||||
| 	v1alpha1.ScaleUpTrigger | 	v1alpha1.ScaleUpTrigger | ||||||
|  | 
 | ||||||
|  | 	log *logr.Logger | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) searchScaleTargets(hras []v1alpha1.HorizontalRunnerAutoscaler, f func(v1alpha1.ScaleUpTrigger) bool) []ScaleTarget { | func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) searchScaleTargets(hras []v1alpha1.HorizontalRunnerAutoscaler, f func(v1alpha1.ScaleUpTrigger) bool) []ScaleTarget { | ||||||
|  | @ -770,63 +793,6 @@ HRA: | ||||||
| 	return nil, nil | 	return nil, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) tryScale(ctx context.Context, target *ScaleTarget) error { |  | ||||||
| 	if target == nil { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	copy := target.HorizontalRunnerAutoscaler.DeepCopy() |  | ||||||
| 
 |  | ||||||
| 	amount := 1 |  | ||||||
| 
 |  | ||||||
| 	if target.ScaleUpTrigger.Amount != 0 { |  | ||||||
| 		amount = target.ScaleUpTrigger.Amount |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	capacityReservations := getValidCapacityReservations(copy) |  | ||||||
| 
 |  | ||||||
| 	if amount > 0 { |  | ||||||
| 		now := time.Now() |  | ||||||
| 		copy.Spec.CapacityReservations = append(capacityReservations, v1alpha1.CapacityReservation{ |  | ||||||
| 			EffectiveTime:  metav1.Time{Time: now}, |  | ||||||
| 			ExpirationTime: metav1.Time{Time: now.Add(target.ScaleUpTrigger.Duration.Duration)}, |  | ||||||
| 			Replicas:       amount, |  | ||||||
| 		}) |  | ||||||
| 	} else if amount < 0 { |  | ||||||
| 		var reservations []v1alpha1.CapacityReservation |  | ||||||
| 
 |  | ||||||
| 		var found bool |  | ||||||
| 
 |  | ||||||
| 		for _, r := range capacityReservations { |  | ||||||
| 			if !found && r.Replicas+amount == 0 { |  | ||||||
| 				found = true |  | ||||||
| 			} else { |  | ||||||
| 				reservations = append(reservations, r) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		copy.Spec.CapacityReservations = reservations |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	before := len(target.HorizontalRunnerAutoscaler.Spec.CapacityReservations) |  | ||||||
| 	expired := before - len(capacityReservations) |  | ||||||
| 	after := len(copy.Spec.CapacityReservations) |  | ||||||
| 
 |  | ||||||
| 	autoscaler.Log.V(1).Info( |  | ||||||
| 		fmt.Sprintf("Patching hra %s for capacityReservations update", target.HorizontalRunnerAutoscaler.Name), |  | ||||||
| 		"before", before, |  | ||||||
| 		"expired", expired, |  | ||||||
| 		"amount", amount, |  | ||||||
| 		"after", after, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	if err := autoscaler.Client.Patch(ctx, copy, client.MergeFrom(&target.HorizontalRunnerAutoscaler)); err != nil { |  | ||||||
| 		return fmt.Errorf("patching horizontalrunnerautoscaler to add capacity reservation: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func getValidCapacityReservations(autoscaler *v1alpha1.HorizontalRunnerAutoscaler) []v1alpha1.CapacityReservation { | func getValidCapacityReservations(autoscaler *v1alpha1.HorizontalRunnerAutoscaler) []v1alpha1.CapacityReservation { | ||||||
| 	var capacityReservations []v1alpha1.CapacityReservation | 	var capacityReservations []v1alpha1.CapacityReservation | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,55 @@ | ||||||
|  | package controllers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // worker is a worker that has a non-blocking bounded queue of scale targets, dequeues scale target and executes the scale operation one by one.
 | ||||||
|  | type worker struct { | ||||||
|  | 	scaleTargetQueue chan *ScaleTarget | ||||||
|  | 	work             func(*ScaleTarget) | ||||||
|  | 	done             chan struct{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newWorker(ctx context.Context, queueLimit int, work func(*ScaleTarget)) *worker { | ||||||
|  | 	w := &worker{ | ||||||
|  | 		scaleTargetQueue: make(chan *ScaleTarget, queueLimit), | ||||||
|  | 		work:             work, | ||||||
|  | 		done:             make(chan struct{}), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	go func() { | ||||||
|  | 		defer close(w.done) | ||||||
|  | 
 | ||||||
|  | 		for { | ||||||
|  | 			select { | ||||||
|  | 			case <-ctx.Done(): | ||||||
|  | 				return | ||||||
|  | 			case t := <-w.scaleTargetQueue: | ||||||
|  | 				work(t) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	return w | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Add the scale target to the bounded queue, returning the result as a bool value. It returns true on successful enqueue, and returns false otherwise.
 | ||||||
|  | // When returned false, the queue is already full so the enqueue operation must be retried later.
 | ||||||
|  | // If the enqueue was triggered by an external source and there's no intermediate queue that we can use,
 | ||||||
|  | // you must instruct the source to resend the original request later.
 | ||||||
|  | // In case you're building a webhook server around this worker, this means that you must return a http error to the webhook server,
 | ||||||
|  | // so that (hopefully) the sender can resend the webhook event later, or at least the human operator can notice or be notified about the
 | ||||||
|  | // webhook develiery failure so that a manual retry can be done later.
 | ||||||
|  | func (w *worker) Add(st *ScaleTarget) bool { | ||||||
|  | 	select { | ||||||
|  | 	case w.scaleTargetQueue <- st: | ||||||
|  | 		return true | ||||||
|  | 	default: | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (w *worker) Done() chan struct{} { | ||||||
|  | 	return w.done | ||||||
|  | } | ||||||
|  | @ -0,0 +1,36 @@ | ||||||
|  | package controllers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/require" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestWorker_Add(t *testing.T) { | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  | 	defer cancel() | ||||||
|  | 
 | ||||||
|  | 	w := newWorker(ctx, 2, func(st *ScaleTarget) {}) | ||||||
|  | 	require.True(t, w.Add(&ScaleTarget{})) | ||||||
|  | 	require.True(t, w.Add(&ScaleTarget{})) | ||||||
|  | 	require.False(t, w.Add(&ScaleTarget{})) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestWorker_Work(t *testing.T) { | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  | 	defer cancel() | ||||||
|  | 
 | ||||||
|  | 	var count int | ||||||
|  | 
 | ||||||
|  | 	w := newWorker(ctx, 1, func(st *ScaleTarget) { | ||||||
|  | 		count++ | ||||||
|  | 		cancel() | ||||||
|  | 	}) | ||||||
|  | 	require.True(t, w.Add(&ScaleTarget{})) | ||||||
|  | 	require.False(t, w.Add(&ScaleTarget{})) | ||||||
|  | 
 | ||||||
|  | 	<-w.Done() | ||||||
|  | 
 | ||||||
|  | 	require.Equal(t, count, 1) | ||||||
|  | } | ||||||
|  | @ -1367,7 +1367,7 @@ func (env *testEnvironment) ExpectRegisteredNumberCountEventuallyEquals(want int | ||||||
| 
 | 
 | ||||||
| 			return len(rs) | 			return len(rs) | ||||||
| 		}, | 		}, | ||||||
| 		time.Second*5, time.Millisecond*500).Should(Equal(want), optionalDescriptions...) | 		time.Second*10, time.Millisecond*500).Should(Equal(want), optionalDescriptions...) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (env *testEnvironment) SendOrgPullRequestEvent(org, repo, branch, action string) { | func (env *testEnvironment) SendOrgPullRequestEvent(org, repo, branch, action string) { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue