AlinsRan commented on PR #13840:
URL: https://github.com/apache/apisix/pull/13840#issuecomment-5349797067

   Thanks — I checked all four against the code before answering. Two are 
already handled, one is a real gap and is now fixed, one is real but the 
obvious fix does not close it.
   
   **1. Request-path introspection I/O — confirmed, deliberate, staying.**
   
   The fetch is lazy on purpose, and it is bounded rather than unbounded: it is 
skipped entirely when the service carries no decorations, so a deployment not 
using the cost model never makes the call; it is single-flighted per cache key 
with `resty.lock` under an explicit lock timeout, so an unresponsive endpoint 
parks one request rather than every request; the result is cached per worker 
per service, and a failure is cached for 10s so a dead endpoint costs at most 
one upstream request per worker per 10s.
   
   Prefetching at `init_worker` is not available to it: the plugin does not 
know at boot which services carry decorations — they arrive through the 
`/services` watcher — the derived endpoint depends on the route's upstream, and 
the upstream is frequently not reachable during boot. Moving it off the request 
path would mean a services-watcher-driven registry plus a refresher timer, 
fetching schemas for services that may never receive GraphQL traffic. That is a 
reasonable thing to want, but it is a separate change; I would rather not add a 
background fetcher to this PR. Tracking separately.
   
   **2. Plugin reload coverage — valid gap, fixed.**
   
   You are right that nothing asserted it. Rather than a `t/cli` lifecycle 
script, I added an in-tree case that primes both introspection caches through 
the shipped module's own upvalues (not a replica), calls `destroy()` the way 
`apisix/plugin.lua` does on reload, and asserts both are empty. I verified it 
is not vacuous: making `destroy()` a no-op reddens it.
   
   **3. Backend error classification — already guarded; the residual case is 
the operator's own choice.**
   
   `graphql-limit-count.lua` already distinguishes them:
   
   ```lua
   local code, msg = limit_count.rate_limit(conf, ctx, plugin_name, cost)
   if code == 500 and conf.rejected_code ~= 500 then
       return code, msg
   end
   ```
   
   A counter-backend failure returns a literal 500 from `limit-count`, a 
rejection returns `conf.rejected_code`. The guard is written this way rather 
than `code >= 500` precisely so the default `rejected_code` of 503 does not get 
returned where the `max_cost` rejection should answer 403.
   
   That leaves exactly one case: `rejected_code == 500`. There the two are 
indistinguishable by construction, because the operator chose the rejection 
code that collides with the internal one — the ambiguity exists for a plain 
rejection too, with or without `max_cost`. And when `max_cost` is exceeded, 403 
is the accurate answer regardless of whether the counter also failed: the query 
really is over the limit, and that is a property of the query, not of the 
counter. The backend failure is not lost either — `limit-count/init.lua` logs 
it at error level before returning.
   
   An explicit outcome marker from `rate_limit` would be cleaner, but 
`rate_limit` lives in the shared `limit-count/init.lua` and is called by 
`limit-count` as well, so changing its contract is not a change this plugin can 
make on its own. Tracking separately.
   
   **4. Service recreation race — confirmed real, and reordering does not close 
it.**
   
   The window is as you describe: `service_absent` → `base_put` → 
`reclaim_stale_decorations`, and a decoration `PUT` landing between the last 
two is deleted after returning 201.
   
   I checked the obvious fix — reclaiming before `base_put` — and it does not 
work. It only moves the window: `service_absent` says absent, another client 
creates the service *and* a decoration, then our reclaim deletes it. The 
decoration handler 404s when the parent service is missing, so the window is 
empty at that instant, but the check-then-act gap is still there.
   
   The correct fix is the one you point at: delete only the keys that predate 
the new service, i.e. those whose `mod_revision` is below the create revision 
`base_put` gets back from etcd. `core.etcd.rmdir` is an unconditional range 
delete, so this needs a list followed by a per-key compare-and-delete, which is 
a different shape from the current one-call reclaim.
   
   Worth noting how narrow it is in practice: the reclaim only runs on a 
**create** of a service id that was absent a moment earlier, so the racing 
client has to be writing a decoration to a service id that is being created 
concurrently by someone else. The failure mode is a lost decoration, not a 
wrong cost — the cost model degrades to the node count.
   
   Happy to implement the revision-conditioned reclaim in this PR if you would 
rather not carry it; my inclination is to track it separately so this PR does 
not also change the etcd write shape. Let me know which you prefer.
   


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