mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
- .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
265 lines
10 KiB
Markdown
265 lines
10 KiB
Markdown
# 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.40–0.60 | warm → `warm/rclone` (VFS) |
|
||
| pg_dump / RDS COPY TO | urgency=F, large sequential | 0.15–0.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`.
|