Repository navigation
fix(storage): propagate Volcengine deletion request failures - #5268
Conversation
Add opt-in TTL for events and sessions. Default OFF: when disabled every read path and the cleanup scanner short-circuit on ttl_enabled(), so existing objects and behaviour are untouched. Scope is strictly user_events, peer_events and sessions (openviking/core/ttl.py classifies URIs; nothing else is ever affected). ttl_days/received_at/ expires_at are frozen once at object creation (event memory create, session first materialize) and persisted verbatim, so later policy changes never move an existing object's deadline. A successful session commit renews expires_at from the object's own frozen ttl_days, never from current config. Logical invisibility is separate from physical cleanup: - Read barrier (expiry_filter_now -> range_out on expires_at) is injected in the vector retrieval seam and the FS grep/glob/stat filters, applied BEFORE candidate budget/top-k so expired vectors cannot occupy a slot. Absent or future expires_at stays visible. - Session load/list and the idle auto-commit scan use the scalar hidden_by_ttl gate so an expired session is invisible and never resurrected by a late auto-commit. Physical cleanup (openviking/service/ttl_cleanup.py) mirrors the existing durable-work pairs: an interval scanner (like SessionAutoCommitScheduler) walks /local for past-due objects and enqueues them on a single-consumer TTL_CLEANUP queue; the processor (like _DeletionProcessor) re-reads the live expiry immediately before deleting (renewal-wins / never delete a renewed or newly-created object), then deletes strictly. Restart/recovery reuses QueueFS + TaskTracker; a terminal task is never reprocessed; NotFoundError settles as success (idempotent); delete failure fails the task and is retried next scan. Add strict delete mode (FSService.rm / VikingFS.rm / SessionService.delete): after the FS+vector delete it re-counts the URI scope and raises if any vector record remains, so cleanup only marks complete — and billing only stops — once files and vectors are both gone. Backend count errors propagate, so strict never masks a real failure. The default interactive delete keeps the historical best-effort semantics. TTL is preserved through maintenance links: reindex re-derives expires_at from the memory-file body (never from current policy), ovpack export/import carries it as a portable scalar, and the vector schema adds an indexed expires_at date_time column. The commercial data-plane date_time normalization covers expires_at alongside created_at/updated_at. Tests: unit coverage for scope classification, freeze/renew, is_expired edge cases, the enabled/disabled gate, the barrier predicate on both commercial and local backends, and the cleanup processor decision (strict delete, renewal-wins, TTL-disabled halt, terminal-skip, already-gone, delete-failure). Co-authored-by: TRAE CLI <traecli@bytedance.com>
cocolord
left a comment
There was a problem hiding this comment.
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
评审对象:1d540e309b6cdd03e02243ec5bfab297848cd50a。结论:当前设计和实现还不能合入,主要问题不是零散边界条件,而是 TTL 的策略、可见性和清理一致性没有形成同一个端到端契约。
动机
这个 PR 要解决两个真实问题:event memory 和 session 持续增长带来的存储/计费压力,以及过期内容继续参与读取和检索的问题。预期结果是:配置 TTL 后,对象到期立即从界面、session、find/search/grep/glob 等读取面消失;随后后台可靠地删除文件、元数据和向量,确认物理清理完成后才停止计费。默认关闭时,现有读取和写入行为应保持兼容。
方向本身合理,但当前实现把问题拆成“创建时冻结字段 + 多处读过滤 + 独立扫描删除”三套机制,缺少贯穿公开写入口、共享读取边界和异步删除队列的统一 ownership。因此 helper 和 mock 测试可以通过,真实调用链仍会绕过或破坏 TTL 语义。
改动思路
当前方案以 core/ttl.py 做 URI scope 归类和 expires_at 计算;event 在 MemoryUpdater 创建时写入快照,session 在首次物化时写入并在 commit 后按自身 ttl_days 续期。向量检索和部分 FS 查询拼接 expiry filter;后台 scanner 遍历 /local,把过期对象写入 TTL_CLEANUP 队列,再通过 strict=True 删除并检查向量残留。reindex、ovpack、Context 和 schema 同步携带 expires_at。
问题在于这些机制没有落在真正的共享边界:
- 策略 resolver 只支持
scope > library global,无法表达对象级 absolute/relative、目录和 User 层覆盖,也不支持已有对象增量调整。 - event 的 TTL producer 放在 MemoryUpdater,而公开 content write 还有另一条创建路径。
- 读屏障放在若干 vector/filter 分支,而不是 VikingFS 的统一可见性边界。
- cleanup 自己建立了 scanner/task 状态,却没有与写入、embedding 和已有 deletion lifecycle 共享 generation/tombstone。
更稳妥的边界应是:一个权威 policy resolver;一个持锁的 event/session 创建与更新入口;一个所有读取/枚举都经过的 storage visibility seam;一个由对象 generation/tombstone 约束、可在源文件消失后继续重试向量删除的清理生命周期。
具体改动
关键代码讲解
-
TTLConfig.resolve_scope只解析三个固定 scope 与 library global;文件头还明确说明目录 override 本次不持久化。与此同时 session create/update 的请求模型没有ttl_relative/ttl_absolute。这使当前公共模型无法表达既定的 session absolute > session relative > directory > User > library 优先级,也无法支持 TTL 的增量调整。若这是分阶段交付,应先把 PR scope、对外配置兼容策略和后续迁移写清;否则不应把这个较窄模型作为最终公共契约。 -
MemoryUpdater._apply_upsert只在该路径创建 event 时冻结 TTL。公开content/write和content/batch-write经ContentWriteCoordinator._create_and_write -> _write_memory_with_refresh -> _write_in_place创建 memory 文件,随后直接刷新 embedding,不进入这里。因此同一 event URI 通过不同公开入口创建,会得到不同生命周期:content API 创建的文件和向量没有expires_at,永不过期。TTL 固化应移动到共享、持锁的 memory-file creation seam。 -
SessionService.get用not auto_create跳过过期检查,但auto_create的契约只是“缺失时创建”。message/batch-message、MCP remember 和 context assembly 都以auto_create=True调用;已有 session 已过期时仍会被 load、追加并可能在 commit 中续期。应区分“本次确实创建”和“对象已存在”,并在 append/commit 持有 session mutation lock、重载 meta 后再次执行权威 expiry check。 -
_with_expiry_barrier的 grep 接入 只覆盖带tag_filter的向量预筛选;默认 native/fallback FS grep 仍直接扫描物理文件。read/read_file、stat、exists、ls、tree以及本地 glob 也没有对象级 TTL 判断。于是到期后、cleanup 前(或 cleanup 失败后)内容仍可按 URI 读取和枚举,和“立即逻辑不可见”冲突。应把规则收口到 VikingFS 的共享读取/枚举 owner,而不是继续逐个 query 分支补 filter。 -
TTLCleanupService._process先无锁读取 expiry,之后才在rm内获取删除锁。期间 session 可以续期、event 可以同 URI 重建,旧任务仍会删除新状态。另一方向上,延迟 embedding 可在删除/残留检查后重新 upsert;若 strict residue check 失败,物理文件已删除,handler 把任务标为 FAILED 后 NamedQueue 仍 ACK,下一轮基于文件的 scanner 也无法再发现孤儿向量。需要让 writer、embedding 和 deleter 共享 generation/tombstone,并在同一 lease/CAS 下“复核 + 条件删除”;任务应以稳定 URI/generation 为幂等键,即使源文件已不存在也能重试 vector delete,直到 residue 为 0。
其余改动(Context.expires_at、reindex/ovpack 透传、schema/index、strict 参数链路、scheduler/runtime wiring)在形状上是完整的,但它们依赖上述前提。尤其商业后端的 date-time range_out 支持、已有远端 collection 的 expires_at schema/index 迁移,目前只有本地字典级测试或无验证,重审前还需要真实 backend contract 证据。
对主干的风险
- 策略不兼容:公共配置一旦发布,后续补对象/目录/User 层会带来配置与已冻结数据的迁移成本;当前“配置变化不影响旧对象”也与增量调整要求相冲突。
- TTL 可被绕过:普通 content API 创建的 event 永不过期;普通 message API 可以写入并复活过期 session。
- 过期内容继续可见:纯 FS 读取和枚举在清扫完成前仍暴露数据,清扫失败会把短窗口放大为永久窗口。
- 并发数据丢失或计费残留:renew/recreate 与 cleanup 竞争会误删有效对象;late embedding 或 strict failure 会留下无源向量且没有可靠重试入口。
- 验证不足:本次运行中
test_ttl_config.py8 个、test_ttl.py25 个用例通过,git diff --check和compileall通过;cleanup/backend 测试在当前环境因可选依赖未完整收集。更重要的是,现有 cleanup 测试 mock 了 delete,未覆盖公开入口、真实存储、queue ACK、锁竞争和 embedding 回写。PR 当前也没有 GitHub status checks。
最低修复验证应包括:public content write/batch-write 的 event 文件与向量 readback;existing-expired + auto_create=True 的 add/batch/commit;expired-but-not-cleaned 的 read/stat/ls/tree/grep/glob;以及可控时序下 renewal、同 URI recreate、late embedding、residue failure 的端到端测试。
我的整体评价
核心价值成立,expires_at 作为持久化投影、检索前过滤、reindex/ovpack 保留和 strict delete 的方向也有可取之处。但 exact head 1d540e309b6cdd03e02243ec5bfab297848cd50a 当前仍有五个 P1 级设计/正确性 blocker,而且它们共同指向 ownership 分散:策略不是完整产品契约,producer/read barrier/delete fence 都不在共享边界。建议先收敛模型和边界,再补真实 public-path 与并发测试;修复后需要按完整 head 重新 review,而不是只验证单个补丁点。
English verdict: Request changes on exact head 1d540e309b6cdd03e02243ec5bfab297848cd50a. The TTL policy does not yet match the required object/directory hierarchy, public event writes and session mutations can bypass expiry, filesystem reads expose expired data, and cleanup lacks the atomic generation fence needed to prevent data loss and orphaned billable vectors. Config/core unit tests and static checks passed, but real storage/queue/backend boundary coverage is still missing.
Correct inherited directory policies and recover successful session renewal before expiry cleanup. Expire derived summaries and their vectors, persist delayed cleanup retries, claim bounded due work through PersistentTaskStore, and reject physically occupied expired session IDs. Include the prerequisite strict-delete verification and generation fences for asynchronous writers, plus TTL snapshot preservation across maintenance paths. Execution continues through QueueFS and TaskTracker. Validation: 671 passed, 3 skipped in the affected regression group; additional maintenance and API checks; Ruff and diff checks pass. Native-engine integration remains blocked locally by missing PersistStore.
Preserve all L0/L1 files and vectors during TTL cleanup, including inside expired resource and session roots. Reuse strict deletion with an L2 filter and remove summary expiry projection and rebuild work. Share runtime TTL resolution across creation paths and normalize config aliases without losing sparse PATCH semantics. Add common live-document expiry controls through HTTP, CLI, Python SDK and MCP, reusing object locks and the existing cleanup registry. Validate native AGFS/vector retention, deletion retries, configuration precedence and the public client chain; document hosted gateway limits. Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com>
Track resource TTL per file, renew relative deadlines after successful content updates, and keep directories as policy-only containers. Isolate resources from library-global retention, make cleanup pausable and jittered, and preserve Watch behavior across expired files. Validate merged runtime settings and update public docs and compatibility tests. Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com> Co-authored-by: TRAE CLI <traecli@bytedance.com>
…ixes Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com> Co-authored-by: TRAE CLI <traecli@bytedance.com> # Conflicts: # openviking/config/account_config.py # openviking/config/binding.py # openviking/config/manager.py # openviking/session/compressor_v3.py # openviking/storage/abstract_overview.py # openviking/storage/collection_schemas.py # openviking/storage/ovpack/operations.py # openviking/storage/viking_vector_index_backend.py # tests/config/test_runtime_config.py # tests/server/test_admin_rebuild_api.py # tests/storage/test_collection_schemas.py # tests/storage/test_semantic_queue_memory_dedupe.py
…ixes Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com> Co-authored-by: TRAE CLI <traecli@bytedance.com> # Conflicts: # openviking/service/session_auto_commit.py # openviking/session/session.py
Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com> Co-authored-by: TRAE CLI <traecli@bytedance.com>
Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com> Co-authored-by: TRAE CLI <traecli@bytedance.com>
Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com> Co-authored-by: TRAE CLI <traecli@bytedance.com>
Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com> Co-authored-by: TRAE CLI <traecli@bytedance.com>
Co-authored-by: TRAE CLI <traecli@bytedance.com>
Co-authored-by: TRAE CLI <traecli@bytedance.com>
Coordinate event initialization, policy updates, and cleanup with the existing overview lock, and reuse Session root leases throughout nested writes. Acquire transfer locks together to avoid lock-order inversions. Remove duplicate write admission and full-body reads during file copies. Validation: 193 targeted tests and native Session probes passed; Ruff and diff checks passed.
Share owner metadata across glob, native Session grep, and response expiry fields. Reuse metadata protected by existing Session locks, and avoid acquiring the same Event file lock twice. Skip peer enumeration when policy changes cannot affect peer events. Bypass owner checks for accounts that have never enabled TTL, remove redundant metadata stat calls from reads, and widen vector refill batches when expired candidates are dense. Preserve expiry checks and document the deferred vector expiry field without adding a cross-request cache or changing cleanup. Validation: 252 distinct targeted tests passed for the final request-read changes. Earlier vector/read-path and lock changes passed their recorded targeted suites and native probes. Ruff, format, and diff checks passed.
Restore native pagination for unmanaged accounts with before/after marker checks, bound TTL candidate and exclusion budgets, and retry transient cleanup lock contention twice after releasing locks. Add retention-day boundary and failed-save coverage, explicit validation feedback, and bilingual behavior documentation. Known limitation: unmanaged pagination still adds marker reads. Default-off zero additional storage I/O remains unresolved pending a safe cross-node activation boundary; documented as an unmet performance gate. Validation: 69 targeted Python tests passed before the added boundary cases; the final config suite passed 34 tests. Web Studio TTL component suite passed 14 tests. Targeted Ruff, ESLint and git diff checks passed. No remote-backend load or hosted-console acceptance testing.
Compare query, keyword and random read wrappers with the native backend across first, deep, partial, empty and over-budget pages. Verify sorting, scores, projection and forwarded search parameters. Null expiry remains an intentional response field; marker I/O remains an unresolved performance gate. Validation: 49 TTL vector read tests passed; Ruff and diff checks passed.
Description
In the Volcengine AK/SK backend, scalar-query failures used during deletion were treated as empty matches, and delete-request failures returned an empty success result. Enable the existing request error handling for scalar queries, ID deletion and delete-all, so HTTP failures and invalid JSON responses reach the caller.
Reuse the existing delete path and treat an accepted deletion request as success without waiting for index visibility. TTL can consume these errors for failure handling later; failure registration and retry scheduling are outside this change.
Validation
uv run --no-sync pytest -q -o addopts= tests/storage/test_volcengine_clients.py: 45 passed, including all eight cases of the single added parameterized regression.git diff --checkpass. Changed-file mypy reports the same 14 existing test diagnostics, with no errors in the production file or added regression. Imported modules have existing errors; mypy is not green.Human Involvement