Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
334c3d5
feat(onlion): search a long query by its clause and planned windows
wefio Oct 28, 2026
cd81357
feat(onlion): add the append-only log and lazy v4 manifest plumbing
wefio Oct 28, 2026
bf05ddb
docs: record the query window measurement and design the append-log w…
wefio Oct 28, 2026
cbd93fd
fix(onlion): stop clearLog paging when the cursor stops advancing
wefio Oct 28, 2026
3c42fdb
docs: record why clearLog imports the log prefix instead of redeclari…
wefio Oct 28, 2026
09553c9
feat(onlion): accept an Add into the log and fold it in bounded batches
wefio Oct 28, 2026
de7c669
fix(onlion): make the fold actually fold, and count the entries it re…
wefio Oct 28, 2026
bd8e102
test(onlion): run the log-mode integration test as part of verify
wefio Oct 28, 2026
0211e34
fix(onlion): make the pending counter err high, and correct it when i…
wefio Oct 28, 2026
9a6b118
fix(onlion): compensate an over-counted reservation instead of waitin…
wefio Oct 28, 2026
80b43bd
fix(onlion): a drain answers 409 until the log is empty, and setPendi…
wefio Oct 28, 2026
5a634ee
fix(onlion): ask the bucket whether a failed write landed, and never …
wefio Oct 28, 2026
143d3e4
fix(onlion): report the pending count after a release, not before
wefio Oct 28, 2026
628ac51
docs: write down the pending counter's states instead of discovering …
wefio Oct 28, 2026
8aed669
fix(onlion): stop a stale generation from shadowing entries accepted …
wefio Oct 28, 2026
e9290d6
ci: run the service backend in CI, and let the coverage gate see it
wefio Oct 28, 2026
6e2d678
docs: agree on when this work stops
wefio Oct 28, 2026
7a3e540
fix(onlion): scope the accept-side replay lookup to the current gener…
wefio Oct 28, 2026
6437d5d
refactor: bring the two touched files back under the complexity cap
wefio Oct 28, 2026
377fc32
refactor(onlion): signal a lost CAS with retry, not with a null response
wefio Oct 28, 2026
236e0a6
test(onlion): measure the Add burst the default switch is gated on
wefio Oct 28, 2026
de31e50
test(onlion): keep one user per run, derive folds, and isolate the fo…
wefio Oct 28, 2026
c485f05
fix(onlion): pool the burst percentiles from samples, and stop callin…
wefio Oct 28, 2026
1d171a0
fix(onlion): say which log scan is which, and refuse a misnamed entry
wefio Oct 28, 2026
5da6071
test(onlion): label the burst percentiles honestly, and group the rounds
wefio Oct 28, 2026
2bcb3c1
fix(onlion): drop the stale top-level add_mode, and stop asserting a …
wefio Oct 28, 2026
07aeb5b
test(onlion): quote the clean-round percentile, not only the pooled one
wefio Oct 28, 2026
8643267
test(onlion): refuse to measure a mode the deployment is not running
wefio Oct 28, 2026
fa39500
feat(onlion): accept Add through the log by default
wefio Oct 28, 2026
42e2c49
fix(onlion): pin the test runtime, and make the new scripts lint-clean
wefio Oct 28, 2026
aace89c
fix(ci): let the coverage gate judge the service suites without break…
wefio Oct 28, 2026
38317e5
test(onlion): wire the suites that run today, and say why the rest do…
wefio Oct 28, 2026
3594ae2
revert: drop files a glob-add swept into this branch
wefio Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@ jobs:
- name: Dependency audit
run: npm audit --production --audit-level=high

onlion:
name: Service backend (onlion)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
# Two manifests: the service builds its prompt tables with a root-level
# tool, so both dependency trees are installed.
cache-dependency-path: |
package-lock.json
onlion/package-lock.json
- run: npm ci
- name: Install service dependencies
run: npm ci
working-directory: onlion
- name: Service verification contract
run: npm run verify:onlion

tests:
name: Product tests and coverage
runs-on: ubuntu-latest
Expand Down
95 changes: 95 additions & 0 deletions docs/decisions/implemented/2026-10-28-add-log-accept-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Add is accepted into the log by default

[简体中文](2026-10-28-add-log-accept-default.zh-CN.md) · [Terms](2026-10-28-add-log-accept-default.terms.yaml)

**Status:** implemented
**Approved:** explicit

## Problem

`Add` was a read-modify-write of the whole user partition under a per-user manifest CAS.
Sixteen concurrent Adds on one user serialize into sixteen rounds, each moving megabytes,
and the harness saw the result as `/add` rising 36 s → 95 s → a 120 s platform timeout
while `/health` and `/search` stayed in milliseconds.

