Playbook: Pinbox24 W4 — Deleted Records Cleanup
Typ: Maintenance / Data Cleanup
Serwis: w4_db (regRecords + files), Wasabi S3 (3 regiony)
Kontener: s3-v2-v42-prod @ bms-1 (94.23.26.113)
Skrypt: scripts/cleanup-deleted-records.js
GH tracking: (utwórz issue przed pierwszym wykonaniem)
Co robi ten cleanup
Usuwa z w4_db rekordy oznaczone deleted: true w kolekcji regRecords wraz z:
- Plikiem na Wasabi — primary bucket (
files.bucket, endpoint EU eu-central-1) + każda kopia wstorageInfo[]gdzieexist: true - Dokumentem w
filescollection — 1 dokument per plik (repliki cross-region śledzone wstorageInfo[], nie jako osobne dokumenty) - Rekordem
regRecords
regRecord (deleted=true)
└─ recordData.recordMainDocument = files._id
└─ files doc
├─ bucket: "test-replicated-to-us-bucket" endpoint: s3.eu-central-1.wasabisys.com ← primary
├─ storageInfo[0]: bucketName: "test-us-bucket-for-replication-testing" endpoint: s3.us-east-2.wasabisys.com
└─ storageInfo[1]: bucketName: "p24-was-us-east-1" endpoint: s3.us-east-1.wasabisys.com
Conservative policy: jeśli którykolwiek deleteObject zawiedzie — rekord zapisywany do CLEANUP_ERRORS.json, MongoDB (files + regRecord) nie jest usuwane dla tego rekordu, skrypt kontynuuje.
Kiedy uruchamiać
- Scheduled: raz na miesiąc (np. w nocy, po godzinie ruchu)
- On-demand: gdy
filescollection lub Wasabi bucket wykazuje wysokie użycie, po masowych operacjach usuwania rekordów (np. Stock cleanup)
Wstępny audyt (2026-07-04): ~10 427 rekordów deleted: true (szczyt maj 2025: 8 231, głównie status Stock).
Wymagania wstępne
- SOPS split wykonany —
secrets/pinbox24-w4.env.sopsistnieje z kluczami Wasabi i MongoDB W4 (tracking issue) -
pinbox24PublicAccessKeyId/pinbox24PublicSecretAccessKeymają uprawnienia do delete na bucketach:pinbox24,test-replicated-to-us-bucket,test-us-bucket-for-replication-testing,p24-was-us-east-1 - Kontener
s3-v2-v42-proddziała na bms-1 i ma dostęp do MongoDB w4_db
Kroki
Krok 1 — Skopiuj skrypt do kontenera
# Na maszynie dev (Windows):
scp C:\code_2026\p24-infra\scripts\cleanup-deleted-records.js root@94.23.26.113:/root/cleanup-deleted-records.js
# Na bms-1:
docker cp /root/cleanup-deleted-records.js s3-v2-v42-prod:/app/cleanup-deleted-records.jsKrok 2 — Dry run (OBOWIĄZKOWY)
docker exec s3-v2-v42-prod sh -c 'cd /app && node cleanup-deleted-records.js --dry-run 2>&1'Sprawdź raport walidacyjny:
docker exec s3-v2-v42-prod cat /tmp/DELETED_RECORDS_CLEANUP_VALIDATION_REPORT.jsonZweryfikuj:
totals.regRecordsToDelete— oczekiwane ~10 427 (może być więcej bez age cutoff)totals.wasabiEndpointsTotal> 0 — storageInfo lookup działasample_records[0].filesPreview— pokazuje bucket/endpoint dla każdej kopiibreakdowns.byStatus— czy są niespodziewane statusy (np. nie-deleted rekordy)?
Nie kontynuuj jeśli liczby wyglądają nieprawidłowo — zbadaj przed —execute.
Krok 3 — Execute
docker exec s3-v2-v42-prod sh -c 'cd /app && node cleanup-deleted-records.js --execute --confirmed 2>&1'Skrypt loguje postęp co 20 rekordów. Dla ~10k rekordów z 3 endpointami oczekiwany czas: 60–90 minut.
Krok 4 — Natychmiast skopiuj audit log (PRZED restartem kontenera!)
Kontener jest ephemeral — przy restarcie
/tmp/jest kasowane. Pliki MUSZĄ być skopiowane.
# Na bms-1:
docker cp s3-v2-v42-prod:/tmp/DELETED_RECORDS_CLEANUP_LOG.jsonl /root/cleanup-log-$(date +%Y%m%d).jsonl
# Opcjonalnie — skopiuj na maszynę dev:
# scp root@94.23.26.113:/root/cleanup-log-*.jsonl C:\backup\Jeśli istnieje CLEANUP_ERRORS.json — skopiuj też:
docker cp s3-v2-v42-prod:/tmp/CLEANUP_ERRORS.json /root/cleanup-errors-$(date +%Y%m%d).json 2>/dev/null || trueKrok 5 — Weryfikacja
# Sprawdź czy coś zostało
docker exec s3-v2-v42-prod sh -c 'cd /app && node -e "
var m = require(\"mongoose\");
m.connect(process.env.DB_URI || process.env.MONGODB_URI || process.env.NEW_MONGODB_URI).then(async function(c) {
var n = await c.connection.db.collection(\"regRecords\").countDocuments({ deleted: true });
console.log(\"deleted=true remaining: \" + n);
process.exit(0);
});
" 2>&1'Oczekiwane: deleted=true remaining: 0 (lub nowe rekordy dodane podczas cleanup).
Obsługa błędów
CLEANUP_ERRORS.json istnieje
Rekordy w tym pliku miały Wasabi delete failure — MongoDB pozostało nienaruszone.
[
{ "recordId": "...", "fileId": "...", "endpoint": "s3.us-east-2.wasabisys.com", "bucket": "test-us-bucket-for-replication-testing", "error": "..." }
]Retry: Uruchom skrypt ponownie — przy --execute --confirmed ponowi próbę dla tych rekordów (query deleted: true trafi na te same rekordy, które nie zostały usunięte). Jeśli błąd Wasabi jest trwały (np. brak uprawnień), napraw klucze API i powtórz.
Skrypt przerwany w połowie
Conservative policy działa per-rekord. Bezpiecznie uruchomić --execute --confirmed ponownie — rekordy już usunięte z DB nie pojawią się w query (query szuka deleted: true). Wasabi double-delete jest idempotentny (S3 nie zwraca błędu dla nieistniejącego obiektu).
Rollback / Recovery
Usunięcia z Wasabi i MongoDB są NIEODWRACALNE.
- Nie ma rollback dla usuniętych danych.
- Jedynym źródłem prawdy o tym co zostało usunięte jest audit log z Kroku 4.
- Jeśli audit log nie został skopiowany przed restartem kontenera — informacja o usuniętych plikach jest bezpowrotnie utracona.
Minimalny recovery plan:
- Sprawdź
/root/cleanup-log-YYYYMMDD.jsonlna bms-1 (skopiowany w Kroku 4) - Każdy wpis zawiera:
recordId,officeId,regId,recordMainDocument,filesDeleted[]z pełnymi lokalizacjami Wasabi - Backup Wasabi: jeśli bucket replication policy to umożliwia — sprawdź Wasabi Console → Bucket → Replication Policy → czy usunięcie propaguje się do repliki
Struktura kopii Wasabi
Pinbox24 przechowuje jeden dokument w files collection per plik. Repliki cross-region śledzone są w tablicy storageInfo[] wewnątrz tego samego dokumentu:
| Pole | Opis |
|---|---|
files.bucket | Primary bucket (np. test-replicated-to-us-bucket) |
| endpoint primary | Zawsze s3.eu-central-1.wasabisys.com (hardcoded w skrypcie) |
storageInfo[n].bucketName | Nazwa bucketu repliki |
storageInfo[n].endpoint | URL endpointu repliki (strip https:// przed użyciem) |
storageInfo[n].exist | true = replika istnieje, false = pominąć |
Skrypt deduplikuje listę (bucket, endpoint) przed wywołaniem deleteObject, żeby uniknąć podwójnego delete jeśli primary i storageInfo[0] wskazują na ten sam bucket.
Znane ograniczenia
- Node.js w kontenerze jest stary (10.x/12.x) — brak optional chaining
?.. Skrypt używa(obj || {}).prop. - Skrypt jest sekwencyjny — ~10k rekordów z 3 endpointami = 30k API calls = ok. 60–90 minut.
- Brak Error Notification Standard dla
CLEANUP_ERRORS.jsonjeśliP24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URLnie jest ustawione w kontenerze. Sprawdź env vars kontenera po SOPS split.
Historia
| Data | Działanie |
|---|---|
| 2026-07-04 | Audyt: 10 427 rekordów deleted: true w w4_db (spike maj 2025: 8 231 Stock) |
| 2026-07-05 | Skrypt napisany, playbook utworzony. Blocked: SOPS split prerequisite |