AlinsRan commented on code in PR #2890:
URL: 
https://github.com/apache/apisix-ingress-controller/pull/2890#discussion_r4119135842


##########
internal/provider/apisix/status.go:
##########
@@ -137,13 +176,50 @@ func (d *apisixProvider) classifySyncResult(
        return gatewayProxyMsgs, failedEndpoints
 }
 
+// dropUnit maps a rejected resource to what has to be dropped for the rest to 
apply. A
+// service is dropped by itself. A route, stream route or named upstream is 
dropped along
+// with the service it lives in: the service is what one Kubernetes rule 
translates to,
+// and a named upstream is referenced by the service's traffic-split by id, so 
dropping
+// it alone would leave that reference dangling. Nested resources are found 
through their
+// parent, which also tells them apart when several services embed upstreams 
of the same
+// id.
+func (d *apisixProvider) dropUnit(configName string, ev adctypes.StatusEvent) 
(wireKey, exclusion, bool) {

Review Comment:
   `dropUnit` treats every `failed[]` entry as a content rejection, but on the 
`apisix` backend ADC also reports transient failures there. Right now a single 
5xx or transport error can permanently exclude a valid service and delete it 
from the data plane.
   
   **Where transient failures come from (ADC master `63a0937`)**
   
   The ADC server runs sync with `exit_on_failure: false`. A failed event does 
not abort the sync. It is returned in `failed[]` with its full `event`, and the 
response is still a 202.
   
   ```rust
   // adc: rust/crates/adc-cli/src/server/sync.rs
   let sync_opts = BackendSyncOptions {
       concurrent: Some(opts.request_concurrent),
       exit_on_failure: Some(false),
   };
   ```
   
   ```ts
   // adc: apps/cli/src/server/sync.ts (the Node server, same behavior)
   backend.sync(diff, { exitOnFailure: false, concurrent: 4 })
   ```
   
   The apisix backend only retries 5xx and transport errors briefly, then gives 
up and reports the event as failed:
   
   ```rust
   // adc: rust/crates/adc-backend-core/src/retry.rs
   fn default() -> Self {
       Self { retries: 3, delay: Duration::from_millis(100) }
   }
   ```
   
   The Admin API returns `503` for any etcd write error:
   
   ```lua
   -- apisix: apisix/admin/resource.lua
   local res, err = core.etcd.set(key, conf, ttl)
   if not res then
       core.log.error("failed to put ", self.kind, "[", key, "] to etcd: ", err)
       return 503, {error_msg = err}
   end
   ```
   
   An etcd leader election usually lasts longer than ~300ms. If one overlaps a 
sync that has already passed dump, the retries run out and the event ends up in 
`failed[]`.
   
   The failed entry carries no HTTP status, so AIC cannot even separate these 
by status code today:
   
   ```rust
   // adc: rust/crates/adc-cli/src/server/sync.rs
   struct FailedEntry {
       server: Option<String>,
       event: Option<Value>,
       failed_at: String,
       reason: String,
   }
   ```
   
   The `apisix-standalone` backend is not affected: the Rust server fills its 
`failed[]` only from `/validate`.
   
   **Lifecycle once an entry is recorded**
   
   1. Every later push leaves the service out, including periodic `SyncPeriod` 
rounds. The ADC diff then DELETEs the previously serving service and its routes:
   
      ```go
      // internal/provider/apisix/provider.go, sync()
      revisions[config.Name] = revision
      resources = exclude(resources, d.skipped.Excluded(config.Name))
      ```
   
   2. Successful rounds never clear the entry. This PR's own test asserts it:
   
      ```go
      // internal/provider/apisix/status_test.go
      ok := map[string]types.ADCExecutionErrors{"GatewayProxy/ns/gp": {}}
      require.False(t, d.updateStatusFromSyncResults(context.Background(), ok, 
revisions), "nothing new to exclude")
      require.Len(t, d.resourceFailures[route], 1, "still dropped: ADC no 
longer mentioning it says nothing about a fix")
      ```
   
   3. The entry is cleared only when:
      - the owner's translated content actually changes,
      - the owner is deleted or evicted,
      - the GatewayProxy config is removed, or
      - the controller restarts or leadership changes.
   
      The content check is the only path a healthy resource could take:
   
      ```go
      // internal/provider/apisix/provider.go, applyResourceState()
      before := d.store.Revision()
      defer func() {
          if d.store.OwnerChangedSince(rk, before) {
              d.skipped.ClearOwner(rk)
          }
      }()
      ```
   
      For a transient failure the CR is already correct, so none of these is 
expected to happen. The route stays down while the GatewayProxy reports 
`DataPlaneAvailable=True`.
   
   When etcd becomes unavailable partway through a large sync, every event 
after that point fails. The corresponding services are all excluded, and once 
etcd recovers they are all deleted from the data plane with no self-healing. 
Before this PR, the whole sync failed and was retried with backoff until it 
converged.
   
   **Repro (unit level)**
   
   ```go
   func TestTransient5xxExcludesStickily(t *testing.T) {
        d, route := newDroppingProvider(t)
        rev := map[string]uint64{"GatewayProxy/ns/gp": d.store.Revision()}
        results := map[string]types.ADCExecutionErrors{"GatewayProxy/ns/gp": 
failedEvent(
                adctypes.StatusEvent{ResourceType: adctypes.TypeRoute, 
ResourceID: "r1", ParentID: "svc1", Type: "update"},
                "API error (status 503): etcd request timed out")}
        require.True(t, d.updateStatusFromSyncResults(context.Background(), 
results, rev))
   
        for i := 0; i < 10; i++ { // data plane recovered, every round succeeds
                d.updateStatusFromSyncResults(context.Background(), 
map[string]types.ADCExecutionErrors{"GatewayProxy/ns/gp": {}}, rev)
        }
        require.Contains(t, d.skipped.Excluded("GatewayProxy/ns/gp"), 
wireKey{adctypes.TypeService, "svc1"}) // passes
        require.NotEmpty(t, d.resourceFailures[route])                          
                             // passes
   }
   ```
   
   **Possible directions**
   
   - Have ADC mark each `failed[]` entry as a content rejection (non-retriable 
4xx, version gate) or a retriable failure (5xx, transport), and exclude only 
content rejections. This needs an ADC change first.
   - Or give exclusions a bounded lifetime on the AIC side, for example 
re-including them with backoff, so a false positive heals without a content 
change or a restart.
   



-- 
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]

Reply via email to