The append-log path was built for this (see
[the design](../../design/add-append-log-and-fold-design.md)): accept writes one immutable
entry, fold absorbs up to `MERGE_BATCH` entries per partition write, and the read path
sees entries that have not been folded. It shipped behind `NMG_ADD_MODE` and stayed
`sync` until a measurement justified the switch.

## Decision

`log` is the default. `NMG_ADD_MODE=sync` restores the old path without a code change, so
the switch is configuration and is reversible in one deployment.

The gate was "no regression" rather than an absolute latency target: the base cost of a
request on this path is dominated by the connection and the embedding call, so the
question was whether accepting through the log made an Add slower, not whether it made
the link fast.

## Measured result

16 concurrent Adds of 8 messages to one user, same client, same endpoint
(`ap-hongkong`), one warmup Add first, `concurrency=16` therefore one round per run.
Rounds that crossed the fold trigger are excluded, and with `rounds=1` none did.

| mode | runs | responses | p50 | max | failures |
| ------ | -------------- | ------------------------ | ----------------------------- | ----------------------------- | -------- |
| `sync` | 1 clean round | 16 x 200 | 21.8 s | 30.7 s | 0 |
| `log` | 4 clean rounds | 16 x 200 each (64 total) | 3.8 s / 4.3 s / 4.3 s / 3.8 s | 5.9 s / 5.2 s / 7.3 s / 4.9 s | 0 |

The accept path is roughly five times cheaper at the median and six times cheaper at the
worst case in this burst. Nothing regressed.

### Instability is part of the result

Roughly half the runs taken during the same hour could not establish a connection for 5 to
20 of the 16 requests (`UND_ERR_CONNECT_TIMEOUT`), in **both** modes, while other requests
in the same run answered in milliseconds. Those runs are not "noise to be discarded": a
client that cannot reach the endpoint is the experience the leaderboard reports, and the
log switch neither causes nor cures it. What matters for this decision is that the clean
runs in both modes were taken an hour apart on the same path, and the difference between
them is far larger than the run-to-run spread within a mode.

## What this does and does not establish

- **The fold cost is not measured.** With `rounds=1` and a fresh user, pending peaked at
17 and never reached `FOLD_TRIGGER` (24), so no request in these runs paid for folding.
What is measured is the accept path. The fold's cost is one partition write per batch and
is bounded by `MERGE_BATCH`, but its latency in production is unknown.
- One region, one user, one payload shape (8 messages). A different burst size, a longer
running user, or a different region can differ.
- The instability above means individual runs are not comparable; only runs with zero
client-side failures were used, and there were four of those for `log` and one for `sync`.
- No answer-quality or retrieval effect is claimed: this changes when an Add becomes part of
a partition, not what is stored.

## Alternatives considered

- **Keep `sync` and lower the retry budget.** The 503s come from twelve CAS attempts
running out under sixteen writers; a shorter budget turns them into faster 503s, and a
longer one into slower successes. Neither changes how many partition rewrites a burst
costs.
- **Additive delta writes for `Add`.** Measured and rejected earlier: an 8-message Add
changes 72–86 pages of 8 KiB against a 32-page (`PART_DELTA_LIMIT`) budget, 0 of 13 adds
fit, and the overlay lives inline in the manifest every writer contends for.
- **Pool `Add` requests server-side.** Functions are stateless across instances, so
"collect concurrent Adds and write once" needs a coordinator the deployment does not have
and a queue this runtime cannot provide.
- **Wait for a stable-client measurement before switching.** The base cost here is the
connection and the model call, both of which are shared by the two paths; a five-fold
difference in the accept path is far larger than the run-to-run spread, and the switch is
one environment variable away from being reverted.

## Consequences

- An accepted Add is now readable through `pendingForSearch` until a fold moves it into a
partition, capped at `MAX_PENDING_READ` entries per search.
- `/capacity` reports `pending_entries`; `sqlite_bytes` stays zero until the first fold,
which is why the EdgeOne smoke had to stop asserting `sqlite_bytes > 0` after an Add.
- Rolling the **code** back after pending entries exist requires draining first:
`POST /admin/fold?user_id=` answers 409 until the log is empty, and a v4 manifest is
unreadable to an older deployment.
- Folding happens on the `Add` path when pending reaches `FOLD_TRIGGER`; it never happens
on the read path, so search latency does not inherit write contention.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
version: 2
terms: []
references: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Add 默认走日志受理

[English](2026-10-28-add-log-accept-default.md) · [Terms](2026-10-28-add-log-accept-default.terms.yaml)

**Status:** implemented
**Approved:** explicit

## 问题

`Add` 原本是对整个用户分区的读-改-写,由每用户的 manifest CAS 串行化。同一用户 16 路并发
会被拉成 16 轮,每轮搬动兆字节;评测方看到的结果是 `/add` 从 36 s 升到 95 s、最后 120 s
平台超时,而 `/health` 与 `/search` 一直是毫秒级。

