# Experiment の復帰と cleanup

{mod}`ommx.experiment` は、最適化の試行錯誤を 1 つの OMMX Artifact として記録するための API です。Experiment のデータモデル、実行できる logging 例、共有、確認、fork については [実験を記録して共有する](../tutorial/experiment_management.md) を参照してください。

このページでは、失敗時の挙動に絞って説明します。Experiment が commit される前に何が書かれるのか、checkpoint からどう復帰するのか、Local Registry cleanup がどの blob を削除可能と判断するのかを扱います。

## 保存の境界

Experiment のデータは 3 つの層に分かれて保存されます。

| 層 | 保存先 | 役割 |
|---|---|---|
| Blob | Local Registry の content-addressed file | Attachment、Instance、Solution、run parameters、config、manifest の bytes |
| Manifest | OCI Image Manifest blob | 1 つの immutable な OMMX Artifact を構成する blob 一覧 |
| Ref | Local Registry index の SQLite rows | manifest を到達可能にする名前または checkpoint pointer |

このページで **publish** と呼ぶのは、Local Registry の ref を更新して、
すでに書き込まれた manifest を指すようにする操作です。これは local SQLite
上の操作であり、Artifact を remote container registry に push することでは
ありません。

{py:meth}`~ommx.experiment.Experiment.log_json` や {py:meth}`~ommx.experiment.Run.log_solve` のような logging method は、payload bytes をすぐ Local Registry に書き込みます。Experiment の最後まで全データをメモリに保持してから一括保存するわけではありません。同じ内容が既に存在する場合は既存の CAS blob を再利用し、その modification time を更新します。これにより、最近の active write は GC の grace period で保護されます。

正常に {py:meth}`~ommx.experiment.Experiment.commit` すると、Experiment config と root manifest が書かれ、requested image reference が SQLite に publish されます。ref の publish は payload blob を書き直しません。この順序のため、process が途中で終了すると、どの manifest や ref からも到達できない blob file が残ることがあります。そのような blob は Local Registry GC の対象になります。

## Run context と Experiment commit

`Run` は context manager として使ってください。Run は 1 つの試行であり、
Run を close することが、その close 済み Run を親 Experiment の未 commit 状態へ
追加する復帰境界になります。Run が close された後、OMMX はその親 Experiment の
draft checkpoint を書き込み、checkpoint ref を publish します。

一方、Experiment は必ずしも context manager として使う必要はありません。
notebook では、1 つの Experiment を複数 cell にまたがって開いたままにするのが
自然な workflow です。1 つの Run を実行し、その結果を可視化・考察し、
次の条件を決めて別の Run を開始し、人間の workflow が終わった時点で明示的に
commit します。

```python
from ommx.experiment import Experiment

image_name = "ghcr.io/example/team/experiment:baseline"

experiment = Experiment(image_name)
experiment.log_json("dataset", {"name": "demo"})

with experiment.run() as run:
    run.log_parameter("capacity", 47)

# 結果を確認し、plot し、次の条件を決める。

with experiment.run() as run:
    run.log_parameter("capacity", 64)

artifact = experiment.commit()
```

すべての Run があらかじめ決まっている batch script では、
`with Experiment(...)` は便利な書き方です。正常終了時には `commit()` を呼び、
例外終了時には成功用の image reference を進めず failed または interrupted
checkpoint を publish します。

| 操作または event | 保存される状態 |
|---|---|
| `Run` が正常終了する | close 済み Run が status `"finished"` として親 Experiment に追加され、その Experiment の best-effort draft checkpoint が publish されます。 |
| `Run` が例外で終了する | close 済み Run が status `"failed"` または `"interrupted"` として親 Experiment に追加され、その Experiment の best-effort draft checkpoint が publish されます。例外はそのまま伝播します。 |
| `experiment.commit()` が成功する | final Experiment が commit され、requested image reference が publish されます。その Experiment の local checkpoint があれば削除されます。 |
| `with Experiment(...)` が正常終了する | block 末尾で `commit()` を呼ぶのと同じです。 |
| `with Experiment(...)` が例外で終了する | 成功用の requested image reference は進みません。status `"failed"` または `"interrupted"` の checkpoint Experiment が publish されます。 |
| Run close 後、`commit()` 前に notebook kernel または process が終了する | Run close 後に作られた最新の Experiment draft checkpoint から復帰します。 |
| open `Run` の exit 前に notebook kernel または process が終了する | その open Run が書いた payload blob は存在する可能性がありますが、復帰可能な Run state には含まれません。復帰地点は、その Run より前の最新 checkpoint です。 |

`KeyboardInterrupt` は Run / Experiment ともに `"interrupted"` として記録されます。それ以外の例外は `"failed"` として記録されます。

