Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
既存の世代はそのまま一覧・復元できます。

### Fixed
- **既にある世代の名前で `devbase snapshot create --name <名前> --full` を打つと、その世代を今のボリュームの全体で
作り直すようにしました(#458)。** これまでは `full.tar.zst` を作り直せずに終了コード 1 で止まり、そのとき
`snapshot.snar` だけが進んでいました。作り直した世代は前のフルと差分を持たず、その世代から前の時点へは戻せません。
前の時点を残したいときは別の名前で作るか、先に `devbase snapshot copy` で写してください。控えに失敗したときは
世代が打つ前のまま残ります。入れ替えの途中で止まった世代は `restore` がボリュームを消す前に止まり、同じ名前で
もう一度 `--full` を打つと作り直せます。
**これまでに既存の世代の名前へ `--full` を打って失敗したことがあり、その後に同じ世代へ差分が積まれていれば、
その世代の差分は失敗の前の変更を欠いていることがあります。** 世代のファイルからは見分けられないため、
心当たりがあれば新しい世代を作ってください(`devbase snapshot create`)。
- **プロジェクトの `env` に `DEV_SERVICE_NAME=` と空で書いたとき、開発サービス名を `dev` として扱うようにしました(#425)。**
これまで `devbase build` は `dev` を建てる一方、`up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定・
token の配布は空の名前のサービスを探して開発サービスを見失っていました。未設定や空でない値のプロジェクトは変わりません。
Expand Down
9 changes: 5 additions & 4 deletions docs/specifications/snapshot-exclude.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
| 世代の記録 | 同 `_recorded_excludes` | 世代の `meta.yml` の `exclude` を読む。無い・リストでない・文字列でない要素を含むなら `None` |
| 比較 | 同 `_excludes_differ`・`_incremental_block_reason` | 世代の記録が今の一覧と違うかを決める。差分を積めない理由(対象ボリューム → 控えの起点 → 除外の一覧の順)の最後に当てる |
| 控えに使う一覧 | 同 `_excludes_for`・`_latest_recorded_excludes` | 設定を読めたら今の一覧、読めなければ記録の一覧を返す(下の「読めない設定の間」) |
| tar のコマンド | 同 `backup_command(archive, volumes, excludes)` | 除外の引数を `-C` とメンバーより前に置く |
| tar のコマンド | 同 `backup_command(archive, volumes, excludes, snar)` | 除外の引数を `-C` とメンバーより前に置く。`snar` は既定で `snapshot.snar`、世代の作り直しは `snapshot.snar.new` |
| 作成 | 同 `_create_full`・`_create_incremental` | `_excludes_for` の一覧で控える。full は `meta.yml` に `exclude` を書き、差分は書き換えない |

世代に関わる判断(記録・比較・積み先)は `manager.py` に置き、`exclude.py` は世代の状態に依らない処理だけを
Expand All @@ -58,7 +58,7 @@ classDiagram
class SnapshotManager {
+excludes: tuple~str~
+exclude_config_readable: bool
+backup_command(archive, volumes, excludes)$ str
+backup_command(archive, volumes, excludes, snar)$ str
+auto_snapshot_target(max_incrementals) str|None
-_exclude_resolution: tuple
-_create_full(name, snap_dir, volumes)
Expand Down Expand Up @@ -261,8 +261,9 @@ exclude:
Claude Code は最初のセッションが始まるまで `claude plugin list` がプラグインを `cache-miss` と出し、最初の
セッションでマーケットプレイスとプラグインを取り直して `installed_plugins.json` の一覧のまま読み込む。
Codex は消した `logs_2.sqlite` と `cache` を作り直す
- 既存の世代の名前へ `--full` を打つと full が変更分だけになる不具合は #458 にある。除外の一覧が違う世代への
案内は「新しい世代を作成してください (devbase snapshot create)」とし、`--name` に既存の名前を勧めない
- 既存の世代の名前へ `--full` を打つと、その世代を作り直した時点の除外の一覧で作り直す(世代の作り直し。
[スナップショットの系列](snapshot-series.md#世代の作り直し))。前の差分は消え、前の時点へは戻せない。除外の一覧が
違う世代への案内は「新しい世代を作成してください (devbase snapshot create)」とし、`--name` に既存の名前を勧めない

## テスト観点

Expand Down
61 changes: 61 additions & 0 deletions docs/specifications/snapshot-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
| 系列の最新の世代 | 系列の中で `created_at` が最も新しい世代。同じ値なら `snapshot.yml` で後ろのもの |
| 全体の上限 | 系列をまたいで数えた世代の数の上限(`max_total`) |
| 復元前バックアップ | `restore` が書き戻す前に自動で作るフルの世代 `pre-restore-<時刻>`。復元する世代の対象ボリュームの組を控える。実行時の `DEVBASE_ACCOUNT_GROUP` には依らない。控えに失敗したら警告を出してそのディレクトリを消し、控え無しで復元を続ける |
| 世代の作り直し | 既にある世代のディレクトリへ、前の `snapshot.snar` を引き継がずに新しい full を作り、前の差分と状態のファイルを除くこと。`--full` を既存の名前へ打ったとき・snar の無い世代への差分の指定・同じ秒に重なった復元前バックアップで起きる |

## 構成要素

Expand All @@ -65,6 +66,7 @@
| 積み先の判定 | 同 `auto_snapshot_target` | 積み先の世代の名前か、新しい世代を作るべきこと(`None`)を返し、理由をログへ出す |
| 最小間隔の元 | 同 `last_snapshot_time(volumes=None)` | 渡された組の系列の世代のアーカイブの mtime の最大を返す |
| 保持 | 同 `rotate(keep, max_total)` と、副作用の無い `_rotation_plan` | 削除候補を決め、場所を検証してから消す |
| 世代の作り直し | 同 `_create_full` / `_install_rebuilt_full` / `_discard_rebuild_temps` | 前のファイルを持つ世代へ full を作るとき、仮の名前へ控えてから決まった順で入れ替える(下の「世代の作り直し」) |
| 世代の場所の検証 | 同 `_safe_snap_dir` | 名前・シンボリックリンク・`backups/` の外を `SnapshotError` で止める |
| 自動スナップショット | `lib/devbase/commands/container.py` の `_auto_snapshot` / `_should_skip_by_interval` | 最小間隔を系列で判定し、判定の結果に従って `create` と `rotate()` を呼ぶ |
| 自動ローテーション | 同 `cmd_down` | `mgr.rotate()` を既定の引数で呼ぶ |
Expand Down Expand Up @@ -249,6 +251,45 @@ classDiagram
(`name` が無い・上の 3 つ・場所にディレクトリ以外がある)で不正なエントリを判定し、中を読まずに警告して
一覧から外す。`snapshot.yml` は書き換えない

### 世代の作り直し

`_create_full` は、世代に前のファイル(`full.tar.zst`・`snapshot.snar`・`incr-*.tar.zst` のどれか)があれば
世代の作り直しとして控える。無ければ本来の名前(`full.tar.zst`・`snapshot.snar`)へ直接控え、最後に
`meta.yml` を書く(新しい世代の控えのコマンドと `meta.yml` はこの規則の前と同じ)。

作り直しの手順:

1. 前の中断の残りの仮の名前のファイル(`full.tar.zst.new`・`snapshot.snar.new`・`meta.yml.new`)を消す
2. 前の `snapshot.snar` を tar に渡さず、空から始まる `snapshot.snar.new` と `full.tar.zst.new` へ控える
(tar はレベル 0 でボリュームの全体を控える)。除外の一覧は新しい世代と同じ規則(`_excludes_for`)で決める
3. 控えが成功したら `_install_rebuilt_full` が次の順に 1 つずつ入れ替える。改名は同じディレクトリの中の
`os.replace` で、ファイルを写さない
1. `snapshot.snar.bak` と `snapshot.snar` を消す
2. `incr-*.tar.zst` を番号の大きい順に消す
3. `meta.yml` を新しい控えの内容に `rebuilding: true` を足した形へ置き換える
4. `full.tar.zst.new` を `full.tar.zst` へ改名する
5. `meta.yml` を新しい控えの内容(印無し)へ置き換える
6. `snapshot.snar.new` を `snapshot.snar` へ改名する
4. INFO を 1 行出す。`snapshot.yml` のエントリは `create` / `_backup_before_restore` が今の規則で更新する
(`incremental_count` と `updated_at`。`created_at` は変えない)

`meta.yml` の置き換えは `meta.yml.new` へ書いてから改名し、途中まで書いた `meta.yml` を残さない。
作り直した `meta.yml` は `type: full`・`files: [full.tar.zst]`・`incremental_count: 0`・作り直した時点の
`volumes`・`exclude`・`created_at` を持つ。

常に成り立つ条件:

- 控えのコマンドが失敗したら、仮の名前のファイルを消して `SnapshotError` を投げ直す。世代の既存のファイルは
1 バイトも変わらず、ファイルは増えない
- 入れ替え(手順 3)の途中で止まった世代は `snapshot.snar` を持たない。次の `create`(名前の指定・自動
スナップショット)は snar の無い世代の経路で作り直す
- 入れ替えの途中で止まった世代は、次のどちらかである
- `meta.yml` に印が無く、`full.tar.zst` と `meta.yml` が同じ 1 回の控えのもので、前の full から番号の
小さい順に続く差分とともにある時点へ戻せる
- `meta.yml` が `rebuilding: true` を持つ。`restore` はこの世代を、復元前バックアップとボリュームの消去の前に
`SnapshotError` で止める
- 作り直した後は、その世代から前の時点へ戻す手段は無い

## データ・設定

**形は変えていない。** 移行は無く、変更前の devbase も同じファイルを読める。各世代の `meta.yml` に
Expand All @@ -261,6 +302,7 @@ classDiagram
| 同 `snapshots[].incremental_count` | 差分の上限の判定 |
| 同 `max_generations` | `rotate` が消したときに `keep` を書く。読む側は無い |
| 各世代の `meta.yml` の `volumes` | 系列の判定には使わない。積む直前の検証(`auto_snapshot_target` の順 3 と `_create_incremental`)でだけ読む |
| 各世代の `meta.yml` の `rebuilding` | 世代の作り直しの入れ替えの途中だけ `true`。持つ世代は `restore` が止める。作り直しを終えた世代と新しい世代は持たない |
| 各世代の `meta.yml` の `exclude` | full を作ったときの除外の一覧(並べ替え済み)。差分では書き換えない。積む直前の検証(順 4a と `_create_incremental`)でだけ読む |
| アーカイブの mtime | 最小間隔の判定 |

Expand All @@ -282,6 +324,7 @@ classDiagram
| 全体の上限で消した | INFO | `ローテーション: 全体の上限 {max_total} 世代を超えたため、{系列} の {名前} を削除しました` |
| 全体の上限を満たせない | WARNING | `全体の上限 {max_total} 世代を超えていますが、各グループの最新の世代は消さないため {残り} 世代を残します` |
| 場所が不正なエントリ | WARNING | `snapshot.yml の世代 '{名前}' は場所が不正なため、ディレクトリを消さずに一覧からだけ外します: {理由}` |
| 世代を作り直した | INFO | `世代 {名前} を今のボリュームの全体で作り直しました(前の差分 {本数} 本を除きました)` |
| `snapshot rotate` で消す候補が無い | INFO | `ローテーション不要です`(`_snapshot_rotate`。消した数も外した数も 0 のときだけ) |
| `snapshot rotate` で一覧から外しただけ | INFO | `ローテーション: 場所が不正な {数} 世代を一覧から外しました(削除した世代はありません)`(`_snapshot_rotate`) |
| `snapshot rotate` で世代を消し、一覧からも外した | INFO | `ローテーション: 場所が不正な {数} 世代を一覧から外しました`(`_snapshot_rotate`。`rotate` の削除の INFO の後) |
Expand All @@ -299,6 +342,10 @@ classDiagram
| `rotate` の `keep` / `max_total` が 1 未満 | `SnapshotError`。何も消さず `snapshot.yml` も書かない |
| `rotate` の候補が `_safe_snap_dir` を通らない | ディレクトリを消さず、一覧から外して警告。他の候補の削除は続ける |
| `create` / `restore` / `copy` / `delete` の世代が `_safe_snap_dir` を通らない | `SnapshotError` → CLI は終了コード 1 |
| 世代の作り直しの控えのコマンドが失敗 | 仮の名前のファイルを消して `SnapshotError`(`Dockerでのtar操作に失敗しました: {stderr}`)→ CLI は終了コード 1。世代は打つ前のまま |
| 世代の作り直しの入れ替えが失敗 | 仮の名前のファイルを消して `SnapshotError`(`世代 '{名前}' の作り直しを終えられませんでした: {理由}。もう一度 devbase snapshot create --name {名前} --full を打ってください`)→ CLI は終了コード 1 |
| `meta.yml` に `rebuilding: true` を持つ世代の `restore` | 復元前バックアップを作らず、ボリュームを消さずに `SnapshotError`(`世代 '{名前}' は作り直しの途中で止まっているため復元できません。devbase snapshot create --name {名前} --full を打って作り直してください`)→ CLI は終了コード 1 |
| 仮の名前のファイルを消せない | 警告 1 行で続ける。次の作り直しが控えの前に消す |

## 運用

Expand Down Expand Up @@ -365,6 +412,20 @@ CLI と TUI:
- TUI の問いの文言と、`max_total` を渡さないこと(`tests/cli/tui/test_actions_snapshot.py`)
- `cmd_down` の `rotate()` の失敗で終了コード 0(`tests/commands/test_container_down_snapshot.py`)

世代の作り直し(`tests/snapshot/test_rebuild_generation.py`、`tests/snapshot/test_backup_real_tar.py`):

- 差分を持つ世代へ `--full` を打つと、`full.tar.zst`・`snapshot.snar`・`meta.yml` だけが残り、`meta.yml` が
full の形で今の除外の一覧を持ち、`snapshot.yml` のエントリが 1 件のまま `created_at` を変えないこと。
控えのコマンドが前の snar を受け取らないこと
- 控えのコマンドの失敗で世代のファイルのバイト列と `snapshot.yml` が変わらないこと。入れ替えの各段で
止めると `SnapshotError` になり、snar と仮の名前のファイルが残らず、次の差分の指定が作り直すこと
- 別のボリュームの組で作り直して止めた世代は、印があれば `restore` が docker を 1 回も呼ばずに止まり、
印が無ければ `full.tar.zst` と `meta.yml` の組が合うこと。差分が番号の大きい順に消えること
- snar の無い世代への差分の指定と、同じ秒の復元前バックアップが作り直しになること。新しい世代の控えの
コマンドがこの規則の前と同じであること
- 実の GNU tar と zstd で、作り直した full がボリュームの全体を持ち、復元で全ファイルが作り直した時点の
中身で戻り、その後の差分が後の変更だけを持つこと(GNU tar と zstd が無ければ飛ぶ)

復元のテスト(`tests/snapshot/test_restore_incremental.py`、`test_manager_volumes.py` の復元)は
変えずに通ること。ただし `test_restore_incremental.py` の実の Docker で入れ替える場所は、既定の除外
(#368)に入らない `data/cache` に置く。
Expand Down
6 changes: 6 additions & 0 deletions docs/user/cli-reference/05-snapshot.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ devbase snapshot create [--name NAME] [--full] [--group NAME]
既にある世代の名前を `--full` なしで指定すると、その世代へ差分を積みます。その世代が今と違う除外の一覧で
作られている(除外の一覧を記録していない世代を含む)ときは、差分を作らずに終了コード 1 で止まります。

既にある世代の名前を `--full` 付きで指定すると、その世代を今のボリュームの全体で作り直します。前の
フルと差分は消え、その世代から前の時点へは戻せません。前の時点を残したいときは、別の名前で作るか、
先に `devbase snapshot copy` で写してください。作り直しに失敗したときは終了コード 1 で止まり、控えの
コマンドが失敗した場合は世代が打つ前のまま残ります。入れ替えの途中で止まった世代は、もう一度同じ名前で
`--full` を打つと作り直せます(それまで復元は止まることがあります)。

控えには、既定の除外(作り直せる一時ファイル・ログ・取り直せる取得物)と `backups/exclude.yml` に
書いた除外が入りません。一覧と書き方は
[スナップショットガイドの「控えから外すもの」](../snapshot-guide.md#控えから外すもの) にあります。
Expand Down
10 changes: 10 additions & 0 deletions docs/user/snapshot-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,16 @@ devbase snapshot create --full
devbase snapshot create --name before-migration --full
```

既にある世代の名前を `--full` 付きで指定すると、その世代を今のボリュームの全体で作り直します。
前のフルと差分は消え、その世代から前の時点へは戻せません。前の時点を残したいときは、別の名前で作るか、
先に `devbase snapshot copy <名前> <新しい名前>` で写してください。

- 控えのコマンドが失敗したときは終了コード 1 で止まり、世代は打つ前のまま残ります
- 控えの後の入れ替えで止まったときも終了コード 1 で止まり、同じ名前でもう一度 `--full` を打つよう案内します。
入れ替えの途中で止まった世代を `restore` すると、ボリュームを消す前に「作り直しの途中で止まっているため
復元できません」と出て止まります
- 作り直したときは「世代 <名前> を今のボリュームの全体で作り直しました(前の差分 N 本を除きました)」と出ます

### スナップショットの一覧

```bash
Expand Down
Loading
Loading