istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/delta.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package xds
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"strconv"
    21  	"strings"
    22  	"time"
    23  
    24  	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    25  	"google.golang.org/grpc/codes"
    26  	"google.golang.org/grpc/peer"
    27  	"google.golang.org/grpc/status"
    28  
    29  	"istio.io/istio/pilot/pkg/features"
    30  	istiogrpc "istio.io/istio/pilot/pkg/grpc"
    31  	"istio.io/istio/pilot/pkg/model"
    32  	"istio.io/istio/pilot/pkg/networking/util"
    33  	v3 "istio.io/istio/pilot/pkg/xds/v3"
    34  	istiolog "istio.io/istio/pkg/log"
    35  	"istio.io/istio/pkg/slices"
    36  	"istio.io/istio/pkg/util/sets"
    37  	"istio.io/istio/pkg/xds"
    38  )
    39  
    40  var deltaLog = istiolog.RegisterScope("delta", "delta xds debugging")
    41  
    42  func (s *DiscoveryServer) StreamDeltas(stream DeltaDiscoveryStream) error {
    43  	if knativeEnv != "" && firstRequest.Load() {
    44  		// How scaling works in knative is the first request is the "loading" request. During
    45  		// loading request, concurrency=1. Once that request is done, concurrency is enabled.
    46  		// However, the XDS stream is long lived, so the first request would block all others. As a
    47  		// result, we should exit the first request immediately; clients will retry.
    48  		firstRequest.Store(false)
    49  		return status.Error(codes.Unavailable, "server warmup not complete; try again")
    50  	}
    51  	// Check if server is ready to accept clients and process new requests.
    52  	// Currently ready means caches have been synced and hence can build
    53  	// clusters correctly. Without this check, InitContext() call below would
    54  	// initialize with empty config, leading to reconnected Envoys loosing
    55  	// configuration. This is an additional safety check inaddition to adding
    56  	// cachesSynced logic to readiness probe to handle cases where kube-proxy
    57  	// ip tables update latencies.
    58  	// See https://github.com/istio/istio/issues/25495.
    59  	if !s.IsServerReady() {
    60  		return errors.New("server is not ready to serve discovery information")
    61  	}
    62  
    63  	ctx := stream.Context()
    64  	peerAddr := "0.0.0.0"
    65  	if peerInfo, ok := peer.FromContext(ctx); ok {
    66  		peerAddr = peerInfo.Addr.String()
    67  	}
    68  
    69  	if err := s.WaitForRequestLimit(stream.Context()); err != nil {
    70  		deltaLog.Warnf("ADS: %q exceeded rate limit: %v", peerAddr, err)
    71  		return status.Errorf(codes.ResourceExhausted, "request rate limit exceeded: %v", err)
    72  	}
    73  
    74  	ids, err := s.authenticate(ctx)
    75  	if err != nil {
    76  		return status.Error(codes.Unauthenticated, err.Error())
    77  	}
    78  	if ids != nil {
    79  		deltaLog.Debugf("Authenticated XDS: %v with identity %v", peerAddr, ids)
    80  	} else {
    81  		deltaLog.Debugf("Unauthenticated XDS: %v", peerAddr)
    82  	}
    83  
    84  	// InitContext returns immediately if the context was already initialized.
    85  	if err = s.globalPushContext().InitContext(s.Env, nil, nil); err != nil {
    86  		// Error accessing the data - log and close, maybe a different pilot replica
    87  		// has more luck
    88  		deltaLog.Warnf("Error reading config %v", err)
    89  		return status.Error(codes.Unavailable, "error reading config")
    90  	}
    91  	con := newDeltaConnection(peerAddr, stream)
    92  
    93  	// Do not call: defer close(con.pushChannel). The push channel will be garbage collected
    94  	// when the connection is no longer used. Closing the channel can cause subtle race conditions
    95  	// with push. According to the spec: "It's only necessary to close a channel when it is important
    96  	// to tell the receiving goroutines that all data have been sent."
    97  
    98  	// Block until either a request is received or a push is triggered.
    99  	// We need 2 go routines because 'read' blocks in Recv().
   100  	go s.receiveDelta(con, ids)
   101  
   102  	// Wait for the proxy to be fully initialized before we start serving traffic. Because
   103  	// initialization doesn't have dependencies that will block, there is no need to add any timeout
   104  	// here. Prior to this explicit wait, we were implicitly waiting by receive() not sending to
   105  	// reqChannel and the connection not being enqueued for pushes to pushChannel until the
   106  	// initialization is complete.
   107  	<-con.InitializedCh()
   108  
   109  	for {
   110  		// Go select{} statements are not ordered; the same channel can be chosen many times.
   111  		// For requests, these are higher priority (client may be blocked on startup until these are done)
   112  		// and often very cheap to handle (simple ACK), so we check it first.
   113  		select {
   114  		case req, ok := <-con.deltaReqChan:
   115  			if ok {
   116  				if err := s.processDeltaRequest(req, con); err != nil {
   117  					return err
   118  				}
   119  			} else {
   120  				// Remote side closed connection or error processing the request.
   121  				return <-con.ErrorCh()
   122  			}
   123  		case <-con.StopCh():
   124  			return nil
   125  		default:
   126  		}
   127  		// If there wasn't already a request, poll for requests and pushes. Note: if we have a huge
   128  		// amount of incoming requests, we may still send some pushes, as we do not `continue` above;
   129  		// however, requests will be handled ~2x as much as pushes. This ensures a wave of requests
   130  		// cannot completely starve pushes. However, this scenario is unlikely.
   131  		select {
   132  		case req, ok := <-con.deltaReqChan:
   133  			if ok {
   134  				if err := s.processDeltaRequest(req, con); err != nil {
   135  					return err
   136  				}
   137  			} else {
   138  				// Remote side closed connection or error processing the request.
   139  				return <-con.ErrorCh()
   140  			}
   141  		case ev := <-con.PushCh():
   142  			pushEv := ev.(*Event)
   143  			err := s.pushConnectionDelta(con, pushEv)
   144  			pushEv.done()
   145  			if err != nil {
   146  				return err
   147  			}
   148  		case <-con.StopCh():
   149  			return nil
   150  		}
   151  	}
   152  }
   153  
   154  // Compute and send the new configuration for a connection.
   155  func (s *DiscoveryServer) pushConnectionDelta(con *Connection, pushEv *Event) error {
   156  	pushRequest := pushEv.pushRequest
   157  
   158  	if pushRequest.Full {
   159  		// Update Proxy with current information.
   160  		s.computeProxyState(con.proxy, pushRequest)
   161  	}
   162  
   163  	if !s.ProxyNeedsPush(con.proxy, pushRequest) {
   164  		deltaLog.Debugf("Skipping push to %v, no updates required", con.ID())
   165  		if pushRequest.Full {
   166  			// Only report for full versions, incremental pushes do not have a new version
   167  			reportAllEventsForProxyNoPush(con, s.StatusReporter, pushRequest.Push.LedgerVersion)
   168  		}
   169  		return nil
   170  	}
   171  
   172  	// Send pushes to all generators
   173  	// Each Generator is responsible for determining if the push event requires a push
   174  	wrl := con.watchedResourcesByOrder()
   175  	for _, w := range wrl {
   176  		if err := s.pushDeltaXds(con, w, pushRequest); err != nil {
   177  			return err
   178  		}
   179  	}
   180  
   181  	if pushRequest.Full {
   182  		// Report all events for unwatched resources. Watched resources will be reported in pushXds or on ack.
   183  		reportEventsForUnWatched(con, s.StatusReporter, pushRequest.Push.LedgerVersion)
   184  	}
   185  
   186  	proxiesConvergeDelay.Record(time.Since(pushRequest.Start).Seconds())
   187  	return nil
   188  }
   189  
   190  func (s *DiscoveryServer) receiveDelta(con *Connection, identities []string) {
   191  	defer func() {
   192  		close(con.deltaReqChan)
   193  		close(con.ErrorCh())
   194  		// Close the initialized channel, if its not already closed, to prevent blocking the stream
   195  		select {
   196  		case <-con.InitializedCh():
   197  		default:
   198  			close(con.InitializedCh())
   199  		}
   200  	}()
   201  	firstRequest := true
   202  	for {
   203  		req, err := con.deltaStream.Recv()
   204  		if err != nil {
   205  			if istiogrpc.IsExpectedGRPCError(err) {
   206  				deltaLog.Infof("ADS: %q %s terminated", con.Peer(), con.ID())
   207  				return
   208  			}
   209  			con.ErrorCh() <- err
   210  			deltaLog.Errorf("ADS: %q %s terminated with error: %v", con.Peer(), con.ID(), err)
   211  			xds.TotalXDSInternalErrors.Increment()
   212  			return
   213  		}
   214  		// This should be only set for the first request. The node id may not be set - for example malicious clients.
   215  		if firstRequest {
   216  			// probe happens before envoy sends first xDS request
   217  			if req.TypeUrl == v3.HealthInfoType {
   218  				log.Warnf("ADS: %q %s send health check probe before normal xDS request", con.Peer(), con.ID())
   219  				continue
   220  			}
   221  			firstRequest = false
   222  			if req.Node == nil || req.Node.Id == "" {
   223  				con.ErrorCh() <- status.New(codes.InvalidArgument, "missing node information").Err()
   224  				return
   225  			}
   226  			if err := s.initConnection(req.Node, con, identities); err != nil {
   227  				con.ErrorCh() <- err
   228  				return
   229  			}
   230  			defer s.closeConnection(con)
   231  			deltaLog.Infof("ADS: new delta connection for node:%s", con.ID())
   232  		}
   233  
   234  		select {
   235  		case con.deltaReqChan <- req:
   236  		case <-con.deltaStream.Context().Done():
   237  			deltaLog.Infof("ADS: %q %s terminated with stream closed", con.Peer(), con.ID())
   238  			return
   239  		}
   240  	}
   241  }
   242  
   243  func (conn *Connection) sendDelta(res *discovery.DeltaDiscoveryResponse, newResourceNames []string) error {
   244  	sendResonse := func() error {
   245  		start := time.Now()
   246  		defer func() { xds.RecordSendTime(time.Since(start)) }()
   247  		return conn.deltaStream.Send(res)
   248  	}
   249  	err := sendResonse()
   250  	if err == nil {
   251  		if !strings.HasPrefix(res.TypeUrl, v3.DebugType) {
   252  			conn.proxy.UpdateWatchedResource(res.TypeUrl, func(wr *model.WatchedResource) *model.WatchedResource {
   253  				if wr == nil {
   254  					wr = &model.WatchedResource{TypeUrl: res.TypeUrl}
   255  				}
   256  				// some resources dynamically update ResourceNames. Most don't though
   257  				if newResourceNames != nil {
   258  					wr.ResourceNames = newResourceNames
   259  				}
   260  				wr.NonceSent = res.Nonce
   261  				if features.EnableUnsafeDeltaTest {
   262  					wr.LastResources = applyDelta(wr.LastResources, res)
   263  				}
   264  				return wr
   265  			})
   266  		}
   267  	} else if status.Convert(err).Code() == codes.DeadlineExceeded {
   268  		deltaLog.Infof("Timeout writing %s: %v", conn.ID(), v3.GetShortType(res.TypeUrl))
   269  		xds.ResponseWriteTimeouts.Increment()
   270  	}
   271  	return err
   272  }
   273  
   274  // processDeltaRequest is handling one request. This is currently called from the 'main' thread, which also
   275  // handles 'push' requests and close - the code will eventually call the 'push' code, and it needs more mutex
   276  // protection. Original code avoided the mutexes by doing both 'push' and 'process requests' in same thread.
   277  func (s *DiscoveryServer) processDeltaRequest(req *discovery.DeltaDiscoveryRequest, con *Connection) error {
   278  	stype := v3.GetShortType(req.TypeUrl)
   279  	deltaLog.Debugf("ADS:%s: REQ %s resources sub:%d unsub:%d nonce:%s", stype,
   280  		con.ID(), len(req.ResourceNamesSubscribe), len(req.ResourceNamesUnsubscribe), req.ResponseNonce)
   281  
   282  	if req.TypeUrl == v3.HealthInfoType {
   283  		s.handleWorkloadHealthcheck(con.proxy, deltaToSotwRequest(req))
   284  		return nil
   285  	}
   286  	if strings.HasPrefix(req.TypeUrl, v3.DebugType) {
   287  		return s.pushDeltaXds(con,
   288  			&model.WatchedResource{TypeUrl: req.TypeUrl, ResourceNames: req.ResourceNamesSubscribe},
   289  			&model.PushRequest{Full: true, Push: con.proxy.LastPushContext})
   290  	}
   291  
   292  	if s.StatusReporter != nil {
   293  		s.StatusReporter.RegisterEvent(con.ID(), req.TypeUrl, req.ResponseNonce)
   294  	}
   295  
   296  	shouldRespond := s.shouldRespondDelta(con, req)
   297  	if !shouldRespond {
   298  		return nil
   299  	}
   300  
   301  	subs, _ := deltaWatchedResources(nil, req)
   302  	request := &model.PushRequest{
   303  		Full:   true,
   304  		Push:   con.proxy.LastPushContext,
   305  		Reason: model.NewReasonStats(model.ProxyRequest),
   306  
   307  		// The usage of LastPushTime (rather than time.Now()), is critical here for correctness; This time
   308  		// is used by the XDS cache to determine if a entry is stale. If we use Now() with an old push context,
   309  		// we may end up overriding active cache entries with stale ones.
   310  		Start: con.proxy.LastPushTime,
   311  		Delta: model.ResourceDelta{
   312  			// Record sub/unsub, but drop synthetic wildcard info
   313  			Subscribed:   sets.New(subs...),
   314  			Unsubscribed: sets.New(req.ResourceNamesUnsubscribe...).Delete("*"),
   315  		},
   316  	}
   317  	// SidecarScope for the proxy may has not been updated based on this pushContext.
   318  	// It can happen when `processRequest` comes after push context has been updated(s.initPushContext),
   319  	// but before proxy's SidecarScope has been updated(s.updateProxy).
   320  	if con.proxy.SidecarScope != nil && con.proxy.SidecarScope.Version != request.Push.PushVersion {
   321  		s.computeProxyState(con.proxy, request)
   322  	}
   323  
   324  	err := s.pushDeltaXds(con, con.proxy.GetWatchedResource(req.TypeUrl), request)
   325  	if err != nil {
   326  		return err
   327  	}
   328  	// Anytime we get a CDS request on reconnect, we should always push EDS as well.
   329  	// It is always the server's responsibility to send EDS after CDS, regardless if
   330  	// Envoy asks for it or not (See https://github.com/envoyproxy/envoy/issues/33607 for more details).
   331  	// Without this logic, there are cases where the clusters we send could stay warming forever,
   332  	// expecting an EDS response. Note that in SotW, Envoy sends an EDS request after the delayed
   333  	// CDS request; however, this is not guaranteed in delta, and has been observed to cause issues
   334  	// with EDS and SDS.
   335  	// This can happen with the following sequence
   336  	// 1. Envoy disconnects and reconnects to Istiod.
   337  	// 2. Envoy sends EDS request and we respond with it.
   338  	// 3. Envoy sends CDS request and we respond with clusters.
   339  	// 4. Envoy detects a change in cluster state and tries to warm those clusters but never sends
   340  	//    an EDS request for them.
   341  	// 5. Therefore, any initial CDS request should always trigger an EDS response
   342  	// 	  to let Envoy finish cluster warming.
   343  	// Refer to https://github.com/envoyproxy/envoy/issues/13009 for some more details on this type of issues.
   344  	if req.TypeUrl != v3.ClusterType {
   345  		return nil
   346  	}
   347  	return s.forceEDSPush(con)
   348  }
   349  
   350  func (s *DiscoveryServer) forceEDSPush(con *Connection) error {
   351  	if dwr := con.proxy.GetWatchedResource(v3.EndpointType); dwr != nil {
   352  		request := &model.PushRequest{
   353  			Full:   true,
   354  			Push:   con.proxy.LastPushContext,
   355  			Reason: model.NewReasonStats(model.DependentResource),
   356  			Start:  con.proxy.LastPushTime,
   357  		}
   358  		deltaLog.Infof("ADS:%s: FORCE %s PUSH for warming.", v3.GetShortType(v3.EndpointType), con.ID())
   359  		return s.pushDeltaXds(con, dwr, request)
   360  	}
   361  	return nil
   362  }
   363  
   364  // shouldRespondDelta determines whether this request needs to be responded back. It applies the ack/nack rules as per xds protocol
   365  // using WatchedResource for previous state and discovery request for the current state.
   366  func (s *DiscoveryServer) shouldRespondDelta(con *Connection, request *discovery.DeltaDiscoveryRequest) bool {
   367  	stype := v3.GetShortType(request.TypeUrl)
   368  
   369  	// If there is an error in request that means previous response is erroneous.
   370  	// We do not have to respond in that case. In this case request's version info
   371  	// will be different from the version sent. But it is fragile to rely on that.
   372  	if request.ErrorDetail != nil {
   373  		errCode := codes.Code(request.ErrorDetail.Code)
   374  		deltaLog.Warnf("ADS:%s: ACK ERROR %s %s:%s", stype, con.ID(), errCode.String(), request.ErrorDetail.GetMessage())
   375  		xds.IncrementXDSRejects(request.TypeUrl, con.proxy.ID, errCode.String())
   376  		return false
   377  	}
   378  
   379  	deltaLog.Debugf("ADS:%s REQUEST %v: sub:%v unsub:%v initial:%v", stype, con.ID(),
   380  		request.ResourceNamesSubscribe, request.ResourceNamesUnsubscribe, request.InitialResourceVersions)
   381  	previousInfo := con.proxy.GetWatchedResource(request.TypeUrl)
   382  
   383  	// This can happen in two cases:
   384  	// 1. Envoy initially send request to Istiod
   385  	// 2. Envoy reconnect to Istiod i.e. Istiod does not have
   386  	// information about this typeUrl, but Envoy sends response nonce - either
   387  	// because Istiod is restarted or Envoy disconnects and reconnects.
   388  	// We should always respond with the current resource names.
   389  	if previousInfo == nil {
   390  		con.proxy.Lock()
   391  		defer con.proxy.Unlock()
   392  
   393  		if len(request.InitialResourceVersions) > 0 {
   394  			deltaLog.Debugf("ADS:%s: RECONNECT %s %s resources:%v", stype, con.ID(), request.ResponseNonce, len(request.InitialResourceVersions))
   395  		} else {
   396  			deltaLog.Debugf("ADS:%s: INIT %s %s", stype, con.ID(), request.ResponseNonce)
   397  		}
   398  
   399  		res, wildcard := deltaWatchedResources(nil, request)
   400  		con.proxy.WatchedResources[request.TypeUrl] = &model.WatchedResource{
   401  			TypeUrl:       request.TypeUrl,
   402  			ResourceNames: res,
   403  			Wildcard:      wildcard,
   404  		}
   405  		return true
   406  	}
   407  
   408  	// If there is mismatch in the nonce, that is a case of expired/stale nonce.
   409  	// A nonce becomes stale following a newer nonce being sent to Envoy.
   410  	// TODO: due to concurrent unsubscribe, this probably doesn't make sense. Do we need any logic here?
   411  	if request.ResponseNonce != "" && request.ResponseNonce != previousInfo.NonceSent {
   412  		deltaLog.Debugf("ADS:%s: REQ %s Expired nonce received %s, sent %s", stype,
   413  			con.ID(), request.ResponseNonce, previousInfo.NonceSent)
   414  		xds.ExpiredNonce.With(typeTag.Value(v3.GetMetricType(request.TypeUrl))).Increment()
   415  		return false
   416  	}
   417  	// If it comes here, that means nonce match. This an ACK. We should record
   418  	// the ack details and respond if there is a change in resource names.
   419  	var previousResources, currentResources []string
   420  	var alwaysRespond bool
   421  	con.proxy.UpdateWatchedResource(request.TypeUrl, func(wr *model.WatchedResource) *model.WatchedResource {
   422  		previousResources = wr.ResourceNames
   423  		currentResources, _ = deltaWatchedResources(previousResources, request)
   424  		wr.NonceAcked = request.ResponseNonce
   425  		wr.ResourceNames = currentResources
   426  		alwaysRespond = wr.AlwaysRespond
   427  		wr.AlwaysRespond = false
   428  		return wr
   429  	})
   430  
   431  	subChanged := !slices.EqualUnordered(previousResources, currentResources)
   432  	// Spontaneous DeltaDiscoveryRequests from the client.
   433  	// This can be done to dynamically add or remove elements from the tracked resource_names set.
   434  	// In this case response_nonce is empty.
   435  	spontaneousReq := request.ResponseNonce == ""
   436  	// It is invalid in the below two cases:
   437  	// 1. no subscribed resources change from spontaneous delta request.
   438  	// 2. subscribed resources changes from ACK.
   439  	if spontaneousReq && !subChanged || !spontaneousReq && subChanged {
   440  		deltaLog.Errorf("ADS:%s: Subscribed resources check mismatch: %v vs %v", stype, spontaneousReq, subChanged)
   441  		if features.EnableUnsafeAssertions {
   442  			panic(fmt.Sprintf("ADS:%s: Subscribed resources check mismatch: %v vs %v", stype, spontaneousReq, subChanged))
   443  		}
   444  	}
   445  
   446  	// Envoy can send two DiscoveryRequests with same version and nonce
   447  	// when it detects a new resource. We should respond if they change.
   448  	if !subChanged {
   449  		// We should always respond "alwaysRespond" marked requests to let Envoy finish warming
   450  		// even though Nonce match and it looks like an ACK.
   451  		if alwaysRespond {
   452  			deltaLog.Infof("ADS:%s: FORCE RESPONSE %s for warming.", stype, con.ID())
   453  			return true
   454  		}
   455  
   456  		deltaLog.Debugf("ADS:%s: ACK %s %s", stype, con.ID(), request.ResponseNonce)
   457  		return false
   458  	}
   459  	deltaLog.Debugf("ADS:%s: RESOURCE CHANGE previous resources: %v, new resources: %v %s %s", stype,
   460  		previousResources, currentResources, con.ID(), request.ResponseNonce)
   461  
   462  	return true
   463  }
   464  
   465  // Push a Delta XDS resource for the given connection.
   466  func (s *DiscoveryServer) pushDeltaXds(con *Connection, w *model.WatchedResource, req *model.PushRequest) error {
   467  	if w == nil {
   468  		return nil
   469  	}
   470  	gen := s.findGenerator(w.TypeUrl, con)
   471  	if gen == nil {
   472  		return nil
   473  	}
   474  	t0 := time.Now()
   475  
   476  	originalW := w
   477  	// If delta is set, client is requesting new resources or removing old ones. We should just generate the
   478  	// new resources it needs, rather than the entire set of known resources.
   479  	// Note: we do not need to account for unsubscribed resources as these are handled by parent removal;
   480  	// See https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol#deleting-resources.
   481  	// This means if there are only removals, we will not respond.
   482  	var logFiltered string
   483  	if !req.Delta.IsEmpty() && !requiresResourceNamesModification(w.TypeUrl) {
   484  		// Some types opt out of this and natively handle req.Delta
   485  		logFiltered = " filtered:" + strconv.Itoa(len(w.ResourceNames)-len(req.Delta.Subscribed))
   486  		w = &model.WatchedResource{
   487  			TypeUrl:       w.TypeUrl,
   488  			ResourceNames: req.Delta.Subscribed.UnsortedList(),
   489  		}
   490  	}
   491  
   492  	var res model.Resources
   493  	var deletedRes model.DeletedResources
   494  	var logdata model.XdsLogDetails
   495  	var usedDelta bool
   496  	var err error
   497  	switch g := gen.(type) {
   498  	case model.XdsDeltaResourceGenerator:
   499  		res, deletedRes, logdata, usedDelta, err = g.GenerateDeltas(con.proxy, req, w)
   500  		if features.EnableUnsafeDeltaTest {
   501  			fullRes, l, _ := g.Generate(con.proxy, originalW, req)
   502  			s.compareDiff(con, originalW, fullRes, res, deletedRes, usedDelta, req.Delta, l.Incremental)
   503  		}
   504  	case model.XdsResourceGenerator:
   505  		res, logdata, err = g.Generate(con.proxy, w, req)
   506  	}
   507  	if err != nil || (res == nil && deletedRes == nil) {
   508  		// If we have nothing to send, report that we got an ACK for this version.
   509  		if s.StatusReporter != nil {
   510  			s.StatusReporter.RegisterEvent(con.ID(), w.TypeUrl, req.Push.LedgerVersion)
   511  		}
   512  		return err
   513  	}
   514  	defer func() { recordPushTime(w.TypeUrl, time.Since(t0)) }()
   515  	resp := &discovery.DeltaDiscoveryResponse{
   516  		ControlPlane: ControlPlane(),
   517  		TypeUrl:      w.TypeUrl,
   518  		// TODO: send different version for incremental eds
   519  		SystemVersionInfo: req.Push.PushVersion,
   520  		Nonce:             nonce(req.Push.LedgerVersion),
   521  		Resources:         res,
   522  	}
   523  	currentResources := slices.Map(res, func(r *discovery.Resource) string {
   524  		return r.Name
   525  	})
   526  	if usedDelta {
   527  		resp.RemovedResources = deletedRes
   528  	} else if req.Full {
   529  		// similar to sotw
   530  		subscribed := sets.New(w.ResourceNames...)
   531  		removed := subscribed.DeleteAll(currentResources...)
   532  		resp.RemovedResources = sets.SortedList(removed)
   533  	}
   534  	var newResourceNames []string
   535  	if shouldSetWatchedResources(w) {
   536  		// Set the new watched resources. Do not write to w directly, as it can be a copy from the 'filtered' logic above
   537  		if usedDelta {
   538  			// Apply the delta
   539  			newResourceNames = sets.SortedList(sets.New(w.ResourceNames...).
   540  				DeleteAll(resp.RemovedResources...).
   541  				InsertAll(currentResources...))
   542  		} else {
   543  			newResourceNames = currentResources
   544  		}
   545  	}
   546  	if neverRemoveDelta(w.TypeUrl) {
   547  		resp.RemovedResources = nil
   548  	}
   549  	if len(resp.RemovedResources) > 0 {
   550  		deltaLog.Debugf("ADS:%v REMOVE for node:%s %v", v3.GetShortType(w.TypeUrl), con.ID(), resp.RemovedResources)
   551  	}
   552  
   553  	configSize := ResourceSize(res)
   554  	configSizeBytes.With(typeTag.Value(w.TypeUrl)).Record(float64(configSize))
   555  
   556  	ptype := "PUSH"
   557  	info := ""
   558  	if logdata.Incremental {
   559  		ptype = "PUSH INC"
   560  	}
   561  	if len(logdata.AdditionalInfo) > 0 {
   562  		info = " " + logdata.AdditionalInfo
   563  	}
   564  	if len(logFiltered) > 0 {
   565  		info += logFiltered
   566  	}
   567  
   568  	if err := con.sendDelta(resp, newResourceNames); err != nil {
   569  		logger := deltaLog.Debugf
   570  		if recordSendError(w.TypeUrl, err) {
   571  			logger = deltaLog.Warnf
   572  		}
   573  		logger("%s: Send failure for node:%s resources:%d size:%s%s: %v",
   574  			v3.GetShortType(w.TypeUrl), con.proxy.ID, len(res), util.ByteCount(configSize), info, err)
   575  		return err
   576  	}
   577  
   578  	switch {
   579  	case !req.Full:
   580  		if deltaLog.DebugEnabled() {
   581  			deltaLog.Debugf("%s: %s%s for node:%s resources:%d size:%s%s",
   582  				v3.GetShortType(w.TypeUrl), ptype, req.PushReason(), con.proxy.ID, len(res), util.ByteCount(configSize), info)
   583  		}
   584  	default:
   585  		debug := ""
   586  		if deltaLog.DebugEnabled() {
   587  			// Add additional information to logs when debug mode enabled.
   588  			debug = " nonce:" + resp.Nonce + " version:" + resp.SystemVersionInfo
   589  		}
   590  		deltaLog.Infof("%s: %s%s for node:%s resources:%d removed:%d size:%v%s%s",
   591  			v3.GetShortType(w.TypeUrl), ptype, req.PushReason(), con.proxy.ID, len(res), len(resp.RemovedResources),
   592  			util.ByteCount(ResourceSize(res)), info, debug)
   593  	}
   594  
   595  	return nil
   596  }
   597  
   598  // requiresResourceNamesModification checks if a generator needs mutable access to w.ResourceNames.
   599  // This is used when resources are spontaneously pushed during Delta XDS
   600  func requiresResourceNamesModification(url string) bool {
   601  	return url == v3.AddressType || url == v3.WorkloadType
   602  }
   603  
   604  // neverRemoveDelta checks if a type should never remove resources
   605  func neverRemoveDelta(url string) bool {
   606  	// https://github.com/envoyproxy/envoy/issues/32823
   607  	// We want to garbage collect extensions when they are no longer referenced, rather than delete immediately
   608  	return url == v3.ExtensionConfigurationType
   609  }
   610  
   611  // shouldSetWatchedResources indicates whether we should set the watched resources for a given type.
   612  // for some type like `Address` we customly handle it in the generator
   613  func shouldSetWatchedResources(w *model.WatchedResource) bool {
   614  	if requiresResourceNamesModification(w.TypeUrl) {
   615  		// These handle it directly in the generator
   616  		return false
   617  	}
   618  	// Else fallback based on type
   619  	return xds.IsWildcardTypeURL(w.TypeUrl)
   620  }
   621  
   622  func newDeltaConnection(peerAddr string, stream DeltaDiscoveryStream) *Connection {
   623  	return &Connection{
   624  		Connection:   xds.NewConnection(peerAddr, nil),
   625  		deltaStream:  stream,
   626  		deltaReqChan: make(chan *discovery.DeltaDiscoveryRequest, 1),
   627  	}
   628  }
   629  
   630  // To satisfy methods that need DiscoveryRequest. Not suitable for real usage
   631  func deltaToSotwRequest(request *discovery.DeltaDiscoveryRequest) *discovery.DiscoveryRequest {
   632  	return &discovery.DiscoveryRequest{
   633  		Node:          request.Node,
   634  		ResourceNames: request.ResourceNamesSubscribe,
   635  		TypeUrl:       request.TypeUrl,
   636  		ResponseNonce: request.ResponseNonce,
   637  		ErrorDetail:   request.ErrorDetail,
   638  	}
   639  }
   640  
   641  // deltaWatchedResources returns current watched resources of delta xds
   642  func deltaWatchedResources(existing []string, request *discovery.DeltaDiscoveryRequest) ([]string, bool) {
   643  	res := sets.New(existing...)
   644  	res.InsertAll(request.ResourceNamesSubscribe...)
   645  	// This is set by Envoy on first request on reconnection so that we are aware of what Envoy knows
   646  	// and can continue the xDS session properly.
   647  	for k := range request.InitialResourceVersions {
   648  		res.Insert(k)
   649  	}
   650  	res.DeleteAll(request.ResourceNamesUnsubscribe...)
   651  	wildcard := false
   652  	// A request is wildcard if they explicitly subscribe to "*" or subscribe to nothing
   653  	if res.Contains("*") {
   654  		wildcard = true
   655  		res.Delete("*")
   656  	}
   657  	// "if the client sends a request but has never explicitly subscribed to any resource names, the
   658  	// server should treat that identically to how it would treat the client having explicitly
   659  	// subscribed to *"
   660  	// NOTE: this means you cannot subscribe to nothing, which is useful for on-demand loading; to workaround this
   661  	// Istio clients will send and initial request both subscribing+unsubscribing to `*`.
   662  	if len(request.ResourceNamesSubscribe) == 0 {
   663  		wildcard = true
   664  	}
   665  	return res.UnsortedList(), wildcard
   666  }