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

8.3 KiB

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.tsresolveBucketRequest(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

# 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)

./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:

    -- 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