google.golang.org/grpc@v1.72.2/balancer/rls/picker.go (about)

     1  /*
     2   *
     3   * Copyright 2022 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  package rls
    20  
    21  import (
    22  	"errors"
    23  	"fmt"
    24  	"strings"
    25  	"sync/atomic"
    26  	"time"
    27  
    28  	"google.golang.org/grpc/balancer"
    29  	"google.golang.org/grpc/balancer/rls/internal/keys"
    30  	"google.golang.org/grpc/codes"
    31  	"google.golang.org/grpc/connectivity"
    32  	estats "google.golang.org/grpc/experimental/stats"
    33  	internalgrpclog "google.golang.org/grpc/internal/grpclog"
    34  	rlspb "google.golang.org/grpc/internal/proto/grpc_lookup_v1"
    35  	"google.golang.org/grpc/metadata"
    36  	"google.golang.org/grpc/status"
    37  )
    38  
    39  var (
    40  	errRLSThrottled = errors.New("RLS call throttled at client side")
    41  
    42  	// Function to compute data cache entry size.
    43  	computeDataCacheEntrySize = dcEntrySize
    44  )
    45  
    46  // exitIdler wraps the only method on the BalancerGroup that the picker calls.
    47  type exitIdler interface {
    48  	ExitIdleOne(id string)
    49  }
    50  
    51  // rlsPicker selects the subConn to be used for a particular RPC. It does not
    52  // manage subConns directly and delegates to pickers provided by child policies.
    53  type rlsPicker struct {
    54  	// The keyBuilder map used to generate RLS keys for the RPC. This is built
    55  	// by the LB policy based on the received ServiceConfig.
    56  	kbm keys.BuilderMap
    57  	// Endpoint from the user's original dial target. Used to set the `host_key`
    58  	// field in `extra_keys`.
    59  	origEndpoint string
    60  
    61  	lb *rlsBalancer
    62  
    63  	// The picker is given its own copy of the below fields from the RLS LB policy
    64  	// to avoid having to grab the mutex on the latter.
    65  	rlsServerTarget string
    66  	grpcTarget      string
    67  	metricsRecorder estats.MetricsRecorder
    68  	defaultPolicy   *childPolicyWrapper // Child policy for the default target.
    69  	ctrlCh          *controlChannel     // Control channel to the RLS server.
    70  	maxAge          time.Duration       // Cache max age from LB config.
    71  	staleAge        time.Duration       // Cache stale age from LB config.
    72  	bg              exitIdler
    73  	logger          *internalgrpclog.PrefixLogger
    74  }
    75  
    76  // isFullMethodNameValid return true if name is of the form `/service/method`.
    77  func isFullMethodNameValid(name string) bool {
    78  	return strings.HasPrefix(name, "/") && strings.Count(name, "/") == 2
    79  }
    80  
    81  // Pick makes the routing decision for every outbound RPC.
    82  func (p *rlsPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
    83  	if name := info.FullMethodName; !isFullMethodNameValid(name) {
    84  		return balancer.PickResult{}, fmt.Errorf("rls: method name %q is not of the form '/service/method", name)
    85  	}
    86  
    87  	// Build the request's keys using the key builders from LB config.
    88  	md, _ := metadata.FromOutgoingContext(info.Ctx)
    89  	reqKeys := p.kbm.RLSKey(md, p.origEndpoint, info.FullMethodName)
    90  
    91  	p.lb.cacheMu.Lock()
    92  	var pr balancer.PickResult
    93  	var err error
    94  
    95  	// Record metrics without the cache mutex held, to prevent lock contention
    96  	// between concurrent RPC's and their Pick calls. Metrics Recording can
    97  	// potentially be expensive.
    98  	metricsCallback := func() {}
    99  	defer func() {
   100  		p.lb.cacheMu.Unlock()
   101  		metricsCallback()
   102  	}()
   103  
   104  	// Lookup data cache and pending request map using request path and keys.
   105  	cacheKey := cacheKey{path: info.FullMethodName, keys: reqKeys.Str}
   106  	dcEntry := p.lb.dataCache.getEntry(cacheKey)
   107  	pendingEntry := p.lb.pendingMap[cacheKey]
   108  	now := time.Now()
   109  
   110  	switch {
   111  	// No data cache entry. No pending request.
   112  	case dcEntry == nil && pendingEntry == nil:
   113  		throttled := p.sendRouteLookupRequestLocked(cacheKey, &backoffState{bs: defaultBackoffStrategy}, reqKeys.Map, rlspb.RouteLookupRequest_REASON_MISS, "")
   114  		if throttled {
   115  			pr, metricsCallback, err = p.useDefaultPickIfPossible(info, errRLSThrottled)
   116  			return pr, err
   117  		}
   118  		return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
   119  
   120  	// No data cache entry. Pending request exits.
   121  	case dcEntry == nil && pendingEntry != nil:
   122  		return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
   123  
   124  	// Data cache hit. No pending request.
   125  	case dcEntry != nil && pendingEntry == nil:
   126  		if dcEntry.expiryTime.After(now) {
   127  			if !dcEntry.staleTime.IsZero() && dcEntry.staleTime.Before(now) && dcEntry.backoffTime.Before(now) {
   128  				p.sendRouteLookupRequestLocked(cacheKey, dcEntry.backoffState, reqKeys.Map, rlspb.RouteLookupRequest_REASON_STALE, dcEntry.headerData)
   129  			}
   130  			// Delegate to child policies.
   131  			pr, metricsCallback, err = p.delegateToChildPoliciesLocked(dcEntry, info)
   132  			return pr, err
   133  		}
   134  
   135  		// We get here only if the data cache entry has expired. If entry is in
   136  		// backoff, delegate to default target or fail the pick.
   137  		if dcEntry.backoffState != nil && dcEntry.backoffTime.After(now) {
   138  			// Avoid propagating the status code received on control plane RPCs to the
   139  			// data plane which can lead to unexpected outcomes as we do not control
   140  			// the status code sent by the control plane. Propagating the status
   141  			// message received from the control plane is still fine, as it could be
   142  			// useful for debugging purposes.
   143  			st := dcEntry.status
   144  			pr, metricsCallback, err = p.useDefaultPickIfPossible(info, status.Error(codes.Unavailable, fmt.Sprintf("most recent error from RLS server: %v", st.Error())))
   145  			return pr, err
   146  		}
   147  
   148  		// We get here only if the entry has expired and is not in backoff.
   149  		throttled := p.sendRouteLookupRequestLocked(cacheKey, dcEntry.backoffState, reqKeys.Map, rlspb.RouteLookupRequest_REASON_MISS, "")
   150  		if throttled {
   151  			pr, metricsCallback, err = p.useDefaultPickIfPossible(info, errRLSThrottled)
   152  			return pr, err
   153  		}
   154  		return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
   155  
   156  	// Data cache hit. Pending request exists.
   157  	default:
   158  		if dcEntry.expiryTime.After(now) {
   159  			pr, metricsCallback, err = p.delegateToChildPoliciesLocked(dcEntry, info)
   160  			return pr, err
   161  		}
   162  		// Data cache entry has expired and pending request exists. Queue pick.
   163  		return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
   164  	}
   165  }
   166  
   167  // errToPickResult is a helper function which converts the error value returned
   168  // by Pick() to a string that represents the pick result.
   169  func errToPickResult(err error) string {
   170  	if err == nil {
   171  		return "complete"
   172  	}
   173  	if errors.Is(err, balancer.ErrNoSubConnAvailable) {
   174  		return "queue"
   175  	}
   176  	if _, ok := status.FromError(err); ok {
   177  		return "drop"
   178  	}
   179  	return "fail"
   180  }
   181  
   182  // delegateToChildPoliciesLocked is a helper function which iterates through the
   183  // list of child policy wrappers in a cache entry and attempts to find a child
   184  // policy to which this RPC can be routed to. If all child policies are in
   185  // TRANSIENT_FAILURE, we delegate to the last child policy arbitrarily. Returns
   186  // a function to be invoked to record metrics.
   187  func (p *rlsPicker) delegateToChildPoliciesLocked(dcEntry *cacheEntry, info balancer.PickInfo) (balancer.PickResult, func(), error) {
   188  	const rlsDataHeaderName = "x-google-rls-data"
   189  	for i, cpw := range dcEntry.childPolicyWrappers {
   190  		state := (*balancer.State)(atomic.LoadPointer(&cpw.state))
   191  		// Delegate to the child policy if it is not in TRANSIENT_FAILURE, or if
   192  		// it is the last one (which handles the case of delegating to the last
   193  		// child picker if all child policies are in TRANSIENT_FAILURE).
   194  		if state.ConnectivityState != connectivity.TransientFailure || i == len(dcEntry.childPolicyWrappers)-1 {
   195  			// Any header data received from the RLS server is stored in the
   196  			// cache entry and needs to be sent to the actual backend in the
   197  			// X-Google-RLS-Data header.
   198  			res, err := state.Picker.Pick(info)
   199  			if err != nil {
   200  				pr := errToPickResult(err)
   201  				return res, func() {
   202  					if pr == "queue" {
   203  						// Don't record metrics for queued Picks.
   204  						return
   205  					}
   206  					targetPicksMetric.Record(p.metricsRecorder, 1, p.grpcTarget, p.rlsServerTarget, cpw.target, pr)
   207  				}, err
   208  			}
   209  
   210  			if res.Metadata == nil {
   211  				res.Metadata = metadata.Pairs(rlsDataHeaderName, dcEntry.headerData)
   212  			} else {
   213  				res.Metadata.Append(rlsDataHeaderName, dcEntry.headerData)
   214  			}
   215  			return res, func() {
   216  				targetPicksMetric.Record(p.metricsRecorder, 1, p.grpcTarget, p.rlsServerTarget, cpw.target, "complete")
   217  			}, nil
   218  		}
   219  	}
   220  
   221  	// In the unlikely event that we have a cache entry with no targets, we end up
   222  	// queueing the RPC.
   223  	return balancer.PickResult{}, func() {}, balancer.ErrNoSubConnAvailable
   224  }
   225  
   226  // useDefaultPickIfPossible is a helper method which delegates to the default
   227  // target if one is configured, or fails the pick with the given error. Returns
   228  // a function to be invoked to record metrics.
   229  func (p *rlsPicker) useDefaultPickIfPossible(info balancer.PickInfo, errOnNoDefault error) (balancer.PickResult, func(), error) {
   230  	if p.defaultPolicy != nil {
   231  		state := (*balancer.State)(atomic.LoadPointer(&p.defaultPolicy.state))
   232  		res, err := state.Picker.Pick(info)
   233  		pr := errToPickResult(err)
   234  		return res, func() {
   235  			if pr == "queue" {
   236  				// Don't record metrics for queued Picks.
   237  				return
   238  			}
   239  			defaultTargetPicksMetric.Record(p.metricsRecorder, 1, p.grpcTarget, p.rlsServerTarget, p.defaultPolicy.target, pr)
   240  		}, err
   241  	}
   242  
   243  	return balancer.PickResult{}, func() {
   244  		failedPicksMetric.Record(p.metricsRecorder, 1, p.grpcTarget, p.rlsServerTarget)
   245  	}, errOnNoDefault
   246  }
   247  
   248  // sendRouteLookupRequestLocked adds an entry to the pending request map and
   249  // sends out an RLS request using the passed in arguments. Returns a value
   250  // indicating if the request was throttled by the client-side adaptive
   251  // throttler.
   252  func (p *rlsPicker) sendRouteLookupRequestLocked(cacheKey cacheKey, bs *backoffState, reqKeys map[string]string, reason rlspb.RouteLookupRequest_Reason, staleHeaders string) bool {
   253  	if p.lb.pendingMap[cacheKey] != nil {
   254  		return false
   255  	}
   256  
   257  	p.lb.pendingMap[cacheKey] = bs
   258  	throttled := p.ctrlCh.lookup(reqKeys, reason, staleHeaders, func(targets []string, headerData string, err error) {
   259  		p.handleRouteLookupResponse(cacheKey, targets, headerData, err)
   260  	})
   261  	if throttled {
   262  		delete(p.lb.pendingMap, cacheKey)
   263  	}
   264  	return throttled
   265  }
   266  
   267  // handleRouteLookupResponse is the callback invoked by the control channel upon
   268  // receipt of an RLS response. Modifies the data cache and pending requests map
   269  // and sends a new picker.
   270  //
   271  // Acquires the write-lock on the cache. Caller must not hold p.lb.cacheMu.
   272  func (p *rlsPicker) handleRouteLookupResponse(cacheKey cacheKey, targets []string, headerData string, err error) {
   273  	p.logger.Infof("Received RLS response for key %+v with targets %+v, headerData %q, err: %v", cacheKey, targets, headerData, err)
   274  
   275  	p.lb.cacheMu.Lock()
   276  	defer func() {
   277  		// Pending request map entry is unconditionally deleted since the request is
   278  		// no longer pending.
   279  		p.logger.Infof("Removing pending request entry for key %+v", cacheKey)
   280  		delete(p.lb.pendingMap, cacheKey)
   281  		p.lb.sendNewPicker()
   282  		p.lb.cacheMu.Unlock()
   283  	}()
   284  
   285  	// Lookup the data cache entry or create a new one.
   286  	dcEntry := p.lb.dataCache.getEntry(cacheKey)
   287  	if dcEntry == nil {
   288  		dcEntry = &cacheEntry{}
   289  		if _, ok := p.lb.dataCache.addEntry(cacheKey, dcEntry); !ok {
   290  			// This is a very unlikely case where we are unable to add a
   291  			// data cache entry. Log and leave.
   292  			p.logger.Warningf("Failed to add data cache entry for %+v", cacheKey)
   293  			return
   294  		}
   295  	}
   296  
   297  	// For failed requests, the data cache entry is modified as follows:
   298  	// - status is set to error returned from the control channel
   299  	// - current backoff state is available in the pending entry
   300  	//   - `retries` field is incremented and
   301  	//   - backoff state is moved to the data cache
   302  	// - backoffTime is set to the time indicated by the backoff state
   303  	// - backoffExpirationTime is set to twice the backoff time
   304  	// - backoffTimer is set to fire after backoffTime
   305  	//
   306  	// When a proactive cache refresh fails, this would leave the targets and the
   307  	// expiry time from the old entry unchanged. And this mean that the old valid
   308  	// entry would be used until expiration, and a new picker would be sent upon
   309  	// backoff expiry.
   310  	now := time.Now()
   311  
   312  	// "An RLS request is considered to have failed if it returns a non-OK
   313  	// status or the RLS response's targets list is non-empty." - RLS LB Policy
   314  	// design.
   315  	if len(targets) == 0 && err == nil {
   316  		err = fmt.Errorf("RLS response's target list does not contain any entries for key %+v", cacheKey)
   317  		// If err is set, rpc error from the control plane and no control plane
   318  		// configuration is why no targets were passed into this helper, no need
   319  		// to specify and tell the user this information.
   320  	}
   321  	if err != nil {
   322  		dcEntry.status = err
   323  		pendingEntry := p.lb.pendingMap[cacheKey]
   324  		pendingEntry.retries++
   325  		backoffTime := pendingEntry.bs.Backoff(pendingEntry.retries)
   326  		dcEntry.backoffState = pendingEntry
   327  		dcEntry.backoffTime = now.Add(backoffTime)
   328  		dcEntry.backoffExpiryTime = now.Add(2 * backoffTime)
   329  		if dcEntry.backoffState.timer != nil {
   330  			dcEntry.backoffState.timer.Stop()
   331  		}
   332  		dcEntry.backoffState.timer = time.AfterFunc(backoffTime, p.lb.sendNewPicker)
   333  		return
   334  	}
   335  
   336  	// For successful requests, the cache entry is modified as follows:
   337  	// - childPolicyWrappers is set to point to the child policy wrappers
   338  	//   associated with the targets specified in the received response
   339  	// - headerData is set to the value received in the response
   340  	// - expiryTime, stateTime and earliestEvictionTime are set
   341  	// - status is set to nil (OK status)
   342  	// - backoff state is cleared
   343  	p.setChildPolicyWrappersInCacheEntry(dcEntry, targets)
   344  	dcEntry.headerData = headerData
   345  	dcEntry.expiryTime = now.Add(p.maxAge)
   346  	if p.staleAge != 0 {
   347  		dcEntry.staleTime = now.Add(p.staleAge)
   348  	}
   349  	dcEntry.earliestEvictTime = now.Add(minEvictDuration)
   350  	dcEntry.status = nil
   351  	dcEntry.backoffState = &backoffState{bs: defaultBackoffStrategy}
   352  	dcEntry.backoffTime = time.Time{}
   353  	dcEntry.backoffExpiryTime = time.Time{}
   354  	p.lb.dataCache.updateEntrySize(dcEntry, computeDataCacheEntrySize(cacheKey, dcEntry))
   355  }
   356  
   357  // setChildPolicyWrappersInCacheEntry sets up the childPolicyWrappers field in
   358  // the cache entry to point to the child policy wrappers for the targets
   359  // specified in the RLS response.
   360  //
   361  // Caller must hold a write-lock on p.lb.cacheMu.
   362  func (p *rlsPicker) setChildPolicyWrappersInCacheEntry(dcEntry *cacheEntry, newTargets []string) {
   363  	// If the childPolicyWrappers field is already pointing to the right targets,
   364  	// then the field's value does not need to change.
   365  	targetsChanged := true
   366  	func() {
   367  		if cpws := dcEntry.childPolicyWrappers; cpws != nil {
   368  			if len(newTargets) != len(cpws) {
   369  				return
   370  			}
   371  			for i, target := range newTargets {
   372  				if cpws[i].target != target {
   373  					return
   374  				}
   375  			}
   376  			targetsChanged = false
   377  		}
   378  	}()
   379  	if !targetsChanged {
   380  		return
   381  	}
   382  
   383  	// If the childPolicyWrappers field is not already set to the right targets,
   384  	// then it must be reset. We construct a new list of child policies and
   385  	// then swap out the old list for the new one.
   386  	newChildPolicies := p.lb.acquireChildPolicyReferences(newTargets)
   387  	oldChildPolicyTargets := make([]string, len(dcEntry.childPolicyWrappers))
   388  	for i, cpw := range dcEntry.childPolicyWrappers {
   389  		oldChildPolicyTargets[i] = cpw.target
   390  	}
   391  	p.lb.releaseChildPolicyReferences(oldChildPolicyTargets)
   392  	dcEntry.childPolicyWrappers = newChildPolicies
   393  }
   394  
   395  func dcEntrySize(key cacheKey, entry *cacheEntry) int64 {
   396  	return int64(len(key.path) + len(key.keys) + len(entry.headerData))
   397  }