Importa un repository esterno

Riferimento al flusso di importazione di repository esterni in gitrust: ruolo del database, diagramma di sequenza completo e vie di ottimizzazione.

1. Ruolo della banca dati

Durante un'importazione, il DB svolge quattro funzioni distinte:

Rôle Table Fréquence
File d'attente persistante import_jobs 1 INSERT à la création
Suivi d'état pour l'UI import_jobs 1 UPDATE toutes les ~1.5 s
Enregistrement final du dépôt repositories + resources 2 INSERT à la fin
Journal d'audit audit_log 1 INSERT à la fin

Il clone stesso mai passa attraverso il DB: lo attraversano solo i metadati e i contatori di avanzamento. Gli oggetti Git vengono scritti direttamente sul disco in {GIT_REPOS_BASE_PATH}/{owner}/{slug}.git/.

Perché persistere lo Stato?

  • Riprendi dopo il riavvio: il server potrebbe bloccarsi durante un clone che dura diversi minuti. La tabella consente di contrassegnare questi lavori come "non riusciti" al riavvio.
  • SSE: l'interfaccia utente legge l'avanzamento tramite un endpoint SSE che esegue il polling del DB ogni 2 s. Senza DB, avresti bisogno di un canale in memoria più un meccanismo di instradamento al client giusto.
  • Multi-browser: se l'utente chiude la scheda e poi la riapre, trova lo stato esatto del lavoro.
  • Audit/cronologia: conservazione delle importazioni passate con relative statistiche.

2. Diagramma del flusso di corrente

sequenceDiagram
  autonumber
  participant UI as Navigateur
  participant H as Handler HTTP
  participant Svc as ImportService
  participant Chan as mpsc Channel
  participant W as ImportWorker
  participant G as git2 (libgit2)
  participant DB as PostgreSQL
  participant FS as Disque

  UI->>H: POST /import
(url, slug, pat) H->>Svc: create_job Svc->>DB: INSERT import_jobs (pending) H->>Chan: try_send(ImportTask+PAT) H-->>UI: 302 /imports/{id} UI->>H: GET /imports/{id}/stream (SSE) Note over UI,H: EventSource ouvert Chan->>W: ImportTask W->>Svc: mark_running Svc->>DB: SELECT import_jobs
UPDATE status=running W->>G: RepoBuilder.bare(true).clone() G->>FS: init_bare + fetch loop callbacks transfer_progress (flood) G-->>W: stats (objets/bytes) alt throttle 1500 ms écoulé W->>Svc: update_progress (tokio::spawn) Svc->>DB: SELECT + UPDATE import_jobs else dans le throttle W--xW: ignore end end loop toutes les 2 s H->>DB: SELECT import_jobs WHERE id=? DB-->>H: état courant H-->>UI: SSE event (JSON) UI->>UI: bar.value = percent end G-->>FS: objets écrits G-->>W: Ok W->>Svc: check cancel (SELECT import_jobs) W->>Svc: update_progress (Finalizing) Svc->>DB: SELECT + UPDATE W->>RepositoryService: create RepositoryService->>DB: INSERT resources
INSERT repositories W->>DB: UPDATE repositories
(import_source_url, is_empty=false) W->>Svc: mark_success Svc->>DB: SELECT + UPDATE import_jobs
(status=success, duration) W->>AuditService: log REPO_IMPORTED AuditService->>DB: INSERT audit_log H->>DB: SELECT (prochain tick SSE) H-->>UI: terminal=true UI->>UI: window.location.reload()

3. Costo DB per importazione

Per un clone di 90 secondi con acceleratore 1500 ms:

Étape Opérations DB Cumul
create_job 1 INSERT 1
mark_running 1 SELECT + 1 UPDATE 3
update_progress pendant clone ~60 x (1 SELECT + 1 UPDATE) 123
check cancel 1 SELECT 124
update_progress finalizing 1 SELECT + 1 UPDATE 126
RepositoryService::create 2 INSERT (repo + resource) 128
update repositories 1 UPDATE 129
mark_success 1 SELECT + 1 UPDATE 131
audit log 1 INSERT 132
SSE stream (45 ticks à 2 s) 45 SELECT 177

~177 richieste di importazione, di cui oltre 120 sono aggiornamenti sullo stato di avanzamento.

4. Percorsi di ottimizzazione (senza modificare libgit2)

4.1 Eliminare SELECT prima di UPDATE in update_progress

update_progress attualmente esegue load_model (SELECT) quindi active.update(db) (UPDATE + RETURNING). La SELECT è ridondante: conosciamo l'ID, vogliamo solo patchare 4 colonne.

