Skip to content

fix(onlion): an embedding outage costs latency, not the write - #135

Merged
wefio merged 1 commit into
mainfrom
fix/embedding-breaker
Oct 30, 2026
Merged

wefio merged 1 commit into
mainfrom
fix/embedding-breaker

Conversation

@wefio

@wefio wefio commented Oct 30, 2026

Copy link
Copy Markdown
Owner

What

  • Doc owner: docs/decisions/implemented/2026-10-30-embedding-breaker.{md,zh-CN.md,terms.yaml}
  • Maps Tencent's three-class analysis (74 timeouts): class B (39 rows, 52.7%) is ours — the accept's inline embed loses its 8 s race, the fold backfills, syncRecordEmbeddings throws on the provider timeout, that throw leaves rememberBatch → addOneBatch → mutateUser, and mutateUser re-runs the whole attempt up to 12 times, each paying the same 12 s timeout (12 × 12 s = 144 s). The records were already durable.
  • The backfill never fails its caller, except EmbeddingPreparation — that one is control flow (runPart unwinds the SQLite session, embeds outside it, re-enters); swallowing it silently dropped records and the fold-searchability test caught it.
  • Provider circuit breaker (NMG_EMBED_BREAKER_MS, default 30 s) on dependency_timeout / embedding_timeout / embedding_network_error / 429 / 5xx: while open, an embedding call fails in microseconds as embedding_suppressed; records are stored vectorless and score lexically until a later fold backfills them. A 400 does not trip it.

Not verified

Not deployed yet (local verify 37 PASS). Class A (31 rows, 53 ms, 54.48 MB) and class C (4 rows) never reached the handler — no nmg_request line — so the worker cannot fix them; reducing the 600-800 MB per-instance baseline is a separate task, stated as such in the record. The breaker window (30 s) is a judgement, not a measurement.

Tencent's analysis of 16:52-17:52 split 74 timeouts into three classes; class B
(39 rows, 52.7%) is ours and it is a mechanism:

1. the accept's inline embed loses its 8 s race, so the entry is stored
   vectorless;
2. the fold backfills, syncRecordEmbeddings throws on the provider timeout;
3. the throw leaves rememberBatch -> addOneBatch -> mutateUser, which treats it
   as a lost compare-and-swap and re-runs the whole attempt - up to twelve
   times, each paying the same 12 s timeout. 12 x 12 s = 144 s, which is the
   120 s ceiling with only the fold on the stack.

The records were already durable; every retry re-embedded them for nothing.

Fixes:
- the backfill never fails its caller, except EmbeddingPreparation, which is
  control flow (runPart unwinds the SQLite session, embeds outside it, re-enters).
  Swallowing that one silently dropped records - the fold-must-stay-searchable
  test caught it.
- monitoring gains a provider circuit breaker (NMG_EMBED_BREAKER_MS, default
  30 s) on dependency_timeout / embedding_timeout / embedding_network_error /
  429 / 5xx. While open an embedding call fails in microseconds as
  embedding_suppressed; records are stored vectorless and score lexically until
  a later fold backfills them. A 400 does not trip it: that is our bug.

Class A (31 rows, 53 ms, 54.48 MB) and C (4 rows) never reached the handler, so
the worker cannot fix them; reducing the 600-800 MB per-instance baseline is the
separate next task, and the record says so.

Recorded in docs/decisions/implemented/2026-10-30-embedding-breaker.*
@wefio
wefio merged commit 72b8389 into main Oct 30, 2026
8 checks passed
@wefio
wefio deleted the fix/embedding-breaker branch October 30, 2026 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant