Research-Stack/4-Infrastructure/storage/ZFS_PREFLIGHT.md
Brandon Schneider ac4e23dc9b Expand devcontainer with full Python stack, add MCP servers (Notion/AWS), strengthen Lean theorems
- .devcontainer/Dockerfile: add PostgreSQL client libs, OpenSSL/libffi headers, gfortran/BLAS for scipy, rclone; install full Python dependency set (boto3, psycopg2-binary, fastapi, uvicorn, notion-client, httpx, pytest, numpy, scipy, etc.) in uv-managed venv; add rclone S3 gateway init script as ENTRYPOINT
- .devcontainer/devcontainer.json: switch from build to pre-built image (localhost/research
2026-05-19 01:52:14 -05:00

265 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ZFS Pre-Flight Checklist
**Machine:** qfox-1 (100.88.57.96)
**Target kernel:** 7.0.9-1-cachyos (currently running 7.0.8-1-cachyos — one patch behind)
**Pool name:** `stackcache`
**Vdev:** sparse file at `/var/lib/stackcache/pool.img` (500 G) on `/dev/nvme0n1p2` (btrfs)
**Script:** `4-Infrastructure/storage/zfs-pool-setup.sh`
**Status:** ZFS module NOT loaded; reboot required first.
---
## 1. Block Device Inventory
```
NAME SIZE TYPE MOUNTPOINT FSTYPE
zram0 30.4G disk [SWAP] swap
nvme0n1 1.8T disk
├─nvme0n1p1 4G part /boot vfat
└─nvme0n1p2 1.8T part / btrfs
```
**Available space on btrfs root (`/dev/nvme0n1p2`):**
```
Size: 1.9 TB Used: 174 GB Avail: 1.7 TB Use%: 10%
```
**There is only one NVMe. No raw partitions or spare disks are available.**
The correct approach is a ZFS sparse-file vdev carved from the existing btrfs filesystem — exactly what `zfs-pool-setup.sh` does. Btrfs + ZFS file vdevs coexist safely; ZFS handles its own checksumming and compression independently of btrfs `zstd:1`.
---
## 2. ZFS Module Status
```
modinfo zfs → ERROR: Module zfs not found.
zpool list → The ZFS modules cannot be auto-loaded.
```
**Action required:** reboot into the 7.0.9-1-cachyos kernel once it is installed.
Verify after reboot with:
```bash
uname -r # expect 7.0.9-1-cachyos
lsmod | grep "^zfs" # must print a zfs line
modinfo zfs | head -3 # shows version
```
---
## 3. Exact Commands to Run After Reboot
Run these **in order** as root after confirming `lsmod | grep "^zfs"` is non-empty.
```bash
# 1. Confirm kernel and module
uname -r # 7.0.9-1-cachyos
lsmod | grep "^zfs"
# 2. Confirm free space (need ≥ 500 G available on btrfs)
df -h /var/lib # ≥ 500 G avail
# 3. Run the setup script (idempotent — safe to run again if pool exists)
sudo bash 4-Infrastructure/storage/zfs-pool-setup.sh
# 4. Verify pool came up
zpool status stackcache
zfs list -r stackcache
# 5. Confirm systemd services are enabled
systemctl is-enabled zfs-import-cache.service zfs-import-scan.service \
zfs-mount.service zfs.target
# 6. Smoke-test thermal zone mounts
ls /mnt/stackcache/hot/db
ls /mnt/stackcache/warm/pgdump
ls /mnt/stackcache/warm/rclone
ls /mnt/stackcache/cold/snap
# 7. Confirm quotas
zfs get quota stackcache/hot/db
zfs get quota stackcache/warm/pgdump
zfs get quota stackcache/warm/rclone
zfs get quota stackcache/cold/snap
```
---
## 4. Pool Layout Recommendation
Single NVMe — no redundancy is possible with one disk. The sparse-file approach is the right call: it avoids repartitioning and gives ZFS its own namespace on top of btrfs.
```
stackcache (500 G sparse file vdev, ashift=12, lz4 global)
├── hot/ (logbias=latency, primarycache=all, sync=standard)
│ └── hot/db quota=150G, recordsize=16K ← SQLite scratch
├── warm/ (logbias=throughput, primarycache=metadata, sync=disabled)
│ ├── warm/pgdump quota=150G, recordsize=128K, zstd-3 ← pg_dump / RDS
│ └── warm/rclone quota=50G, recordsize=32K ← rclone VFS cache
└── cold/ (logbias=throughput, primarycache=none, zstd-3, sync=disabled)
└── cold/snap quota=100G, recordsize=128K ← ZFS send/recv
```
Total quotas: **450 G** on a 500 G pool → **50 G headroom** for pool metadata,
snapshot churn, and ZFS internal bookkeeping.
Boot re-import path: `zfs-import-cache.service` reads `/etc/zfs/zpool.cache`
(written by `zpool create -o cachefile=`). The scan-based fallback
(`zfs-import-scan.service`) is also enabled for resilience.
---
## 5. Issues Found in `zfs-pool-setup.sh` and Fixes Applied
All issues were fixed in-place. `bash -n` syntax check passes.
### Issue 1 — **No thermal-zone hierarchy** *(structural, fixed)*
**Before:** Pool had flat datasets `stackcache/db`, `stackcache/pgdump`,
`stackcache/rclone`, `stackcache/snap`.
**After:** Datasets are nested under thermal-zone parents `hot/`, `warm/`, `cold/`
matching the UCM substrate schema:
```
stackcache/hot/db
stackcache/warm/pgdump
stackcache/warm/rclone
stackcache/cold/snap
```
Thermal-zone parents carry the tuning properties (`logbias`, `primarycache`,
`sync`) so child datasets inherit them, reducing per-dataset boilerplate and
making the routing intent explicit.
### Issue 2 — **Quota arithmetic left zero pool headroom** *(correctness, fixed)*
**Before:** `db=200G + pgdump=200G + rclone=50G + snap=50G = 500G` — exactly the
pool size. This left no free space for pool metadata, snapshot bookkeeping, or
ZFS internal structures, which would cause `ENOSPC` during normal operation.
**After:** `hot/db=150G + warm/pgdump=150G + warm/rclone=50G + cold/snap=100G = 450G`,
leaving 50 G (10%) free.
### Issue 3 — **`cachefile` not set on pool creation** *(reliability, fixed)*
**Before:** `zpool create` had no `-o cachefile=` argument. File-vdev pools are
not automatically found by `zfs-import-scan` unless the vdev path is known at
scan time, which is fragile.
**After:** `-o cachefile=/etc/zfs/zpool.cache` is passed to `zpool create`, and
`zfs-import-cache.service` is now enabled alongside `zfs-import-scan.service`.
The cache file is the canonical, reliable import path for file-vdev pools.
### Issue 4 — **`zfs-import-cache.service` not enabled** *(reliability, fixed)*
**Before:** Only `zfs-import-scan.service` was enabled.
**After:** Both `zfs-import-cache.service` and `zfs-import-scan.service` are
enabled. Cache-based import runs first; scan is the fallback.
### Non-issues confirmed
- **Idempotency:** `zpool list "${POOL_NAME}" &>/dev/null` guard at top —
already idempotent (prints status and exits 0 if pool exists). ✓
- **Device paths:** No hard-coded `/dev/sdX` paths — uses a sparse file under
`/var/lib/stackcache/`. The btrfs root has 1.7 TB free, well above the 500 G
vdev. ✓
- **`set -euo pipefail`:** Present. ✓
- **ZFS module check:** `lsmod | grep "^zfs"` guard present. ✓
- **`|| true` on `systemctl enable`:** Correct — prevents `set -e` abort if
a service unit name differs slightly across kernel/distro versions. ✓
---
## 6. Thermal Routing Score → ZFS Dataset Placement
The `UniversalThermalRouter.compute_thermal_signature()` function in
`.devcontainer/Universal Computational Modeling.md` produces a score in [0, 1]:
```
score += 0.30 if urgency=True (real-time required)
score += 0.02 per active_region (spatial locality, up to 0.20)
score += 0.20 if char_time < 1e-3 (fast dynamics, fs/ps timescale)
score += 0.15 if scar_density > 0.5 (historically failure-prone → demote)
score += 0.15 if parallel_efficiency < 0.5 (poor scaling → batch)
```
| Score range | Zone | ZFS dataset path | Garage / restic role |
|---|---|---|---|
| `score < 0.2` | **hot** | `stackcache/hot/db` | — (local scratch only) |
| `0.2 ≤ score < 0.7` | **warm** | `stackcache/warm/pgdump` or `warm/rclone` | Garage S3 batch (`db-scratch`, `rds-overflow`) |
| `score ≥ 0.7` | **cold** | `stackcache/cold/snap` → then offload | restic snap → Garage `snap-zone` → gdrive cold copy |
### Concrete routing examples
| Workload | Score drivers | Score | Zone → Dataset |
|---|---|---|---|
| Active SQLite DB write | urgency=T, char_time<1ms | 0.50 | warm `warm/pgdump` or `hot/db`\* |
| Interactive Lean proof search | urgency=T, active_regions>5 | 0.400.60 | warm → `warm/rclone` (VFS) |
| pg_dump / RDS COPY TO | urgency=F, large sequential | 0.150.30 | warm → `warm/pgdump` |
| ZFS send/recv from node | urgency=F, rare, archival | 0.70+ | cold → `cold/snap` |
| restic snapshot chunks | urgency=F, background | 0.70+ | cold → Garage `research-stack` |
\*SQLite scratch sits at the hot/warm boundary. Route to `hot/db` when
`urgency=True AND char_time < 1ms`; otherwise `warm/pgdump` is acceptable.
---
## 7. Recommended `storage_agent.py` Changes for ZFS Thermal Zones
These are **recommendations only** — not applied here, as `storage_agent.py`
already passes `py_compile` and is in active use.
### 7.1 Add ZFS observation probe
`storage_agent.py` currently observes Garage and restic but has no ZFS
awareness. After the pool is live, add a `_probe_zfs(obs)` method that calls
`zpool list -H -o name,health,allocated,free stackcache` and adds:
```python
obs.zfs_pool_health: str # "ONLINE" / "DEGRADED" / absent
obs.zfs_hot_used_pct_q16: int # Q16_16 fraction of hot/db quota used
obs.zfs_warm_used_pct_q16: int
obs.zfs_cold_used_pct_q16: int
```
### 7.2 Add ZFS routing in the `decide()` function
When a ZFS zone exceeds ~80% of quota (`0xCCCC` in Q16_16 ≈ 0.8), trigger
the appropriate offload:
```python
# trigger_zfs_hot_evict: when hot/db > 80% quota
# → mv stackcache/hot/db/... → warm, or flush to Garage db-scratch
# trigger_zfs_cold_offload: when cold/snap > 80% quota
# → db-consolidate.sh offload to Garage snap-zone
```
### 7.3 Route `storage_agent.py` offloads through thermal paths
Currently `trigger_offload` always calls `db-consolidate.sh offload` which
pushes to Garage `db-scratch`. After ZFS is live, the agent could:
1. Write hot SQLite scratchwork to `stackcache/hot/db`
2. On `trigger_offload`, first snapshot: `zfs snapshot stackcache/warm/pgdump@agent-<ts>`
3. Then send to Garage `snap-zone` via `zfs send | aws s3 cp` rather than plain rclone.
This keeps the thermal routing provenance in the ZFS snapshot lineage.
### 7.4 Add `snap-zone` bucket observation
`obs.garage_buckets` already lists buckets. Add a check that `snap-zone` exists
once ZFS is live (it is already defined in AGENTS.md), and alert if missing:
```python
if "snap-zone" not in obs.garage_buckets:
d.alerts.append("WARN: Garage snap-zone bucket missing — ZFS send/recv will fail")
```
---
## 8. Location Note
`AGENTS.md` (Infrastructure) lists `zfs-pool-setup.sh` under
`4-Infrastructure/storage/garage/` in the Garage scripts table. The file
actually lives at `4-Infrastructure/storage/zfs-pool-setup.sh` (one level up).
This is a documentation inconsistency — the file location is correct; the table
entry in AGENTS.md should be updated to point to `storage/zfs-pool-setup.sh`.