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:
- Shell-out
git clone --bare: guadagno reale per l'utente (clone 3-5 volte più veloce). update_manyforupdate_progress— divide la pressione del DB per 2, modifica meccanica di ~20 righe.- canale
watch+ attività da 1 Hz: architettura più pulita, elimina i problemi di pooling. - Pool DB dedicato per i lavoratori: rete di sicurezza operativa.
- 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.