Experiment を context manager として使わない場合、Run の外側で起きた例外は
failed Experiment checkpoint を自動 publish しません。通常の interactive workflow
では、探索中は Run close 後に作られる Experiment draft checkpoint によって復帰可能性を確保し、
Experiment を公開できる状態になった時点で明示的に
{py:meth}`~ommx.experiment.Experiment.commit` します。

## Checkpoint から復帰する

checkpoint から復帰するには、元の Experiment image name を渡します。

```python
from ommx.experiment import Experiment

image_name = "ghcr.io/example/team/experiment:baseline"

experiment = Experiment.restore_from_checkpoint(image_name)

with experiment.run() as run:
    run.log_parameter("capacity", 64)

artifact = experiment.commit()
```

checkpoint ref は元の image name から導出される internal Local Registry ref です。通常の Artifact handle としては公開されないため、復帰したい Experiment では元の image name を保持してください。

restore された Experiment は未 commit の Experiment なので、新しく作った
Experiment と同じように notebook cell をまたいで開いておけます。`commit()` を
呼ぶと元の requested image reference に publish され、checkpoint は削除されます。
restore 後の Experiment を context manager として使い、そこで再び失敗した場合は、
成功用の image reference を進めず、新しい failed または interrupted checkpoint が
publish されます。

## 失敗後の到達可能性

Local Registry cleanup は SQLite refs からの到達可能性で判断します。

| Data | 到達可能か | cleanup の挙動 |
|---|---|---|
| commit 済み Experiment image ref | Yes | `ommx gc` は manifest、config、layers、subject chain を保持します。 |
| Experiment checkpoint ref | Yes | `ommx gc` は復帰できるよう checkpoint を保持します。正常 commit すると checkpoint は削除されます。 |
| fork 先 Experiment から OCI `subject` で参照される parent manifest | child ref が保持されていれば Yes | `ommx gc` は subject chain を辿り、保持されている child から到達可能な parent payload を保持します。 |
| anonymous artifact refs | ref が存在する間は Yes | `ommx prune-anonymous` でこれらの ref を削除します。その後の `ommx gc` で、到達不能になった blob を回収できます。 |
| manifest/ref publish 前に process が終了して残った blob | No | grace period を過ぎると `ommx gc` が orphan candidate として report します。 |
| 実行中 process が書いている blob | checkpoint または commit されるまでは通常 No | `ommx gc` は grace period より新しければ deferred として削除を見送ります。 |

OMMX は SQLite に orphan table を作りません。orphan は GC report のたびに refs と manifests を辿って reachable set を作り、それを Local Registry の CAS file と比較して計算します。

## Cleanup workflow

cleanup command はまず report mode で実行してください。

```bash
ommx prune-anonymous
ommx gc
```

どちらもデフォルトでは dry-run で、registry を変更するのは `--delete` 指定時だけです。

```bash
ommx prune-anonymous --delete
ommx gc --delete
```

同じ操作は Python SDK からも実行できます。Python API は整形済みの CLI output ではなく、
structured report を返します。

```python
from ommx.artifact import gc, prune_anonymous

prune_report = prune_anonymous()
gc_report = gc()

prune_deleted = prune_anonymous(delete=True)
gc_deleted = gc(delete=True)
```

default 以外の Local Registry を調べる場合は `root=...`、GC の grace period を
変える場合は `grace_period="2h"` を指定します。

一時的な Artifact build や名前なし archive import から anonymous Artifact ref が残っている場合は、先に {command}`ommx prune-anonymous` を実行します。この command は該当する SQLite refs だけを削除し、blob は unlink しません。その blob は、他の ref から到達できなければ {command}`ommx gc` で回収可能になります。

{command}`ommx gc` は mark-sweep を行います。

- root は Experiment checkpoint refs を含むすべての SQLite refs です。
- 到達可能な manifest ごとに、manifest blob、config blob、layer blobs、OCI `subject` manifest chain を mark します。
- mark されなかった blob file は unreachable です。
- unreachable かつ `--grace-period` より古い blob は orphan candidate として report されます。
- unreachable だが `--grace-period` より新しい blob は deferred として report されます。
- `--delete` 指定時は orphan candidate だけを unlink し、削除直前にも各 candidate を再確認します。

default grace period は `24h` です。`s`、`m`、`h`、`d` suffix を指定できます。

```bash
ommx gc --grace-period 2h
ommx gc --grace-period 0s
```

`0s` は、その registry に書き込み中の OMMX process がないと分かっている場合だけ使ってください。共有 registry や default Local Registry では、open Run や interrupted import の書き込みを消さないよう、非ゼロの grace period を残してください。

通常の report は raw digest ではなく件数と byte size を表示します。特定の missing、invalid、orphan、deferred blob を調べる場合は `--show-digests` を追加します。

```bash
ommx gc --show-digests
ommx gc --delete --show-digests
```

default 以外の Local Registry を調べる、または掃除する場合は `--root <path>` を指定します。