日志受理路径就是为此而建(见[设计](../../design/add-append-log-and-fold-design.zh-CN.md)):
受理只写一条不可变条目,折叠每次分区写吸收最多 `MERGE_BATCH` 条,读路径能看见尚未折叠的条目。
它一直藏在 `NMG_ADD_MODE` 后面,默认 `sync`,直到有测量能支撑切换。

## 决策

默认改为 `log`。`NMG_ADD_MODE=sync` 无需改代码即可回到旧路径,所以切换是配置、一次部署可逆。

门槛是"**无回退**"而不是某个绝对延迟:这条路径上的基础成本主要由连接与 embedding 决定,
要回答的是"经日志受理会不会让 Add 变慢",而不是"能不能让链路变快"。

## 实测结果

同一用户、每次 8 条消息、16 路并发,同一客户端、同一端点(`ap-hongkong`),先做一次预热
Add,因此每次运行只有一轮。跨过折叠阈值的轮被排除;`rounds=1` 时没有任何一轮跨过。

| 模式 | 运行 | 响应 | p50 | max | 失败 |
| ------ | ---------- | ------------------------ | ----------------------- | ----------------------- | ---- |
| `sync` | 1 个干净轮 | 16 × 200 | 21.8 s | 30.7 s | 0 |
| `log` | 4 个干净轮 | 每轮 16 × 200(合计 64) | 3.8 / 4.3 / 4.3 / 3.8 s | 5.9 / 5.2 / 7.3 / 4.9 s | 0 |

这次突发里,受理路径的中位数便宜约 5 倍、最坏情况便宜约 6 倍。没有回退。

### 抖动本身也是结果

同一小时内约有半数的运行里,16 个请求中有 5–20 个**无法建立连接**
(`UND_ERR_CONNECT_TIMEOUT`),两种模式都出现,而同一次运行里另一些请求是毫秒级返回。
这些运行不是"可以丢掉的噪声":客户端连不上端点,正是评测方报告的那种体验,而日志切换既
不制造它也不治愈它。对这个决策而言重要的是:两种模式的干净运行取自同一路径、相隔约一小时,
而它们之间的差异远大于同一模式内的运行间波动。

## 这确立了什么、没有确立什么

- **折叠成本未测量。** `rounds=1` 且是新用户,pending 峰值只有 17,从未到达
`FOLD_TRIGGER`(24),所以这些运行里没有任何请求为折叠付过代价。测量的是受理路径。
折叠的成本是"每批一次分区写"、由 `MERGE_BATCH` 封顶,但它在生产中的延迟未知。
- 一个地区、一个用户、一种 payload 形状(8 条消息)。不同的突发规模、长期存在的用户或别的
地区都可能不同。
- 上面的抖动意味着单次运行之间不可横比;只有**零客户端失败**的运行被采用,其中 `log` 有四个、
`sync` 只有一个。
- 不主张任何答案质量或检索效果的变化:这次改变的是"Add 何时成为分区的一部分",不是"存了什么"。

## 考虑过的替代方案

- **保留 `sync`,只调重试预算。** 503 来自 16 个写者打满 12 次 CAS;预算调小只是把 503 提前,
调大只是把成功推后,都不改变一次突发要重写多少次分区。
- **`Add` 走增量页写。** 此前实测否决:8 条消息的 Add 改 72–86 页(每页 8 KiB),而预算是
32 页(`PART_DELTA_LIMIT`),13 次里 0 次命中;且 overlay 内联在所有写者争用的 manifest 里。
- **服务端合并并发的 `Add`。** 函数实例之间无共享状态,"收集并发写一次性落盘"需要一个部署里
没有的协调者,以及这个运行时提供不了的队列。
- **等稳定客户端测完再切。** 这条路径的基础成本是连接与模型调用,两条路径共享;受理路径上
5 倍的差异远大于运行间波动,而切回只差一个环境变量。

## 后果

- 受理后的 Add 在折叠之前通过 `pendingForSearch` 可读,每次检索最多 `MAX_PENDING_READ` 条。
- `/capacity` 上报 `pending_entries`;`sqlite_bytes` 在首次折叠前保持 0——这就是 EdgeOne smoke
不能再断言"Add 之后 `sqlite_bytes > 0`"的原因。
- 存在 pending 条目时回滚**代码**需要先排空:`POST /admin/fold?user_id=` 在日志清空前返回 409,
而 v4 的 manifest 对旧部署不可读。
- 折叠只发生在 `Add` 路径(pending 达到 `FOLD_TRIGGER` 时),读路径永不折叠,因此检索延迟不
继承写竞争。
Loading
Loading