feat(vds-gc-cron): done — weekly registry GC + verdaccio prune armed

Scripts on VDS at /opt/stacks/gc/scripts/, /etc/cron.d/vds-gc Sun 03:00/03:30 MSK.
Verdaccio prune smoke: 8.5G → 6.8G (1.7G freed, 4524 tgz, 1551 pkgs mod, 0 err).
Registry GC smoke: alpine push → DELETE manifest → 61M freed. Empty-storage guard added.
Notifications via ntfy vds-ops (low on small/quick, high on fail).

New wiki concept verdaccio-prune-semantics — _distfiles keyed by tarball
filename (NOT version string), locally-published (@snollajs/*) protected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 00:23:40 +03:00
parent aedfddb348
commit 61826cb1ef
2 changed files with 48 additions and 52 deletions

View File

@@ -1,5 +1,5 @@
# Task Board
_Updated: 2026-05-20_
_Updated: 2026-05-21_
## 🟢 [cms-admin-assets-root-folders-seed] — seed 15 missing root AssetsFolder rows в DB, admin assets открывается
**Status:** done (2026-05-19 вечер). Browser-verified user'ом на pilorama98/emspb. Detail в [[cms-admin-assets-root-folder-seed]].
@@ -45,10 +45,12 @@ _Updated: 2026-05-20_
---
## [vds-gc-cron] — cron GC для verdaccio и registry на VDS
**Status:** ready (заведена 2026-05-20). Детали + sketch implementation в [vds-gc-cron.md](vds-gc-cron.md).
**Where I stopped:** task создана как follow-up к [[vds-kzntsv-bootstrap]] Phase 3 — после стабилизации миграции нужно автоматизировать periodic GC чтобы registry не разрастался снова до 99G, и verdaccio storage не пух от старых версий.
**Next action:** дождаться завершения vds-kzntsv-bootstrap Phase 3, потом написать cron + GC-скрипты + проверить.
## 🟢 [vds-gc-cron] — weekly GC armed для verdaccio + registry на VDS
**Status:** done (2026-05-21 00:21 MSK; ~1ч работы). Детали в [vds-gc-cron.md](vds-gc-cron.md).
**Result:** `/etc/cron.d/vds-gc` armed — Sun 03:00 MSK registry-gc.sh, Sun 03:30 MSK verdaccio-prune.sh. Scripts в `/opt/stacks/gc/scripts/` (`notify.sh`, `registry-gc.sh`, `verdaccio-prune.sh`+`verdaccio-prune.js`). Logs в `/var/log/vds-gc/`. Notify через ntfy `vds-ops`.
**Verdaccio prune real run:** 8.5G → 6.8G (freed 1.7G), 7021 packages scanned, 1551 modified, 4524 tgz deleted, 0 errors, 73 locally-published versions защищены (@snollajs/* + similar — detected via `_distfiles[tgzName]` absent). Atomic JSON rewrite (`tmp + rename`) + `_rev` bump.
**Registry GC smoke:** alpine v1+v2 pushed → DELETE manifest → GC freed 61M (whole tree, since both tags pointed одну digest). Empty-storage guard added (first run на свежем registry — `/var/lib/registry/docker/registry/v2/repositories` отсутствует → skip+min-prio notify, не FAIL).
**Atomic revert:** `ssh vitya@89.253.255.94 'sudo rm /etc/cron.d/vds-gc; sudo systemctl restart cron; sudo rm -rf /opt/stacks/gc /var/log/vds-gc'`
**Branch:** master
---

View File

@@ -2,58 +2,52 @@
## Goal
Настроить ежедневный (или другой регулярный) cron GC для двух container-сервисов на VDS:
1. **Registry** (`/opt/stacks/registry/`) — `docker exec registry registry garbage-collect /etc/docker/registry/config.yml`. Иначе старые блобы не удалённых тэгов накапливаются — 99G после нескольких лет накопления было исходно (см. [[vds-kzntsv-bootstrap]] Phase 3.3).
2. **Verdaccio** (`/opt/stacks/verdaccio/`) — нет встроенной GC, но есть несколько подходов:
- `npm-cache clean --force` внутри контейнера (очищает npm-side cache, не storage);
- Скрипт-prune старых versions: для каждого `package.json` в storage оставлять последние N версий, удалить остальные tarball'ы;
- Или использовать verdaccio plugin типа `verdaccio-cache-cleaner`.
Weekly GC для двух container-сервисов на VDS, чтобы storage не разрастался:
1. **Registry** (`/opt/stacks/registry/`) — `registry garbage-collect -m` против nested mount `./docker:/var/lib/registry` (host data в `/opt/stacks/registry/docker/docker/registry/v2/...`).
2. **Verdaccio** (`/opt/stacks/verdaccio/`) — keep N=10 most-recent versions + dist-tagged per-package, drop ТОЛЬКО proxied tarballs (`_distfiles[tgzName]` defined). Локально-published versions (`@snollajs/*` + similar) защищены.
## Open questions
## Key files
- [ ] Cadence: ежедневно ночью? Раз в неделю? Зависит от роста кеша.
- [ ] Retention для verdaccio: сколько версий каждого package хранить? 5 последних? Все из @snollajs/* — всегда?
- [ ] Notify on GC fail — через [[vds-backup-rsync-kreknin]] почтовый канал?
## Implementation sketch
`/etc/cron.d/vds-gc`:
```cron
# Registry GC — каждое воскресенье в 03:00 MSK
0 3 * * 0 root /opt/stacks/registry/scripts/gc.sh > /var/log/registry-gc.log 2>&1
# Verdaccio prune — каждое воскресенье в 04:00 MSK
0 4 * * 0 root /opt/stacks/verdaccio/scripts/prune.sh > /var/log/verdaccio-prune.log 2>&1
```
`/opt/stacks/registry/scripts/gc.sh`:
```bash
#!/bin/bash
set -e
# read-only mode → GC → re-enable writes
docker exec registry sh -c 'export REGISTRY_STORAGE_MAINTENANCE_READONLY_ENABLED=true && sleep 1'
docker exec registry registry garbage-collect -m /etc/docker/registry/config.yml
docker exec registry sh -c 'unset REGISTRY_STORAGE_MAINTENANCE_READONLY_ENABLED'
docker restart registry # для надёжности
```
(Hint: подход с env var может не сработать на running container — лучше через compose `docker-compose.override.yml` с read-only flag + docker compose up -d.)
`/opt/stacks/verdaccio/scripts/prune.sh`:
```bash
#!/bin/bash
# Удалить tarball'ы старше N=10 versions для каждого package
# (нужно написать скрипт)
```
- `/opt/stacks/gc/scripts/notify.sh` — ntfy helper, sources `/opt/stacks/ntfy/.env`, posts на `vds-ops` topic.
- `/opt/stacks/gc/scripts/registry-gc.sh` — stop registry → offline `garbage-collect -m` → start; empty-storage guard в начале.
- `/opt/stacks/gc/scripts/verdaccio-prune.sh` — wrapper, stop verdaccio → docker run `node:20-alpine` mounts storage + script → start.
- `/opt/stacks/gc/scripts/verdaccio-prune.js` — ядро prune: walk storage, sort versions by `time[v]` desc, keep top-N + dist-tagged, delete tgz + `_attachments` entries for proxied versions, atomic JSON rewrite + `_rev` bump.
- `/etc/cron.d/vds-gc` — Sunday 03:00 / 03:30 MSK.
- `/var/log/vds-gc/{registry,verdaccio}-YYYY-MM-DD.log` — append-only logs.
## Decisions log
- усто — задача только заведена._
- **2026-05-21:** keep N=10 per package для verdaccio (включая `@snollajs/*` — uniform, no special-case scope). Reasoning: N=10 покрывает любые reasonable rollback scenarios, локально-published уже защищены логикой `_distfiles[tgzName]` absent.
- **2026-05-21:** weekly Sunday 03:00 / 03:30 MSK (2 часа до 05:00 backup cron — нет конфликта). Reasoning: weekly достаточно для personal scale; verdaccio cache растёт ~MB/день не GB/день.
- **2026-05-21:** registry GC = stop→`-m`→start (~30s downtime) instead of readonly+restart-twice. Reasoning: проще, более atomic; на personal registry 30s downtime в воскресенье ночью acceptable.
- **2026-05-21:** verdaccio prune = stop+prune+start (~1min downtime). Reasoning: избегаем torn-read race window между tgz delete и package.json rewrite.
- **2026-05-21:** verdaccio detection of locally-published — через `_distfiles[<tgzName>]` (НЕ `_distfiles[<version>]`). Initial code был bug — verdaccio keys `_distfiles` by tarball filename like `lodash-0.1.0.tgz`, не version string. Dry-run показал 4597 "locally-published" из 7021 — обнаружен через inspection lodash/react/`@snollajs` storage. Real run после fix: 73 версии legitimately protected.
- **2026-05-21:** notify policy — success-low (`broom` tag, prio low) если freed < 10 MiB AND duration < 5 min; success-default иначе; fail-high (`x,boom` tag, prio high) на любой rc≠0; skip-min на empty registry.
- **2026-05-21:** atomic package.json rewrite через `write tmp + rename` (POSIX atomic on same fs). Bump `_rev` (`<num+1>-<random_hex>`) чтобы npm clients не закешировали stale packument.
- **2026-05-21:** verdaccio prune skip `versions{}` and `time{}` entries — оставляем metadata in-place даже для удалённых tarballs. Reasoning: npm может re-fetch tarball from upstream при следующем install request (proxy behaviour); metadata weighs ~KB не GB.
## Open questions
- [ ] (post-первого live cron run) — phone-verify notification приходит на `vds-ops`.
- [ ] Logrotate для `/var/log/vds-gc/` — пока nope (weekly cadence + лог ~10KB/run = 500KB/year, терпимо).
## Completed steps
- [x] recon ntfy creds + verdaccio storage layout (package.json structure: `versions{}`, `_distfiles{}`, `_attachments{}`, `_rev`)
- [x] write `notify.sh`, `registry-gc.sh`, `verdaccio-prune.sh`+`verdaccio-prune.js`
- [x] dry-run verdaccio prune (initial bug discovery — `_distfiles[v]` vs `_distfiles[tgzName]`)
- [x] fix detection → re-dry-run: 4524 tgz / 1.7 GiB candidate
- [x] real verdaccio prune: 8.5G→6.8G, 1551 pkgs modified, 0 errors
- [x] verify verdaccio responsive post-prune (lodash metadata 117 versions)
- [x] push alpine v1+v2 to registry → test GC scenario
- [x] delete manifest via DELETE API → re-run GC → freed 61M, registry restarted
- [x] patch registry-gc.sh — empty-storage guard
- [x] install `/etc/cron.d/vds-gc`, restart cron service
## Notes
- Триггер: после стабилизации VDS bootstrap (`vds-kzntsv-bootstrap` Phase 3 завершена).
- Кронить только write-ops после убеждения что live registry/verdaccio не получит traffic во время GC, иначе риск повредить.
- Verdaccio `_distfiles` keyed by tarball filename (`lodash-0.1.0.tgz`), не version. Gotcha — easy mistake.
- Multi-arch image push через современный buildkit оставляет manifest как OCI index. Для DELETE через API нужно `Accept: application/vnd.oci.image.manifest.v1+json` (или index variants). Иначе registry возвращает 404.
- `_attachments` field optional — на proxied packages обычно ~5-10 entries (только cached tarballs), на locally-published — все версии package.
- `du -sh` (default block-size) vs `du -sb` (bytes) могут rounding-discrepancy на 5-10%. Used `du -sb` в скриптах для precise byte math.
- registry compose mount `./docker:/var/lib/registry` → registry создаёт data в `/var/lib/registry/docker/registry/v2/...` → host path nested `/opt/stacks/registry/docker/docker/registry/v2/...`. Странно но работает.