didi-lot2-backend/backend/services/orchestration-layer/agent-v3/MIGRATION_MINIO.md
2026-07-10 03:39:53 -07:00

188 lines
8.3 KiB
Markdown

# Migrare MinIO: local multi-bucket → cluster extern single-bucket
**Autor**: refactor 2026-04-25
**Branch**: `feat/minio-cluster-single-bucket`
**Scop**: mutarea fișierelor user-uploaded de pe `staging-dataLayer-minio` (bucket-per-user) pe cluster extern managed (`<minio-host>`, single bucket `didi-prod` cu prefix-uri).
---
## De ce single-bucket
Cluster extern (4 noduri MinIO `minio1..4.<minio-domain>`, EC:2, 466 GiB utilizabili) ne dă credentiale `didi-prod` cu **`s3:*` doar pe bucket-ul `didi-prod`**. Nu putem crea bucket-uri (testat: 3/3 încercări `Access Denied`).
Soluția: păstrăm conceptul "namespace per user" dar ca **prefix** în bucket-ul fix:
```
ÎNAINTE (local, multi-bucket): DUPĂ (cluster, single-bucket):
user-3/ didi-prod/
images/ users/3/
videos/ images/
audio-files/ videos/
user-19/ audio-files/
images/ users/19/
uploads/ images/
image-files/ didi-prod/uploads/ ← legacy fallback
didi-prod/image-files/ ← legacy fallback
```
Numele de prefix-uri sistem (`uploads`, `image-files`, `audio-files`, `video-files`, `text-files`, `document-files`, `pipeline-artifacts`) sunt **identice cu numele bucket-urilor vechi** — astfel URL-urile vechi rămân interpretabile de proxy fără DB rewrite.
---
## Compatibilitate URL — backward compat
URL-urile vechi din `bos_analysis.analysis_session.input_media_url` și PG history pointează la formate vechi:
```
https://didi365.eu/storage/user-3/images/abc.jpg ← format vechi user
https://didi365.eu/api/v3/media/file/user-3/images/abc.jpg ← proxy vechi
http://staging-dataLayer-minio:9000/image-files/foo.jpg ← bucket sistem
```
Codul rezolvă toate aceste forme la canonic `didi-prod/<full-path>`:
| URL primit | Resolved bucket | Resolved key |
|---|---|---|
| `user-3/images/abc.jpg` | `didi-prod` | `users/3/images/abc.jpg` |
| `image-files/foo.jpg` | `didi-prod` | `image-files/foo.jpg` |
| `didi-prod/users/3/images/abc.jpg` | `didi-prod` | `users/3/images/abc.jpg` (passthrough) |
Implementat în:
- `didiFramework/src/config/minio.ts``resolveBucketRequest(bucket, key)`
- `agent-v3/src/shared/media/media-service.ts` → inline în `proxyFile()`
---
## Modificări în cod (branch `feat/minio-cluster-single-bucket`)
### didiFramework
| Fișier | Schimbare |
|---|---|
| `src/config/minio.ts` | Refactor complet: `BUCKET` fix, `resolveBucketRequest()`, `userObjectKey()`, no-op `ensureBucket()` în cluster mode, `createUserBucket()` lazy (doar logging) |
| `src/routes/auth.ts` (endpoint `/internal/get-bucket-info`) | Returnează `bucketName=didi-prod` + `folder=users/{id}/{mimeFolder}` în loc de `bucketName=user-{id}` + `folder={mimeFolder}` |
| `src/routes/uploads.ts` | **Nemodificat** — funcționează prin backward compat (caller pasează `bucket=image-files`, layer-ul îl rezolvă) |
| `sql/migrations/010_add_user_storage_quota.sql` | NEW — adaugă `storage_used_bytes` + `storage_limit_bytes` la `internet_user` (înlocuiește bucket tags) |
| `docker-compose.yml` | Env vars cu fallback `${MINIO_*:-default}` |
### agent-v3
| Fișier | Schimbare |
|---|---|
| `src/shared/media/media-service.ts` | `ensureBucket()` no-op în single-bucket mode. `proxyFile()` rezolvă legacy bucket → cluster prefix. Ownership check acceptă atât `bucket=user-{N}` cât și prefix `users/{N}/`. `uploadFile()` folosește `MINIO_BUCKET` env ca fallback |
| `docker-compose.yml` | `MINIO_BUCKET`, `MINIO_USE_SSL`, `MINIO_PUBLIC_ENDPOINT` adăugate la `x-worker-env` cu fallback empty (= legacy multi-bucket) |
### Scripts
| Fișier | Schimbare |
|---|---|
| `scripts/minio-switch.sh` | NEW — swap între local/cluster. Sourceaza credentiale din `.cluster-credentials.env` |
| `scripts/.cluster-credentials.env` | + `DIDI_MINIO_ACCESS_KEY` / `DIDI_MINIO_SECRET_KEY` / `DIDI_MINIO_DEV_*` |
---
## Cluster extern — date de conectare
```
S3 API: http://<minio-host>:9000 (VIP 10.11.10.128)
Console: http://<minio-console-host>:9001 (VIP 10.11.10.129)
Region: us-east-1
Path style: REQUIRED (forcePathStyle în SDK)
TLS: false acum (HTTPS în curând prin HAProxy)
User PROD: didi-prod / 627074a6ceb8d8531feffaf4ee9cc45007350c58
User DEV: didi-dev / 6f51811a4a157cb8f8d0d4376877552b780a06b8
Bucket PROD: didi-prod (full s3:* doar pe bucket-ul propriu)
Bucket DEV: didi-dev (idem)
```
Infrastructură: 4 noduri Proxmox extern (`minio1..4.<minio-domain>`, IPs `10.11.10.124-127`), erasure coding EC:2, HAProxy + keepalived VRRP, 466 GiB utilizabili.
---
## Cutover plan — NEEXECUTAT încă
Codul e gata pe branch, **runtime-ul production rămâne pe local MinIO** până când executăm cutover-ul deliberat.
### Pre-cutover checklist
1. ✓ Backup complet creat: `/home/admin365/backups/pre-minio-refactor-20260425-0935/` (545 MB cu cod + git bundle + 476 MB MinIO data)
2. ✓ Branch `feat/minio-cluster-single-bucket` cu refactor + tests
3. ✓ Cluster `didi-dev` validat (write/list/delete cu prefix-uri noi)
4. ⏳ Apply migration 010 pe PG cluster
5. ⏳ Smoke test cu un user fictiv pe `didi-dev` (upload + analiză + URL public)
### Cutover steps
```bash
# 1. Apply DB migration
docker exec didi-framework node -e "
const fs = require('fs');
const { Pool } = require('pg');
const pool = new Pool({ host: '10.11.50.167', port: 5000, user: 'bos_interface', password: 'interface', database: 'DIDI' });
const sql = fs.readFileSync('/app/sql/migrations/010_add_user_storage_quota.sql', 'utf8');
pool.query(sql).then(() => console.log('migration 010 applied')).catch(console.error).finally(() => pool.end());
"
# 2. Mirror data local → cluster (~2 min pe LAN)
mc alias set local http://staging-dataLayer-minio:9000 minioadmin minio123
mc alias set prod http://<minio-host>:9000 didi-prod '<secret>'
# Per-user buckets → users/{id}/ prefix
for u in $(mc ls local/ | grep -oE 'user-[0-9]+/' | tr -d '/'); do
id=${u#user-}
echo "mirroring $u → users/$id/"
mc mirror local/$u prod/didi-prod/users/$id/
done
# Sistem buckets → identical names ca prefix top-level
for b in uploads image-files audio-files video-files text-files document-files pipeline-artifacts backups; do
mc mirror local/$b prod/didi-prod/$b/
done
# 3. Switch env la cluster
./backend/services/orchestration-layer/scripts/minio-switch.sh cluster
# (rescrie .env, restart didi-framework + agent-v3)
# 4. Smoke test
curl -X POST https://didi365.eu/api/v3/pipeline/analyze ...
# 5. Monitor 24-48h. Local stays running ca fallback.
```
### Rollback (30s)
```bash
./backend/services/orchestration-layer/scripts/minio-switch.sh local
```
Local container rămâne pornit și păstrează datele. Singura pierdere: upload-uri făcute între cutover și rollback nu sunt pe local. Acceptabil pentru fereastra de monitoring.
---
## Riscuri cunoscute
1. **Quota** — la cutover, `storage_used_bytes` în PG e populat din migration 010 doar dacă userul are deja subscription cu `storage_limit_gb`. Pentru calcul real folosit, după cutover rulez:
```sql
-- Reconciliere usage cu MinIO real (job periodic recomandat)
SELECT internet_user_id FROM bos_sysadmin.internet_user WHERE storage_used_bytes = 0;
-- Pentru fiecare → mc du didi-prod/users/{id}/ → UPDATE
```
2. **TLS în curând** — endpoint va deveni `https://<minio-host>` (Let's Encrypt prin HAProxy). La acel moment: `MINIO_USE_SSL=true` + endpoint nou.
3. **Cleanup local cluster** — local `staging-dataLayer-minio` are 7 alte proiecte (lege365-corpus, ml-models, voice-recordings, publisher-assets, rafai-prod). NU le ștergem la cleanup.
4. **HAProxy failover** — VIP `10.11.10.128` rulează pe `cai1`. La failover poate fi o pauză de 2-5s. SDK-ul minio-js are retry implicit, dar la upload-uri mari (>100MB) merită setări explicite.
---
## Status build
```
✓ tsc didiFramework — exit 0
✓ tsc agent-v3 — zero erori noi (doar pre-existente pe `pg` module)
✓ docker compose build — toate imagini OK
✓ minio-switch.sh status — funcțional
✓ Test write/list/delete pe didi-dev cu prefix-uri noi — validat
```