Guadagno: divide le richieste di avanzamento per 2 (~60 richieste in meno per un clone da 90 s).

Riprogettazione suggerita con UpdateMany (modello già utilizzato in ci_service.rs::cancel_running_pipelines):

import_job::Entity::update_many()
    .col_expr(Column::ReceivedObjects, Expr::value(received as i32))
    .col_expr(Column::TotalObjects, Expr::value(total as i32))
    .col_expr(Column::ReceivedBytes, Expr::value(bytes as i64))
    .col_expr(Column::Phase, Expr::value(phase.as_str()))
    .col_expr(Column::UpdatedAt, Expr::value(Utc::now()))
    .filter(Column::Id.eq(job_id))
    .exec(db)
    .await?;

Stesso principio per mark_running, mark_success, mark_failed, mark_cancelled → tutti dividono i loro viaggi di andata e ritorno nel DB per 2.

4.2 Sottocampionamento della progressione nel lavoratore (dedicato)

Piano alternativo: il callback scrive semplicemente su un tokio::sync::watch<TransferStats> (in memoria, zero DB). Un attività dedicata consuma questo canale e AGGIORNA il DB solo a 1 Hz.

flowchart LR
  G[git2 callback] -->|watch::send
très haute fréquence| W[watch channel] W --> T[Tâche update
ticker 1 Hz] T -->|1 UPDATE / s| DB

Gains : - 1 trasmettitore DB singolo invece di N concorrenti tokio::spawn → zero contesa nel pool - Frequenza deterministica (1 Hz) indipendente dalla velocità della rete - Codice più chiaro (l'attività è asincrona, possiamo attendere direttamente)

4.3 Rimuovere il RETURNING implicito da SeaORM

ActiveModel::update(db) restituisce il modello completo tramite ... RETURNING *. Per gli aggiornamenti sui progressi, non utilizziamo il feedback. UpdateMany::exec non emette RETURNING → meno byte in transito, meno deserializzazione.

4.4 Pool di DB dedicato per il lavoratore

Oggi il lavoratore condivide il pool principale con i gestori HTTP. Un pool di 4-8 connessioni dedicate al lavoratore impedirebbe a un'importazione di saturare il pool e causare il timeout delle richieste dell'utente.

4.5 Eliminare il SSE lato server

L'SSE SELECT ogni 2 s anche quando non cambia nulla. Alternativa: ASCOLTA/NOTIFICA PostgreSQL su una stringa import_job_{uuid}. Ogni AGGIORNAMENTO emette una NOTIFICA, la connessione SSE esegue ASCOLTO → push diretto, zero polling.

Costo: richiede una connessione DB dedicata da parte di SSE (LISTEN è stateful). Compromesso da valutare.

5. Ottimizzazione principale: shell-out su git clone --bare

Indipendente dalle ottimizzazioni del DB ma di gran lunga il guadagno più grande. libgit2 è strutturalmente 2-5 volte più lento della CLI git su cloni HTTPS di grandi dimensioni (nessun multiplexing, risoluzione delta a thread singolo, ecc.).

flowchart TB
  subgraph Actuel[Flux actuel — libgit2]
    A1[RepoBuilder::clone] --> A2[libgit2: fetch + resolve]
    A2 -->|lent 2-5x| A3[bare repo]
  end
  subgraph Cible[Flux optimisé — git CLI]
    B1[Command::new git] --> B2[git clone --bare --progress]
    B2 -->|stderr| B3[Parser regex
Receiving objects: X%] B3 --> B4[update watch channel] B2 -->|vitesse native| B5[bare repo] end

Avantages : - Velocità nativa (parità con la CLI git) - Avanzamento nativo tramite stderr Ricezione di oggetti: 42% (368/876), 1,10 MiB | 2,05 MiB/s - TESTA posizionata correttamente (non è necessaria la soluzione alternativa RepoBuilder) - Meno dipendenza da libgit2 nel caso del clone iniziale

Controparte: binario git richiesto sul server (già così — utilizzato da SbomService::git_archive).

6. Priorità consigliate

Se volessimo ottimizzare adesso, ordine proposto:

  1. Shell-out git clone --bare: guadagno reale per l'utente (clone 3-5 volte più veloce).
  2. update_many for update_progress — divide la pressione del DB per 2, modifica meccanica di ~20 righe.
  3. canale watch + attività da 1 Hz: architettura più pulita, elimina i problemi di pooling.
  4. Pool DB dedicato per i lavoratori: rete di sicurezza operativa.
  5. ASCOLTA/NOTIFICA per SSE: solo se ci sono molti client simultanei.

I guadagni 2-4 sono utili anche con libgit2; il guadagno 1 è il più visibile per l'utente finale.