Files
admin/.wiki/concepts/verdaccio-restore-packument-desync.md
vitya 15c273c153 wiki(ingest): concepts/verdaccio-restore-packument-desync — 409 postmortem
disaster-restore вернул тарболлы но древний/пустой packument →
publish свежей версии EEXISTS 409. Fix: снять только коллизирующий
целевой .tgz (backup first), republish; НЕ rm -rf каталог.
Применено к @snollajs/{data,mailer,numbering,content-api}.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:10:01 +03:00

78 lines
4.6 KiB
Markdown
Raw Permalink 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.
---
title: Verdaccio disaster-restore desync — tarball present, packument stale → EEXISTS 409
type: concept
tags: [verdaccio, npm, storage, restore, backup, 409, gotcha, postmortem]
sources: []
updated: 2026-06-11
---
# Verdaccio disaster-restore desync — 409 при publish свежей версии
## Симптом
После disaster-restore verdaccio из бэкапа: `npm/yarn publish <pkg>@<newver>` падает с
**`409 "this package is already present"`**, хотя в реестре этой версии (по GET) нет —
`dist-tags.latest` показывает древнюю версию. На некоторых пакетах GET вообще `404`.
## Root cause: packument ↔ tarball рассинхрон
Restore вернул в `/storage/.../<pkg>/`:
- **все `.tgz` тарболлы** — до текущих версий включительно (`data-0.9.0.tgz` физически на диске);
- но **`package.json` (packument-индекс) — древний** (`versions{}` знает только 0.1.x/0.2.x,
`dist-tags.latest=0.2.0`), потому что метадата восстановилась из старого слоя бэкапа
либо была затёрта сорванным `unpublish`.
Verdaccio publish-flow:
1. merge новой версии в packument → **OK** (версии нет в индексе);
2. запись `.tgz` на диск → файл **уже существует** (с restore) → `EEXISTS`
surface как **`409 this package is already present`**.
Лог-отпечаток:
```
PUT /@snollajs%2Fdata
http <-- 409 ... error: this package is already present
```
### Подвид: пустой packument после сорванного unpublish
`unpublish` пишет пустой packument-stub (161 байт, `versions:[]`, `_rev:""`), затем пытается
`rmdir` каталог → **`ENOTEMPTY`** (тарболлы на месте) → `500`. Итог: GET `404` (пустой индекс),
publish `409` (тарболл цел). Каталог «полупустой»: индекс мёртв, бинарники живы.
## Fix: хирургия, НЕ `rm -rf` каталога
`rm -rf /storage/.../<pkg>` снимет 409, но **уничтожит всю историю версий** (тарболлы —
locally-published, `_distfiles:{}` ⇒ единственная копия, см. [[verdaccio-prune-semantics]]).
Правильно — удалить **только коллизирующий целевой `.tgz`** под версию, которую republish'ат:
```bash
# backup сначала
sudo tar czf /tmp/verdaccio-rescue-$(date -u +%Y%m%dT%H%M%SZ).tar.gz <pkg-dirs>
# снять коллизию — ровно целевой тарболл, остальное не трогать
sudo rm /opt/stacks/verdaccio/storage/@scope/<pkg>/<pkg>-<targetver>.tgz
# → republish <pkg>@<targetver> из source проходит, packument регенерится корректно
```
Рестарт контейнера **не нужен**: local-storage читает packument/тарболл с диска пер-реквест;
packument не трогаем, только убираем лишний `.tgz`, которого в индексе и так нет.
**Trade-off:** промежуточные версии (тарболлы на диске, но не в индексе после republish) остаются
orphaned — безвредны, не ставятся по pin, GC их не трогает (locally-published). Если нужна полная
история в индексе — отдельный скрипт-reconstruct packument из всех on-disk тарболлов
(`tar xzO package/package.json` per tgz + shasum/integrity) — heavier, по умолчанию не делаем.
## Применено 2026-06-11
`@snollajs/{data@0.9.0, mailer@0.7.4, numbering@0.7.4, content-api@0.8.1}` — удалены 4
коллизирующих тарболла, бэкап `/tmp/verdaccio-snollajs-rescue-20260611T140801Z.tar.gz` на
[[../entities/vds-kzntsv]]. Republish делегирован владельцу пакетов (сессия snolla).
Контекст: восстановление verdaccio после wipe (см. также [[verdaccio-token-lifecycle]] — другой
класс отказа того же restore-эпизода).
## Связанные
- [[verdaccio-prune-semantics]] — locally-published vs proxied, почему `rm` тарболла = permanent loss
- [[verdaccio-token-lifecycle]] — restart trap / ephemeral secret (соседний симптом того же restore)
- [[../entities/vds-kzntsv]] — хост