zhiyi: 单次重读行数上限 + Go IPC 读 deadline + 每 rpc 新 dial (t_576714de)

N=2 准入只拦并发堆叠、不拦单请求自身内存:一次 top_k 大 / scan_all
仍能把单条 zhiyi-consolidate 顶到 1~2GB+ 致 swap thrash(t_eedb2d39 根因)。

改动:
- rust/main.rs: ZHIYI_HEAVY_READ_MAX_ROWS(默认 20000,可覆盖)clamp
  lancedb_search top_k + scan_all/scan_deleted;响应体加 truncated:bool
  (#[serde(default)] 向后兼容旧 Go 客户端);zhiyi_ping 暴露当前 cap。
- rust/lancedb_ops.rs: scan_all/scan_deleted 的 .limit(200_000) 参数化为 max_rows。
- go/lancedb_ipc.go: ipcReq/ipcResp 加 Truncated;rpc() 用 SetWriteDeadline(30s)/
  SetReadDeadline(120s)(原 io.ReadFull 无 deadline = 侧车挂死永久阻塞的根源);
  Search/ScanAll/ScanDeleted 截断时打日志(不静默)。
- go/ipc_reconnect_test.go: TestIPCReadDeadlineBoundsHungSidecar(纯 Go,挂死侧车
  被读 deadline 兜住 ~801ms)+ TestIPCReconnectAfterSidecarRestart(env 门控,
  真 sidecar 杀→快速失败 37µs→重启→重连成功)。

隔离副本 /tmp 实测:封顶 20k peak RSS ~1.17GB vs 不封顶 200k ~1.60GB/80s。
不部署(部署另批,需小怡终审 + 牧尘放行)。
This commit is contained in:
小唯
2026-10-17 21:52:06 +08:00
parent 44fd0e95b4
commit c015febb49
4 changed files with 258 additions and 23 deletions
+130
View File
@@ -0,0 +1,130 @@
package storage
import (
"encoding/binary"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
)
// t_576714de AC2:「边车即使短暂不可用 ⇒ zhiyid 的 IPC 客户端能重连」拆成两半验证:
//
// ① 挂死不无限阻塞:侧车接受连接后永不响应(模拟 thrash 中被 kill / 卡住)时,
// 旧实现 io.ReadFull 无 deadline ⇒ 客户端永久挂起(「边车好了它还挂着」)。
// 修复后读 deadline 封顶,超时即返回错误,调用方降级(Search→本地兜底),recall 不挂。
// ② 真·重连:每次 rpc 都是新 dial。侧车被杀 ⇒ 快速失败(不挂起);侧车重启 ⇒ 下一次
// rpc 重新 dial 成功,无需持有长连接。
//
// TestIPCReadDeadlineBoundsHungSidecar 是纯 Go 自包含(无需真实 sidecar 二进制),恒跑。
// TestIPCReconnectAfterSidecarRestart 需真实 sidecar(env 门控,未设则跳过)。
func TestIPCReadDeadlineBoundsHungSidecar(t *testing.T) {
// Unix socket sun_path 上限 108 字节:t.TempDir() 路径过长会 EINVAL,改用 /tmp 短路径。
tmp, err := os.MkdirTemp(os.TempDir(), "zyri")
if err != nil {
t.Fatalf("mkdtemp: %v", err)
}
defer os.RemoveAll(tmp)
sock := filepath.Join(tmp, "h.sock")
l, err := net.Listen("unix", sock)
if err != nil {
t.Fatalf("listen: %v", err)
}
defer l.Close()
// 侧车替身:accept 后读满请求(长度前缀 + body)但【永不回响应】= 模拟挂死。
go func() {
for {
c, aerr := l.Accept()
if aerr != nil {
return
}
go func(conn net.Conn) {
var lenBuf [4]byte
_, _ = io.ReadFull(conn, lenBuf[:])
n := binary.BigEndian.Uint32(lenBuf[:])
buf := make([]byte, n)
_, _ = io.ReadFull(conn, buf)
// 之后不回任何字节 → 客户端 ReadFull 一直阻塞到读 deadline。
}(c)
}
}()
origRead, origWrite := ipcReadTimeout, ipcWriteTimeout
ipcReadTimeout = 800 * time.Millisecond
ipcWriteTimeout = 5 * time.Second
defer func() { ipcReadTimeout, ipcWriteTimeout = origRead, origWrite }()
rc := NewRustLanceDBClient(sock, nil)
start := time.Now()
_, rpcErr := rc.rpc(ipcReq{Type: "lancedb_search", Vector: make([]float32, 1024), TopK: 5})
elapsed := time.Since(start)
if rpcErr == nil {
t.Fatalf("期望读 deadline 超时返回错误,实际成功(deadline 未生效)")
}
if elapsed < 600*time.Millisecond {
t.Fatalf("错误返回过早(%s)—— 不像读 deadline 触发(应是 ~%s 后)", elapsed, ipcReadTimeout)
}
if elapsed > 15*time.Second {
t.Fatalf("仍长时间阻塞(%s)—— 读 deadline 未封顶,挂死未被兜住", elapsed)
}
t.Logf("挂死侧车被读 deadline 兜住:err=%v in %s(上限 %s,不再无限阻塞 ⇒ recall 可降级不挂)", rpcErr, elapsed, ipcReadTimeout)
}
// startSidecarForTest 启动真实 sidecar(socket 路径 + 数据目录);返回 *exec.Cmd 供后续 kill。
func startSidecarForTest(t *testing.T, bin, sock, dataDir string) *exec.Cmd {
t.Helper()
cmd := exec.Command(bin, "--mode", "socket", "--socket", sock, "--data-dir", dataDir)
cmd.Stdout, cmd.Stderr = io.Discard, io.Discard
if err := cmd.Start(); err != nil {
t.Fatalf("启动 sidecar 失败: %v", err)
}
return cmd
}
func TestIPCReconnectAfterSidecarRestart(t *testing.T) {
bin := os.Getenv("ZHIYI_TEST_SIDECAR_BIN")
dataDir := os.Getenv("ZHIYI_TEST_DATA_DIR")
if bin == "" || dataDir == "" {
t.Skip("ZHIYI_TEST_SIDECAR_BIN / ZHIYI_TEST_DATA_DIR 未设置,跳过真·重连验证")
}
sock := filepath.Join(dataDir, "reconnect-test.sock")
// 1) 起 sidecar → rpc 成功
proc := startSidecarForTest(t, bin, sock, dataDir)
time.Sleep(4 * time.Second)
rc := NewRustLanceDBClient(sock, nil)
if _, err := rc.rpc(ipcReq{Type: "zhiyi_ping"}); err != nil {
proc.Process.Kill()
t.Fatalf("重启前 ping 应成功: %v", err)
}
// 2) 杀掉 sidecar → 下一次 rpc【快速失败】(dial 上限 5s),不挂起
if err := proc.Process.Kill(); err != nil {
t.Logf("kill warning: %v", err)
}
_ = proc.Wait()
start := time.Now()
_, derr := rc.rpc(ipcReq{Type: "zhiyi_ping"})
downElapsed := time.Since(start)
if derr == nil {
t.Fatalf("sidecar 被杀后 rpc 应失败,实际成功(未感知不可用)")
}
if downElapsed > 6*time.Second {
t.Fatalf("sidecar 不可用时未快速失败(用了 %s > 6s dial 上限)—— 疑似挂起", downElapsed)
}
t.Logf("sidecar 不可用 ⇒ rpc 快速失败 in %s(dial 上限 5s,不挂起)", downElapsed)
// 3) 重启 sidecar(Rust 侧会清掉陈旧 socket 再 bind)→ 下一次 rpc 成功 = 重连成立
proc2 := startSidecarForTest(t, bin, sock, dataDir)
defer func() { _ = proc2.Process.Kill() }()
time.Sleep(4 * time.Second)
if _, err := rc.rpc(ipcReq{Type: "zhiyi_ping"}); err != nil {
t.Fatalf("sidecar 重启后 rpc 应成功(重连),实际: %v", err)
}
t.Log("sidecar 重启后 rpc 成功 = 重连成立(每次 rpc 新 dial,无需长连接)")
}
+47 -5
View File
@@ -35,6 +35,18 @@ func NewRustLanceDBClient(sockPath string, emb *Embedder) *RustLanceDBClient {
// ─── IPC 协议 ────────────────────────────────────────────
// t_576714de:单次重读被行数上限截断后,最重的一次响应(20000 行 JSON)在内存
// 紧张时实测 ~43s。读 deadline 取 120s(>> 任何合法操作,≪ 无限阻塞);侧车接受
// 连接后挂死/被 kill 时,旧实现 io.ReadFull 无 deadline ⇒ 客户端永久阻塞
// (「边车好了它还挂着」)。封顶后超时即返回错误,调用方降级(Search→本地兜底),
// 且每次 rpc 都是新 dial ⇒ 侧车恢复后下一次调用天然重连。
// 用 var(非 const)便于测试临时缩短读超时验证「挂死不被无限阻塞」。
var (
ipcDialTimeout = 5 * time.Second
ipcWriteTimeout = 30 * time.Second
ipcReadTimeout = 120 * time.Second
)
type ipcReq struct {
Type string `json:"type"`
Table string `json:"table,omitempty"`
@@ -63,6 +75,9 @@ type ipcResp struct {
Result string `json:"result"` // lancedb search uses this
FailureStep string `json:"failure_step"`
ErrorDetail string `json:"error_detail"`
// t_576714de:单次重读被 Rust 侧行数上限截断时 = true(部分结果,非全量)。
// 旧 sidecar 无此字段 ⇒ 缺省 false,向后兼容。
Truncated bool `json:"truncated"`
}
// updateField lancedb_update 的单列更新项:Value 为 SQL 表达式
@@ -73,22 +88,37 @@ type updateField struct {
}
func (rc *RustLanceDBClient) rpc(req ipcReq) (*ipcResp, error) {
conn, err := net.DialTimeout("unix", rc.sockPath, 5*time.Second)
// 每次调用都是新 dial:侧车不可用(socket 消失/无监听)⇒ 快速失败;
// 侧车恢复后下一次调用天然重连(t_576714de AC)。
conn, err := net.DialTimeout("unix", rc.sockPath, ipcDialTimeout)
if err != nil {
return nil, fmt.Errorf("ipc dial: %w", err)
}
defer conn.Close()
// 给已建立连接封顶读/写 deadline:侧车接受连接后挂死/被 kill ⇒ 旧实现
// io.ReadFull 无限阻塞(「边车好了它还挂着」)。现超时即降级,热路径 recall 不受阻。
now := time.Now()
_ = conn.SetWriteDeadline(now.Add(ipcWriteTimeout))
_ = conn.SetReadDeadline(now.Add(ipcReadTimeout))
body, _ := json.Marshal(req)
lenBuf := make([]byte, 4)
binary.BigEndian.PutUint32(lenBuf, uint32(len(body)))
conn.Write(lenBuf)
conn.Write(body)
if _, err := conn.Write(lenBuf); err != nil {
return nil, fmt.Errorf("ipc write len: %w", err)
}
if _, err := conn.Write(body); err != nil {
return nil, fmt.Errorf("ipc write body: %w", err)
}
io.ReadFull(conn, lenBuf)
if _, err := io.ReadFull(conn, lenBuf); err != nil {
return nil, fmt.Errorf("ipc read len (sidecar down/hung, read deadline %s): %w", ipcReadTimeout, err)
}
msgLen := binary.BigEndian.Uint32(lenBuf)
respBuf := make([]byte, msgLen)
io.ReadFull(conn, respBuf)
if _, err := io.ReadFull(conn, respBuf); err != nil {
return nil, fmt.Errorf("ipc read body (sidecar down/hung, read deadline %s): %w", ipcReadTimeout, err)
}
var resp ipcResp
if err := json.Unmarshal(respBuf, &resp); err != nil {
@@ -243,6 +273,10 @@ func (rc *RustLanceDBClient) Search(table string, vector []float32, topK int, na
Vector []float32 `json:"vector"`
}
json.Unmarshal([]byte(resp.ReportJSON), &raw)
if resp.Truncated {
// t_576714de:Rust 侧单次重读被行数上限截断(部分结果,非全量)—— 显式日志,禁静默。
log.Printf("[ipc] Search truncated: single-read cap applied, got %d rows (partial result)", len(raw))
}
if len(raw) == 0 {
log.Printf("[ipc] Search: resp.ReportJSON unmarshal gave 0 results, falling back to localSearch (Result=%q)", resp.Result)
return rc.localSearch(vector, topK, namespaceFilter), nil
@@ -428,6 +462,10 @@ func (rc *RustLanceDBClient) ScanAll() ([]models.MemoryRecord, error) {
if err != nil {
return nil, fmt.Errorf("scan_all unmarshal: %w", err)
}
if resp.Truncated {
// t_576714de:单次扫描被行数上限截断(部分结果)—— 显式日志,禁静默。
log.Printf("[ipc] ScanAll truncated: single-read cap applied, got %d rows (partial)", len(out))
}
// 同步到本地缓存(与 GetSkillCandidates 同款)
_local.mu.Lock()
for i := range out {
@@ -515,6 +553,10 @@ func (rc *RustLanceDBClient) ScanDeleted() ([]models.MemoryRecord, error) {
if err != nil {
return nil, fmt.Errorf("scan_deleted unmarshal: %w", err)
}
if resp.Truncated {
// t_576714de:单次扫描被行数上限截断(部分结果)—— 显式日志,禁静默。
log.Printf("[ipc] ScanDeleted truncated: single-read cap applied, got %d rows (partial)", len(out))
}
return out, nil
}
+8 -6
View File
@@ -1058,8 +1058,9 @@ impl LanceDBOps {
/// 全表扫描(元数据;2026-09-21 治本改造)
/// ① 用 Select 投影跳过 vector 列(1024 维 = 4KB/条,Step 3/4 都不需要)
/// ② 上限从 10000 提到 200000(原值会静默丢记录:库已有 10065 条)
pub fn scan_all(&self) -> Result<Vec<MemoryRecord>, Box<dyn std::error::Error>> {
/// ② 上限由调用方传入 max_rows(IPC 边界 = 单次重读硬上限 heavy_read_max_rows(),
/// 整合内部全量 = 200_000;t_576714de)
pub fn scan_all(&self, max_rows: usize) -> Result<Vec<MemoryRecord>, Box<dyn std::error::Error>> {
let db = rt().block_on(lancedb::connect(self.data_dir.to_str().unwrap()).execute())?;
let tbl = rt().block_on(db.open_table("memories").execute())?;
@@ -1081,7 +1082,7 @@ impl LanceDBOps {
tbl.query()
.only_if("is_deleted = false")
.select(Select::columns(META_COLUMNS))
.limit(200_000)
.limit(max_rows)
.execute()
)?);
@@ -1143,8 +1144,9 @@ impl LanceDBOps {
/// 都过滤 is_deleted=false —— 存量 11228 条 is_deleted=true 的行**没有任何读路径能看**
/// (唯独 stats 能 count)。回填脚本需要枚举已删除行以回填 delete_reason 并出直方图。
///
/// 只读元数据(跳 vector),上限 200000。带 delete_reason 列(供回填时跳过已回填的)。
pub fn scan_deleted(&self) -> Result<Vec<MemoryRecord>, Box<dyn std::error::Error>> {
/// 只读元数据(跳 vector),上限由调用方传入 max_rows(IPC 边界 = 单次重读硬上限,
/// 整合/回填脚本按需传全量;t_576714de)。带 delete_reason 列(供回填时跳过已回填的)。
pub fn scan_deleted(&self, max_rows: usize) -> Result<Vec<MemoryRecord>, Box<dyn std::error::Error>> {
let db = rt().block_on(lancedb::connect(self.data_dir.to_str().unwrap()).execute())?;
let tbl = rt().block_on(db.open_table("memories").execute())?;
@@ -1164,7 +1166,7 @@ impl LanceDBOps {
tbl.query()
.only_if("is_deleted = true")
.select(Select::columns(META_COLUMNS))
.limit(200_000)
.limit(max_rows)
.execute()
)?);
+73 -12
View File
@@ -76,6 +76,22 @@ impl Drop for ConsolidatingGuard {
static HEAVY_READ_DEFAULT: usize = 2;
static HEAVY_READ_WAIT_MS: u64 = 100;
// ─── 单次重读行数上限(方案②,t_576714de,2026-10-17 残留缺口)────────
// N=2 准入只拦「并发叠加」,不拦「单次请求自身的内存量」:一个 top_k=1000000 /
// scan_all 请求可独自吞下全表(实测 43547 行 × 1024 维向量 ≈ 260MB Rust + JSON ~650MB),
// 照样 thrash(t_eedb2d39 根因复盘定性的 ② 单次量无上限)。
// 治本:给单次重读的**返回行数**设硬上限(默认 20000,env ZHIYI_HEAVY_READ_MAX_ROWS 可调)。
// 请求量超上限 ⇒ 截断到上限 + 标记 truncated:true(降级返回部分结果,而非硬吃内存)。
// 热路径 recall 的 top_k ≤ 1000 永不触发;只挡 admin/穷举型单次大请求。
static HEAVY_READ_MAX_ROWS_DEFAULT: usize = 20000;
fn heavy_read_max_rows() -> usize {
match std::env::var("ZHIYI_HEAVY_READ_MAX_ROWS") {
Ok(v) => v.trim().parse::<usize>().unwrap_or(HEAVY_READ_MAX_ROWS_DEFAULT).max(1),
Err(_) => HEAVY_READ_MAX_ROWS_DEFAULT,
}
}
fn heavy_read_permits() -> usize {
match std::env::var("ZHIYI_HEAVY_READ_CONCURRENCY") {
Ok(v) => v.trim().parse::<usize>().unwrap_or(HEAVY_READ_DEFAULT),
@@ -253,6 +269,10 @@ struct ConsolidateResponse {
report_json: String,
failure_step: String,
error_detail: String,
// 方案②(t_576714de):单次重读被行数上限截断时为 true(降级返回部分结果)。
// 旧 Go 客户端无此字段 ⇒ serde 缺省 false,向后兼容。
#[serde(default)]
truncated: bool,
}
fn main() {
@@ -416,9 +436,14 @@ fn handle_client(mut stream: UnixStream, args: &Args, lancedb: LanceDBOps, state
let v: Vec<f32> = msg["vector"].as_array()
.map(|a| a.iter().filter_map(|v| v.as_f64()).map(|v| v as f32).collect())
.unwrap_or_default();
let tk = msg["top_k"].as_u64().unwrap_or(10) as usize;
let requested = msg["top_k"].as_u64().unwrap_or(10) as usize;
// 空 namespace = 不过滤(匹配所有)
let ns = msg["namespace"].as_str().filter(|s| !s.is_empty());
// 方案②(t_576714de):单次重读行数硬上限 —— 截断到上限,超限标 truncated:true,
// 降级返回部分结果而非让单个请求独自吞下全表向量(43547 行 ≈ 260MB Rust + JSON)。
let cap = heavy_read_max_rows();
let tk = requested.min(cap);
let truncated = requested > tk;
// 重读准入(t_690cc333 方案①):top_k>1000 的重搜索是内存放大器,
// 计入信号量 N;无许可 ⇒ 有界等待后拒绝 heavy_read_busy。
let _perm = if tk > 1000 {
@@ -435,7 +460,15 @@ fn handle_client(mut stream: UnixStream, args: &Args, lancedb: LanceDBOps, state
None
};
match lancedb.search(&v, tk, ns) {
Ok(r) => send_ok(&mut stream, &serde_json::to_string(&r).unwrap_or_default()),
Ok(r) => {
let json = serde_json::to_string(&r).unwrap_or_default();
if truncated {
eprintln!("[zhiyi-ipc] search top_k={} CLAMPED to {} (single-read cap {}), truncated=true", requested, r.len(), cap);
send_ok_truncated(&mut stream, &json);
} else {
send_ok(&mut stream, &json);
}
}
Err(e) => send_error(&mut stream, "lancedb_search", &e.to_string()),
}
}
@@ -559,8 +592,19 @@ fn handle_client(mut stream: UnixStream, args: &Args, lancedb: LanceDBOps, state
return;
}
};
match lancedb.scan_all() {
Ok(r) => send_ok(&mut stream, &serde_json::to_string(&r).unwrap_or_default()),
// 方案②(t_576714de):单次扫描行数上限 —— 超上限截断 + truncated:true(降级部分结果)。
let cap = heavy_read_max_rows();
match lancedb.scan_all(cap) {
Ok(r) => {
let json = serde_json::to_string(&r).unwrap_or_default();
let trunc = r.len() >= cap;
if trunc {
eprintln!("[zhiyi-ipc] scan_all hit cap {} → truncated=true", cap);
send_ok_truncated(&mut stream, &json);
} else {
send_ok(&mut stream, &json);
}
}
Err(e) => send_error(&mut stream, "lancedb_scan_all", &e.to_string()),
}
}
@@ -577,8 +621,19 @@ fn handle_client(mut stream: UnixStream, args: &Args, lancedb: LanceDBOps, state
return;
}
};
match lancedb.scan_deleted() {
Ok(r) => send_ok(&mut stream, &serde_json::to_string(&r).unwrap_or_default()),
// 方案②(t_576714de):单次扫描行数上限 —— 超上限截断 + truncated:true。
let cap = heavy_read_max_rows();
match lancedb.scan_deleted(cap) {
Ok(r) => {
let json = serde_json::to_string(&r).unwrap_or_default();
let trunc = r.len() >= cap;
if trunc {
eprintln!("[zhiyi-ipc] scan_deleted hit cap {} → truncated=true", cap);
send_ok_truncated(&mut stream, &json);
} else {
send_ok(&mut stream, &json);
}
}
Err(e) => send_error(&mut stream, "lancedb_scan_deleted", &e.to_string()),
}
}
@@ -621,6 +676,7 @@ fn handle_client(mut stream: UnixStream, args: &Args, lancedb: LanceDBOps, state
"consolidating": CONSOLIDATING.load(Ordering::SeqCst),
"heavy_read_in_flight": state.heavy_read.in_flight(),
"heavy_read_permits": heavy_read_permits(),
"heavy_read_max_rows": heavy_read_max_rows(),
"ts": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
});
send_ok(&mut stream, &resp.to_string());
@@ -671,11 +727,16 @@ fn handle_client(mut stream: UnixStream, args: &Args, lancedb: LanceDBOps, state
}
fn send_ok(stream: &mut UnixStream, json: &str) {
let resp = ConsolidateResponse { status: "ok".into(), report_json: json.to_string(), failure_step: "".into(), error_detail: "".into() };
let resp = ConsolidateResponse { status: "ok".into(), report_json: json.to_string(), failure_step: String::new(), error_detail: String::new(), truncated: false };
send_response(stream, &resp);
}
/// 方案②(t_576714de):被行数上限截断的正常返回(report_json=部分结果,truncated=true)。
fn send_ok_truncated(stream: &mut UnixStream, json: &str) {
let resp = ConsolidateResponse { status: "ok".into(), report_json: json.to_string(), failure_step: String::new(), error_detail: String::new(), truncated: true };
send_response(stream, &resp);
}
fn send_error(stream: &mut UnixStream, step: &str, detail: &str) {
let resp = ConsolidateResponse { status: "error".into(), report_json: "".into(), failure_step: step.to_string(), error_detail: detail.to_string() };
let resp = ConsolidateResponse { status: "error".into(), report_json: String::new(), failure_step: step.to_string(), error_detail: detail.to_string(), truncated: false };
send_response(stream, &resp);
}
@@ -806,8 +867,8 @@ fn run_consolidation(args: &Args, lancedb: &LanceDBOps) -> ConsolidationReport {
if args.mode == "full" {
let calibrator = DecayCalibrator::new();
let old_rates = decay_calibrate::default_decay_rates();
// 从 LanceDB 加载真实样本
let all_records = lancedb.scan_all().unwrap_or_default();
// 整合内部全量扫描(非 IPC 边界,需全量;行数上限只作用于 IPC 单次重读,t_576714de)。
let all_records = lancedb.scan_all(200_000).unwrap_or_default();
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
@@ -846,8 +907,8 @@ fn run_consolidation(args: &Args, lancedb: &LanceDBOps) -> ConsolidationReport {
eprintln!("[consolidate] Step 4: BGE HTTP (port 8000) unreachable — skipping quality backtrace");
} else {
let backtracer = QualityBacktracer::new(&args.llm_endpoint, &args.llm_model, &args.llm_api_key);
// 从 LanceDB 加载(content = distilled, source metadata = original)
let all_records = lancedb.scan_all().unwrap_or_default();
// 从 LanceDB 加载(content = distilled, source metadata = original)。整合内部全量,非 IPC 边界。
let all_records = lancedb.scan_all(200_000).unwrap_or_default();
let samples: Vec<QualitySample> = all_records
.iter()
.filter(|r| !r.content.is_empty())