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:
80
verdaccio-prune-semantics.md
Normal file
80
verdaccio-prune-semantics.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: Verdaccio prune semantics — proxied vs locally-published
|
||||||
|
type: concept
|
||||||
|
tags: [verdaccio, npm, storage, gc, gotcha]
|
||||||
|
sources: []
|
||||||
|
updated: 2026-05-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Verdaccio prune — proxied vs locally-published
|
||||||
|
|
||||||
|
Прежде чем удалять `.tgz` из verdaccio storage, надо отличать proxied (cached from upstream — безопасно удалить, можно re-fetch) от locally-published (authoritative copy — удаление = permanent loss).
|
||||||
|
|
||||||
|
## Storage layout
|
||||||
|
|
||||||
|
`/storage/data/<package>/`:
|
||||||
|
- `package.json` — packument (metadata, дешёвый)
|
||||||
|
- `<package>-<version>.tgz` — binary tarballs (дорогие)
|
||||||
|
|
||||||
|
Для scoped: `/storage/data/<scope>/<package>/...`. Scope в filename `.tgz` **НЕ** включается — `@snollajs/snolla/snolla-0.2.4.tgz`.
|
||||||
|
|
||||||
|
## Detection rule
|
||||||
|
|
||||||
|
В `package.json` есть три relevant fields:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
_uplinks: { npmjs: { etag, fetched } } # upstream registries verdaccio queried
|
||||||
|
_distfiles: { "<pkg>-<v>.tgz": { url, sha, registry } } # ключи — ИМЯ tarball, не version
|
||||||
|
_attachments: { "<pkg>-<v>.tgz": { shasum } } # cached + locally-uploaded tarballs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detection**:
|
||||||
|
- `_distfiles[<tgzName>]` defined → **proxied**, tarball mirrored from upstream URL. Удалять `.tgz` файл безопасно — verdaccio re-fetch'нет при следующем `npm install pkg@v`.
|
||||||
|
- `_distfiles[<tgzName>]` undefined, but `_attachments[<tgzName>]` defined и `.tgz` есть на диске → **locally-published** (`npm publish` в verdaccio через `verdaccio` registry). **НЕ удалять** — это единственная копия.
|
||||||
|
|
||||||
|
## Gotcha: keying
|
||||||
|
|
||||||
|
Ключи `_distfiles` — это **filename** (`lodash-0.1.0.tgz`), **НЕ** version string (`0.1.0`). Lookup `_distfiles["0.1.0"]` всегда возвращает undefined, ложно классифицируя ВСЕ versions как locally-published.
|
||||||
|
|
||||||
|
Easy mistake: первый draft prune скрипта проверял `_distfiles[v]` → dry-run показал 4597/7021 packages "защищены" (на самом деле ВСЕ proxied lodash/react/etc). Fix: `_distfiles[<tgzBase>-<v>.tgz]` где `tgzBase = name.replace(/^@[^/]+\//, '')` (strip scope для filename).
|
||||||
|
|
||||||
|
## Safe prune algorithm
|
||||||
|
|
||||||
|
```
|
||||||
|
для каждого package:
|
||||||
|
keepSet = dist-tagged versions # {latest, next, beta, ...}
|
||||||
|
others = versions − keepSet, отсортированные по time[v] desc
|
||||||
|
keepSet += first (N - |keepSet|) из others # N = 10
|
||||||
|
для v в (versions − keepSet):
|
||||||
|
tgzName = "<scope-stripped-name>-<v>.tgz"
|
||||||
|
if _distfiles[tgzName] undefined:
|
||||||
|
continue # locally-published, skip
|
||||||
|
if .tgz file exists: delete file
|
||||||
|
if _attachments[tgzName] exists: delete entry
|
||||||
|
если что-то изменили в _attachments:
|
||||||
|
bump _rev (`<num+1>-<random_hex>`)
|
||||||
|
atomic rewrite package.json (tmp + rename)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Atomic write + _rev
|
||||||
|
|
||||||
|
`package.json` rewrite через `fs.writeFileSync(tmp); fs.renameSync(tmp, real)` — atomic on POSIX same-fs. Race window между «file gone» и «package.json updated» ничтожен.
|
||||||
|
|
||||||
|
`_rev` field в формате `<num>-<hash>` (CouchDB-style). Verdaccio проверяет его для optimistic concurrency. Bump = increment number + new random hex. Это invalidates npm client packument cache.
|
||||||
|
|
||||||
|
Дополнительная подстраховка от race: **stop verdaccio перед prune, start после** (~1 min downtime weekly). Альтернатива — atomic rewrite без stop — допустима, но cache invalidation менее clean.
|
||||||
|
|
||||||
|
## Что НЕ трогать
|
||||||
|
|
||||||
|
- `versions{}` и `time{}` — оставляем metadata in-place даже после удаления tarballs. Для proxied packages npm re-fetch'нет tgz при request; metadata лёгкая (~KB).
|
||||||
|
- `_uplinks{}` — agent-level state о fetch timestamps; не относится к individual versions.
|
||||||
|
- `readme`, `users`, `_id` — irrelevant для prune.
|
||||||
|
|
||||||
|
## Применено
|
||||||
|
|
||||||
|
[`vds-gc-cron`](../../.tasks/vds-gc-cron.md) — weekly cron Sun 03:30 MSK на VDS. Smoke 2026-05-21: 8.5G→6.8G freed, 4524 tgz deleted, 73 locally-published versions защищены (`@snollajs/*` + ~40 другие internal packages).
|
||||||
|
|
||||||
|
## Ссылки
|
||||||
|
|
||||||
|
- Verdaccio local-storage docs: https://verdaccio.org/docs/configuration#storage
|
||||||
|
- Algorithm в репо: `/opt/stacks/gc/scripts/verdaccio-prune.js` на VDS.
|
||||||
Reference in New Issue
Block a user