sigs.k8s.io/cluster-api-provider-azure@v1.14.3/pkg/coalescing/reconciler.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package coalescing
    18  
    19  import (
    20  	"context"
    21  	"time"
    22  
    23  	"github.com/go-logr/logr"
    24  	"github.com/pkg/errors"
    25  	"sigs.k8s.io/cluster-api-provider-azure/util/cache/ttllru"
    26  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    27  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    28  )
    29  
    30  type (
    31  	// ReconcileCache uses and underlying time to live last recently used cache to track high frequency requests.
    32  	// A reconciler should call ShouldProcess to determine if the key has expired. If the key has expired, a zero value
    33  	// time.Time and true is returned. If the key has not expired, the expiration and false is returned. Upon successful
    34  	// reconciliation a reconciler should call Reconciled to update the cache expiry.
    35  	ReconcileCache struct {
    36  		lastSuccessfulReconciliationCache ttllru.PeekingCacher
    37  	}
    38  
    39  	// ReconcileCacher describes an interface for determining if a request should be reconciled through a call to
    40  	// ShouldProcess and if ok, reset the cool down through a call to Reconciled.
    41  	ReconcileCacher interface {
    42  		ShouldProcess(key string) (expiration time.Time, ok bool)
    43  		Reconciled(key string)
    44  	}
    45  
    46  	// reconciler is the caching reconciler middleware that uses the cache.
    47  	reconciler struct {
    48  		upstream reconcile.Reconciler
    49  		cache    ReconcileCacher
    50  		log      logr.Logger
    51  	}
    52  )
    53  
    54  // NewRequestCache creates a new instance of a ReconcileCache given a specified window of expiration.
    55  func NewRequestCache(window time.Duration) (*ReconcileCache, error) {
    56  	cache, err := ttllru.New(1024, window)
    57  	if err != nil {
    58  		return nil, errors.Wrap(err, "failed to build ttllru cache")
    59  	}
    60  
    61  	return &ReconcileCache{
    62  		lastSuccessfulReconciliationCache: cache,
    63  	}, nil
    64  }
    65  
    66  // ShouldProcess determines if the key has expired. If the key has expired, a zero value
    67  // time.Time and true is returned. If the key has not expired, the expiration and false is returned.
    68  func (cache *ReconcileCache) ShouldProcess(key string) (time.Time, bool) {
    69  	_, expiration, ok := cache.lastSuccessfulReconciliationCache.Peek(key)
    70  	return expiration, !ok
    71  }
    72  
    73  // Reconciled updates the cache expiry for a given key.
    74  func (cache *ReconcileCache) Reconciled(key string) {
    75  	cache.lastSuccessfulReconciliationCache.Add(key, nil)
    76  }
    77  
    78  // NewReconciler returns a reconcile wrapper that will delay new reconcile.Requests
    79  // after the cache expiry of the request string key.
    80  // A successful reconciliation is defined as one where no error is returned.
    81  func NewReconciler(upstream reconcile.Reconciler, cache ReconcileCacher, log logr.Logger) reconcile.Reconciler {
    82  	return &reconciler{
    83  		upstream: upstream,
    84  		cache:    cache,
    85  		log:      log.WithName("CoalescingReconciler"),
    86  	}
    87  }
    88  
    89  // Reconcile sends a request to the upstream reconciler if the request is outside of the debounce window.
    90  func (rc *reconciler) Reconcile(ctx context.Context, r reconcile.Request) (reconcile.Result, error) {
    91  	ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.reconciler.Reconcile",
    92  		tele.KVP("namespace", r.Namespace),
    93  		tele.KVP("name", r.Name),
    94  	)
    95  	defer done()
    96  
    97  	log = log.WithValues("request", r.String())
    98  
    99  	if expiration, ok := rc.cache.ShouldProcess(r.String()); !ok {
   100  		log.V(4).Info("not processing", "expiration", expiration, "timeUntil", time.Until(expiration))
   101  		var requeueAfter = time.Until(expiration)
   102  		if requeueAfter < 1*time.Second {
   103  			requeueAfter = 1 * time.Second
   104  		}
   105  		return reconcile.Result{RequeueAfter: requeueAfter}, nil
   106  	}
   107  
   108  	log.V(4).Info("processing")
   109  	result, err := rc.upstream.Reconcile(ctx, r)
   110  	if err != nil {
   111  		log.V(4).Info("not successful")
   112  		return result, err
   113  	}
   114  
   115  	log.V(4).Info("successful")
   116  	rc.cache.Reconciled(r.String())
   117  	return result, nil
   118  }