A Commit refused for disk Capacity leaves its packs in the Store, and every later write is refused Capacity

オープン
#379 コメント 2 件 リアクション 0 件 担当者 1 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@jiangkoumo がすでに取り組んでいます。

2026年11月8日 から。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
43/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
linux, rust, sqlite
領域
backend

調査の方向性

Start with 05-storage.md and the Store pack persistence and reclamation paths; the issue identifies Handles::reclaim_space and a reproducible failure during workspace.commit. Trace how packs written before a refused commit are retained and how fs.write maps a persistence Capacity error. Done means failed commits leave unreferenced packs reclaimable, a small write succeeds after the workspace shrinks, and Capacity is reported consistently.

索引モデルが issue の本文から書いたものです。

説明

bug

Summary

When workspace.commit hits a full disk, it is refused Failed | pack persistence: Refused { status: "Capacity" }, and nothing is committed. That part is correct. But the packs it already wrote stay in store.sqlite (about 273 MB in this repro). Nothing references them, and no public or operator call removes them. After that, every write on that Store is refused Capacity: a 1 MB Commit, a write on another Branch, and a 5-byte fs.write from a new process. Deleting the large file from the Workspace does not help. The only way out is to free disk outside LayerFS or delete the Store.

Environment

  • main 5fb68ddc1 (1.0.0 plus #376–#378). Also reproduces on 792715f.
  • x86_64 Debian 13, daemon and addon built on the host with cargo +1.85.1 --offline --locked --release.
  • Store on ext4. Runtime image is Node 22 on trixie.

Reproduce

To fill the disk quickly, the Store volume is a plain local volume whose _data is bind-mounted from a 300 MiB ext4 loop disk. Any Store disk that fills up should behave the same way.

  1. project.init a 26 KB project, branch.fork("f"), then workspace.mount("f").

  2. In the Workspace: head -c 400000000 /dev/urandom > big.bin.

  3. workspace.commit(ws) → Failed: physical Store I/O: pack persistence: Refused { status: "Capacity" }, with detail.phase = "Commit Construct". list_commits("f") is empty, which is correct.

  4. operator.store.stats():

    before after the refused Commit
    logical 26,260 26,260
    canonical 30,259 272,529,372
    stored 13,066 275,696,868
    free — 0

    The disk has 268 MiB used and 2 KiB free. store.sqlite is 275,591,168 bytes.

  5. The same Workspace runs rm big.bin and writes 1 MB, then commits → Refused { status: "Capacity" }, with phase "Commit Begin".

  6. branch.fork("g") + fs.write("g", "/x.txt", "hello") → Capacity: capacity: persistence.

  7. A new process opens the Store fresh, with the old Sandbox gone. stats() is unchanged.

    • fs.write("f", "/tiny.txt", "hello") → Unknown: Service(History(Capacity("persistence")))
    • fork + mount + commit → Capacity
Repro script

The Store volume lfsb-full is a local volume whose _data is bind-mounted from a 300 MiB ext4 loop disk (truncate -s 300M img; mkfs.ext4 -m 0 img; mount -o loop img /mnt/small; mount --bind /mnt/small/store /var/lib/docker/volumes/lfsb-full/_data). The LAYERFS_* env points at that volume.

import { LayerFS } from "./sdk/js/src/index.js";
import { LayerFSOperator } from "./sdk/js/operator/index.js";
const [project, src] = process.argv.slice(2);
const lfs = new LayerFS(project); const op = new LayerFSOperator(lfs);
const T = async (k, f) => { try { const r = await f(); console.log(k, "ok", JSON.stringify(r).slice(0, 400)); return r; } catch (e) { console.log(k, "FAIL", e.code, e.message, JSON.stringify(e.detail)); return null; } };
try {
  await T("init", () => lfs.project.init(src));
  await T("fork", () => lfs.branch.fork("f"));
  const ws = await T("mount", () => lfs.workspace.mount("f"));
  await T("write-400MB", () => lfs.workspace.exec(ws, "head -c 400000000 /dev/urandom > big.bin"));
  await T("commit-full", () => lfs.workspace.commit(ws, "too big"));
  await T("stats", () => op.store.stats());
  await T("list-commits", () => lfs.branch.list_commits("f", 5, 0));
  await T("shrink", () => lfs.workspace.exec(ws, "rm big.bin && head -c 1000000 /dev/urandom > mid.bin"));
  await T("commit-small", () => lfs.workspace.commit(ws, "small"));
  await T("fs.write-other-branch", () => lfs.branch.fork("g").then(() => lfs.fs.write("g", "/x.txt", "hello", "w")));
  await T("unmount", () => lfs.workspace.unmount(ws, true));
  await T("stats-end", () => op.store.stats());
} finally { await lfs.close(); }

The reopen step is a second process: stats(), fs.write("f", "/tiny.txt", "hello"), then fork + mount + commit.

Expected

  • A Commit refused before publication should leave no persistent bytes. Its packs should be deleted on the definite failure, or reclaimed on the next open, or a documented operator call should be able to reclaim them. After the Workspace shrinks, a small Commit should succeed.
  • stats() should not count packs that nothing references.
  • fs.write should refuse Capacity here, not Unknown. Per #372, Unknown also keeps its Store call Workspace.

Notes

  • I checked issues and PRs through #378 and found no report of this. #311 and #321 cover Workspace-overlay reclamation, and they say that local overlay maintenance does not delete global canonical data. #372 is a different Capacity, the leak of call Workspaces, but it shares the Unknown mapping in the last bullet above.
  • 05-storage.md mentions Handles::reclaim_space. It does not seem to be reachable from the SDK or the operator, and I did not check whether it would release these packs.
  • I have not tried a fix. It seems to need Store-side custody of the attempt's packs, for example a pending owner row that is deleted on a definite failure plus a reclaim on open for crashed attempts. I would rather hear how you want it done first. I'm happy to send a PR for the Unknown → Capacity mapping on its own.
主要言語
Rust
スター
2.1k
フォーク
110
平均マージ
23時間 52分
マージ済み PR(30日)
138

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

Ephemeral-AI-Lab/layerfs のほかの issue

Ephemeral-AI-Lab/layerfs の issue をすべて見る

似ている issue

Rust の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。