This is an automated email from the ASF dual-hosted git repository.

bzp2010 pushed a commit to branch bzp/feat-refactor-adc-api
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/bzp/feat-refactor-adc-api by 
this push:
     new 4ec7f5da sperate standalone syncer policy layer
4ec7f5da is described below

commit 4ec7f5da997ade78e4821cb872d85959f93d56a9
Author: bzp2010 <[email protected]>
AuthorDate: Thu Sep 10 23:01:47 2026 +0800

    sperate standalone syncer policy layer
---
 internal/adc/client/standalone_syncer.go       | 120 +++++++++++++++++++++++++
 internal/provider/apisix/provider.go           |  94 ++++---------------
 internal/provider/apisix/provider_test.go      |   2 +-
 internal/provider/apisix/sync_baseline_test.go |   2 +-
 4 files changed, 139 insertions(+), 79 deletions(-)

diff --git a/internal/adc/client/standalone_syncer.go 
b/internal/adc/client/standalone_syncer.go
new file mode 100644
index 00000000..87cd0291
--- /dev/null
+++ b/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)
+
+       var report []error
+       if !in.Config.BypassCache && IsConfVersionRejection(err) {
+               s.log.Info("data plane rejected a stale conf_version, 
rebuilding the ADC baseline",
+                       "config", in.Name, "error", err.Error())
+               // The rebuild is not rate limited, so a rejection on every 
push (someone else
+               // writing to this data plane) turns every push into a full 
fetch and diff, and
+               // this counter is what says so.
+               pkgmetrics.RecordExecutionError(in.Name, 
"conf_version_conflict")
+
+               rejection := err
+               in.Config.BypassCache = true
+               err = s.client.Sync(ctx, in)
+
+               if err != nil && err.Error() != rejection.Error() {
+                       report = append(report, rejection)
+               }
+       }
+
+       // Only a push ADC accepted proves its baseline is now derived from the 
data plane.
+       if err == nil && in.Config.BypassCache {
+               s.markCurrent(in.Name)
+       }
+       if err != nil {
+               report = append(report, err)
+       }
+       return report
+}
diff --git a/internal/provider/apisix/provider.go 
b/internal/provider/apisix/provider.go
index 493aa2c4..f5ffa08c 100644
--- a/internal/provider/apisix/provider.go
+++ b/internal/provider/apisix/provider.go
@@ -43,7 +43,6 @@ import (
        "github.com/apache/apisix-ingress-controller/internal/provider/common"
        "github.com/apache/apisix-ingress-controller/internal/types"
        "github.com/apache/apisix-ingress-controller/internal/utils"
-       pkgmetrics "github.com/apache/apisix-ingress-controller/pkg/metrics"
 )
 
 const (
@@ -73,12 +72,10 @@ type apisixProvider struct {
        // snapshot together with pushing it
        syncLocks *keyedMutex
 
-       // rebuiltMu guards rebuiltBaselines.
-       rebuiltMu sync.Mutex
-       // rebuiltBaselines holds the cacheKeys whose ADC diff baseline this 
leadership term
-       // has already re-derived from the data plane. A key absent from it is 
pushed with
-       // BypassCache first. See invalidateBaselineCache.
-       rebuiltBaselines map[string]struct{}
+       // standaloneSyncer owns apisix-standalone's ADC diff-baseline 
recovery: the
+       // BypassCache decision, the one retry for a stale conf_version, and 
the per-term
+       // record of which cacheKeys it has rebuilt. Unused for every other 
backend type.
+       standaloneSyncer *adcclient.StandaloneSyncer
 
        updater         status.Updater
        statusUpdateMap map[types.NamespacedNameKind][]string
@@ -114,7 +111,7 @@ func New(log logr.Logger, updater status.Updater, readier 
readiness.ReadinessMan
                configManager:    configManager,
                debugProvider:    common.NewADCDebugProvider(store, 
configManager),
                syncLocks:        newKeyedMutex(),
-               rebuiltBaselines: make(map[string]struct{}),
+               standaloneSyncer: adcclient.NewStandaloneSyncer(cli, logger),
                Options:          o,
                translator:       translator.NewTranslator(log, 
o.ListenerPortMatchMode),
                updater:          updater,
@@ -327,35 +324,6 @@ func (d *apisixProvider) evictFromStore(
        return nil
 }
 
-// invalidateBaselineCache forgets which ADC diff baselines are known to be 
current, so
-// the next push of each cacheKey re-derives its baseline from the data plane.
-//
-// Called on leader acquisition, the one moment a stale baseline can enter the 
picture.
-// The ADC server is a sidecar that outlives the controller process: losing 
the lease
-// terminates the manager container but not the sidecar, so what ADC holds for 
a cacheKey
-// (the last synced content plus the conf_version it generated) can still be 
the snapshot
-// this pod left behind in an earlier term, while the leader in between kept 
pushing and
-// moved the data plane's conf_version past it. APISIX standalone requires 
those versions
-// to be monotonic and refuses the whole configuration otherwise.
-func (d *apisixProvider) invalidateBaselineCache() {
-       d.rebuiltMu.Lock()
-       defer d.rebuiltMu.Unlock()
-       clear(d.rebuiltBaselines)
-}
-
-func (d *apisixProvider) baselineIsCurrent(cacheKey string) bool {
-       d.rebuiltMu.Lock()
-       defer d.rebuiltMu.Unlock()
-       _, ok := d.rebuiltBaselines[cacheKey]
-       return ok
-}
-
-func (d *apisixProvider) markBaselineCurrent(cacheKey string) {
-       d.rebuiltMu.Lock()
-       defer d.rebuiltMu.Unlock()
-       d.rebuiltBaselines[cacheKey] = struct{}{}
-}
-
 // syncConfigNow reads name's current data (via build, called only once this 
cacheKey's
 // lock is actually held) and pushes it -- one atomic read-then-push step per 
cacheKey, so
 // whichever caller is granted the lock decides what to push only once it 
holds it: nothing
@@ -380,53 +348,25 @@ func (d *apisixProvider) syncConfigNow(
        return execErrs, nil
 }
 
-// pushConfig syncs input through the adc client once, and when 
apisix-standalone rejects
-// it over a stale conf_version, asks ADC to rebuild its diff baseline from 
the data plane
-// (SyncInput.Config.BypassCache) and syncs again. The adc client never 
retries on its
-// own: this is the one rejection AIC knows how to answer, so AIC owns both 
the decision
-// and the record of which baselines this leadership term has already rebuilt.
-//
-// invalidateBaselineCache on leader acquisition forces the first push of 
every cacheKey
-// this term to rebuild, which covers where staleness comes from. This retry 
is the safety
-// net for a desync no leadership change explains, such as another writer on 
the same data
-// plane, and a conf_version the data plane refuses is the only way that shows 
itself.
+// pushConfig sends input to its data plane and shapes whatever failed into 
the form
+// status reporting consumes. apisix-standalone goes through standaloneSyncer, 
which may
+// rebuild ADC's diff baseline and retry once; every other backend type is a 
single
+// one-shot push through the adc client, which never retries.
 func (d *apisixProvider) pushConfig(ctx context.Context, input 
adcclient.SyncInput) types.ADCExecutionErrors {
        backend := input.Config.BackendType
        if backend == "" {
                backend = d.DefaultBackendMode
        }
-       standalone := backend == adcclient.BackendAPISIXStandalone
 
-       input.Config.BypassCache = standalone && 
!d.baselineIsCurrent(input.Name)
-       err := d.client.Sync(ctx, input)
-
-       var execErrs types.ADCExecutionErrors
-       if standalone && !input.Config.BypassCache && 
adcclient.IsConfVersionRejection(err) {
-               d.log.Info("data plane rejected a stale conf_version, 
rebuilding the ADC baseline",
-                       "config", input.Name, "error", err.Error())
-               // The rebuild is not rate limited, so a rejection on every 
push (someone else writing
-               // to this data plane) turns every push into a full fetch and 
diff, and this counter
-               // is what says so.
-               pkgmetrics.RecordExecutionError(input.Name, 
"conf_version_conflict")
-
-               rejection := err
-               input.Config.BypassCache = true
-               err = d.client.Sync(ctx, input)
-
-               // Keep the rejection visible when the rebuild itself fails: on 
its own a failed
-               // rebuild points nowhere near what it was rebuilding for (an 
ADC server too old for
-               // bypassCache answers with a schema error). Unless the rebuild 
hit the very same
-               // rejection, where repeating it only pads the status message.
-               if err != nil && err.Error() != rejection.Error() {
-                       execErrs.Errors = append(execErrs.Errors, 
toADCExecutionError(input.Name, rejection))
-               }
+       var errs []error
+       if backend == adcclient.BackendAPISIXStandalone {
+               errs = d.standaloneSyncer.Sync(ctx, input)
+       } else if err := d.client.Sync(ctx, input); err != nil {
+               errs = []error{err}
        }
 
-       // Only a push ADC accepted proves its baseline is now derived from the 
data plane.
-       if err == nil && input.Config.BypassCache {
-               d.markBaselineCurrent(input.Name)
-       }
-       if err != nil {
+       var execErrs types.ADCExecutionErrors
+       for _, err := range errs {
                execErrs.Errors = append(execErrs.Errors, 
toADCExecutionError(input.Name, err))
        }
        return execErrs
@@ -490,7 +430,7 @@ func (d *apisixProvider) Start(ctx context.Context) error {
        // one thing that leaves the ADC sidecar holding a baseline from an 
earlier term: it
        // survives the manager container, the configuration it was derived 
from does not.
        // Rebuild every baseline from the data plane before syncing from it.
-       d.invalidateBaselineCache()
+       d.standaloneSyncer.InvalidateBaselines()
 
        d.log.Info("starting provider, waiting for readiness")
        d.readier.WaitReady(ctx, 5*time.Minute)
diff --git a/internal/provider/apisix/provider_test.go 
b/internal/provider/apisix/provider_test.go
index 599de304..66a91b7f 100644
--- a/internal/provider/apisix/provider_test.go
+++ b/internal/provider/apisix/provider_test.go
@@ -60,7 +60,7 @@ func newTestProvider(t *testing.T) *apisixProvider {
                store:            cache.NewStore(logr.Discard()),
                configManager:    
common.NewConfigManager[types.NamespacedNameKind, adctypes.Config](),
                syncLocks:        newKeyedMutex(),
-               rebuiltBaselines: make(map[string]struct{}),
+               standaloneSyncer: adcclient.NewStandaloneSyncer(cli, 
logr.Discard()),
                syncCh:           make(chan struct{}, 1),
                log:              logr.Discard(),
        }
diff --git a/internal/provider/apisix/sync_baseline_test.go 
b/internal/provider/apisix/sync_baseline_test.go
index f53bc120..093c9a71 100644
--- a/internal/provider/apisix/sync_baseline_test.go
+++ b/internal/provider/apisix/sync_baseline_test.go
@@ -114,7 +114,7 @@ func TestPushRebuildsBaselineOncePerTermThenReusesIt(t 
*testing.T) {
 
        require.Empty(t, d.pushConfig(context.Background(), in).Errors)
        require.Empty(t, d.pushConfig(context.Background(), in).Errors)
-       d.invalidateBaselineCache()
+       d.standaloneSyncer.InvalidateBaselines()
        require.Empty(t, d.pushConfig(context.Background(), in).Errors)
 
        assert.Equal(t, []bool{true, false, true}, bypassSeq(reqs()),

Reply via email to