Copilot commented on code in PR #2869:
URL:
https://github.com/apache/apisix-ingress-controller/pull/2869#discussion_r3985396423
##########
internal/adc/client/executor.go:
##########
@@ -150,40 +154,66 @@ func (e *HTTPADCExecutor) Validate(ctx context.Context,
config adctypes.Config,
return e.runHTTPValidate(ctx, config, resources, labels, resourceTypes)
}
-// runHTTPSync performs HTTP sync to ADC Server for each server address
+// runHTTPSync sends config in one /sync request and returns the parsed
failure, if any.
+// A sync is one request whatever config.ServerAddrs holds: apisix-standalone
joins every
+// entry with commas because ADC addresses them together as one logical
destination
+// (buildHTTPRequest splits them back apart into the request body), every
other backend
+// type takes the first entry only, since a GatewayProxy is expected to
resolve to one
+// address there even though nothing enforces it yet. Deciding how many
addresses a
+// GatewayProxy has belongs to the caller that built config.ServerAddrs.
+//
+// A GatewayProxy with no resolved address is a sync failure for
apisix-standalone (the
+// data plane it configures is unreachable, e.g. scaled to zero), and a no-op
for every
+// other backend type, which pushes per address and so has nothing to push.
+//
+// This package never decides whether to retry the failure; callers interpret
it and ask
+// again if they choose to.
func (e *HTTPADCExecutor) runHTTPSync(ctx context.Context, config
adctypes.Config, resources *adctypes.Resources, labels map[string]string,
resourceTypes []string) error {
- var execErrs = types.ADCExecutionError{
- Name: config.Name,
+ standalone := config.BackendType == BackendAPISIXStandalone
+ if len(config.ServerAddrs) == 0 {
+ if standalone {
+ return types.ADCExecutionServerAddrError{Err: "no data
plane address to sync apisix-standalone config to"}
+ }
+ return nil
}
- serverAddrs := func() []string {
- if config.BackendType == backendAPISIXStandalone {
- return []string{strings.Join(config.ServerAddrs, ",")}
+ target := syncTargetAddr(config)
+ e.log.V(1).Info("running http sync", "server", target)
+
+ ctx, cancel := context.WithTimeout(ctx, e.httpClient.Timeout)
+ defer cancel()
+
+ req, err := e.buildHTTPRequest(ctx, target, config, labels,
resourceTypes, resources, pathSync)
+ if err != nil {
+ return types.ADCExecutionServerAddrError{ServerAddr: target,
Err: fmt.Sprintf("failed to build HTTP request: %s", err)}
+ }
+
+ resp, err := e.httpClient.Do(req)
+ if err != nil {
+ return types.ADCExecutionServerAddrError{ServerAddr: target,
Err: fmt.Sprintf("failed to send HTTP request: %s", err)}
+ }
+ defer func() {
+ if closeErr := resp.Body.Close(); closeErr != nil {
+ e.log.Error(closeErr, "failed to close response body")
}
- return config.ServerAddrs
}()
- e.log.V(1).Info("running http sync", "serverAddrs", serverAddrs)
- for _, addr := range serverAddrs {
- if err := e.runHTTPSyncForSingleServer(ctx, addr, config,
resources, labels, resourceTypes); err != nil {
- e.log.Error(err, "failed to run http sync for server",
"server", addr)
- var execErr types.ADCExecutionServerAddrError
- if errors.As(err, &execErr) {
- execErrs.FailedErrors =
append(execErrs.FailedErrors, execErr)
- } else {
- execErrs.FailedErrors =
append(execErrs.FailedErrors, types.ADCExecutionServerAddrError{
- ServerAddr: addr,
- Err: err.Error(),
- })
- }
- }
- }
- if len(execErrs.FailedErrors) > 0 {
- return execErrs
+ if err := e.handleHTTPResponse(resp, target); err != nil {
+ e.log.Error(err, "failed to run http sync", "server", target)
+ return err
}
return nil
}
+// syncTargetAddr resolves config.ServerAddrs into what one /sync request
targets. Callers
+// must have already handled an empty ServerAddrs (see runHTTPSync).
+func syncTargetAddr(config adctypes.Config) string {
+ if config.BackendType == BackendAPISIXStandalone {
+ return strings.Join(config.ServerAddrs, ",")
+ }
+ return config.ServerAddrs[0]
Review Comment:
`ControlPlaneProvider.Endpoints` permits multiple entries
(`api/v1alpha1/gatewayproxy_types.go:129-132`), and the translator copies that
full list for every mode (`internal/adc/translator/gatewayproxy.go:94-97`).
Selecting only index 0 therefore silently stops syncing all later
non-standalone endpoints, whereas the previous loop sent one request to each.
Preserve the fan-out or reject multi-endpoint non-standalone configurations
before they reach this path.
##########
internal/adc/client/standalone_syncer.go:
##########
@@ -0,0 +1,120 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package client
+
+import (
+ "context"
+ "sync"
+
+ "github.com/go-logr/logr"
+
+ pkgmetrics "github.com/apache/apisix-ingress-controller/pkg/metrics"
+)
+
+// StandaloneSyncer drives apisix-standalone's diff-baseline recovery on top
of the
+// one-shot Client. It is only for apisix-standalone: no other backend type
keeps a
+// conf_version, so no other backend type needs any of this.
+//
+// APISIX standalone keeps a monotonic conf_version per resource type and
refuses a whole
+// push whose version is behind the data plane's. ADC diffs against a cached
baseline to
+// build that push, and the baseline can go stale two ways:
+//
+// - Across a leadership change. The ADC server is a sidecar that outlives
the manager
+// container, so what it holds for a cacheKey can be the snapshot this pod
left behind
+// in an earlier term, while the leader in between moved the data plane's
conf_version
+// past it. InvalidateBaselines on leader acquisition forces the first
push of every
+// cacheKey this term to re-derive its baseline from the data plane.
+// - Within a term, from a desync no leadership change explains, e.g.
another writer on
+// the same data plane. A conf_version the data plane refuses is the only
way that
+// shows itself; Sync answers it with one rebuild-and-retry.
+type StandaloneSyncer struct {
+ client *Client
+ log logr.Logger
+
+ mu sync.Mutex
+ rebuilt map[string]struct{}
+}
+
+func NewStandaloneSyncer(c *Client, log logr.Logger) *StandaloneSyncer {
+ return &StandaloneSyncer{
+ client: c,
+ log: log.WithName("standalone-syncer"),
+ rebuilt: make(map[string]struct{}),
+ }
+}
+
+// InvalidateBaselines forgets every rebuilt-this-term record, so the next
push of each
+// cacheKey re-derives its baseline. Call on leader acquisition.
+func (s *StandaloneSyncer) InvalidateBaselines() {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ clear(s.rebuilt)
+}
+
+func (s *StandaloneSyncer) isCurrent(cacheKey string) bool {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ _, ok := s.rebuilt[cacheKey]
+ return ok
+}
+
+func (s *StandaloneSyncer) markCurrent(cacheKey string) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.rebuilt[cacheKey] = struct{}{}
+}
+
+// Sync pushes in, rebuilding ADC's baseline first if this term has not yet
pushed this
+// cacheKey, and once more if the data plane rejects the push over a stale
conf_version.
+//
+// It returns every error worth reporting, so 0, 1, or 2 of them: the final
failure, plus
+// the conf_version rejection that triggered a rebuild which then failed for a
different
+// reason (on its own that failure points nowhere near its cause, e.g. an ADC
server too
+// old to know bypassCache answers with a schema error). BypassCache is scoped
to the call
+// that recovers from a rejection and never written back into in.
+func (s *StandaloneSyncer) Sync(ctx context.Context, in SyncInput) []error {
+ in.Config.BypassCache = !s.isCurrent(in.Name)
+ err := s.client.Sync(ctx, in)
Review Comment:
Each `Client.Sync` call records sync duration/count and execution-error
metrics before this layer decides whether the failure is recoverable. A
stale-conf_version rejection that the retry fixes now records one failed sync
plus `sync_failed`, then another successful sync, in addition to
`conf_version_conflict`; previously it produced one successful logical-sync
sample plus the conflict counter. This changes alerting/rates despite the PR's
metrics-equivalence claim. Record metrics once around the logical standalone
sync, or use an uninstrumented one-shot primitive and aggregate the final
outcome here.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]