fix(quota): hostOf 用了 gmatch 而不是 match —— memo 表从未命中过任何东西

## 这是退避失效的真正原因,而且比 TTL 严重

查生产 state 时发现的:19 条 memo,`hostOf(key)` 命中 **0** 条。

    hostOf = function(u)
      return (string.gmatch(u, "^https?://([^/]+)")()) or u
    end

`string.gmatch` 返回**整个匹配**,不是捕获组;而带 `^` 锚定时迭代器不会前进,
所以第一次 `()` 调用返回 **nil**,函数走 `or u` 分支返回完整 URL。

后果是彻底的、而且完全静默:

  - 每次查表都是 HOST_MEMO[nil],每次写入也是 HOST_MEMO[nil]
  - memo 表**对任何 host 从未命中过**
  - 负 memo、PROBE_MEMO_TTL、以及刚加的指数退避,**全是看起来活着的死代码**
  - 生产那 19 条历史 memo(key 是完整 URL,来自 ccquota 时代)从来没有被读过

所以「两个死上游每 30 分钟重付一次探测」的真实原因不是 TTL 到期,
而是压根没命中过 memo。

## 为什么这么久没人发现

失败模式与「memo 里没有可记的东西」完全不可区分:页面照常显示,卡片照常
skipped,没有任何报错。唯一线索是 state 里那 19 条条目 —— 它们看起来像是
在正常工作留下的痕迹。

## 修法

    string.match(u, "^https?://([^/]+)")

判据 `TestQuotaMemoKeyIsTheHost` 钉住它:读**实际发布的** quota.lua,检查
用 string.match,并在运行时验四个 URL 的 host 抽取,以及同一 host 的两个不同
路径必须产出同一个 key(这正是按 host 做 memo 的意义)。

变异把 match 改回 gmatch 立刻变红,并打印出实际返回的完整 URL。
This commit is contained in:
JianFeeeee
2026-10-05 07:27:13 +08:00
parent 18525ae4c0
commit 7c7cf3e078
2 changed files with 81 additions and 1 deletions

View File

@ -63,8 +63,23 @@ end
-- hostOf extracts the host from a base_url, for showing WHICH upstream a row is
-- about. Two sources on the same vendor (two Command Code keys) are
-- indistinguishable without it.
-- hostOf returns the host of a URL — the part memoisation is keyed on, so two
-- sources pointing at the same host with different paths share one answer.
--
-- It used to be written with string.gmatch and a capture group. gmatch yields
-- WHOLE matches, not captures, and with `^` it never advances, so the iterator
-- returned nil on the first step and hostOf returned its `or u` fallback —
-- the FULL URL.
--
-- The consequence was invisible and total. Every lookup was HOST_MEMO[nil] and
-- every write was HOST_MEMO[nil], so:
-- - the memo table never returned anything, for any host, ever
-- - the negative memo, the TTL and (later) the backoff were all dead code
-- - the 19 entries in this gateway's production state are unreachable: their
-- keys are full URLs, while a lookup asks for nil
-- Measured before the fix: hostOf(key) matched 0 of 19 production entries.
local function hostOf(u)
return (string.gmatch(u, "^https?://([^/]+)")()) or u
return (string.match(u, "^https?://([^/]+)")) or u
end
-- get walks a dotted path through nested tables, returning nil if any hop is