Importer un dépôt externe¶
Référence du flux d'import de dépôts externes dans gitrust : rôle de la base de données, schéma de séquence complet et pistes d'optimisation.
1. Rôle de la base de données¶
Pendant un import, la DB sert quatre fonctions distinctes :
| 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 |
Le clone lui-même ne passe jamais par la DB : seuls les métadonnées et compteurs de progression y transitent. Les objets Git sont écrits directement sur disque dans {GIT_REPOS_BASE_PATH}/{owner}/{slug}.git/.
Pourquoi persister l'état ?¶
- Reprise après redémarrage : le serveur peut crasher pendant un clone de plusieurs minutes. La table permet de marquer ces jobs comme
failedau redémarrage. - SSE : l'UI lit la progression via un endpoint SSE qui interroge la DB toutes les 2 s. Sans DB, il faudrait un canal in-memory plus un mécanisme de routage vers le bon client.
- Multi-navigateurs : si l'utilisateur ferme l'onglet puis le rouvre, il retrouve l'état exact du job.
- Audit / historique : conservation des imports passés avec leurs statistiques.
2. Schéma du flux actuel¶
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. Coût DB par import¶
Pour un clone de 90 secondes avec throttle 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 requêtes pour un import, dont 120+ sont des progress updates.
4. Pistes d'optimisation (sans changer libgit2)¶
4.1 Éliminer le SELECT avant UPDATE dans update_progress¶
update_progress fait actuellement load_model (SELECT) puis active.update(db) (UPDATE + RETURNING). Le SELECT est redondant : on connaît l'ID, on veut juste patcher 4 colonnes.
Gain : divise par 2 les requêtes de progression (~60 requêtes en moins pour un clone de 90 s).
Refonte suggérée avec UpdateMany (pattern déjà utilisé dans 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?;
Même principe pour mark_running, mark_success, mark_failed, mark_cancelled → toutes divisent leurs aller-retours DB par 2.
4.2 Downsampler la progression dans le worker (dédié)¶
Plan alternatif : le callback écrit simplement dans un tokio::sync::watch<TransferStats> (en mémoire, zéro DB). Une tâche dédiée consomme ce channel et UPDATE la DB à 1 Hz seulement.
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 seul émetteur DB au lieu de N tokio::spawn concurrents → zéro contention sur le pool
- Fréquence déterministe (1 Hz) indépendante de la vitesse réseau
- Code plus clair (la tâche est async, on peut await sans détour)
4.3 Supprimer le RETURNING implicite de SeaORM¶
ActiveModel::update(db) renvoie le modèle complet via ... RETURNING *. Pour les progress updates, on n'utilise pas le retour. UpdateMany::exec n'émet pas de RETURNING → moins de bytes sur le wire, moins de désérialisation.
4.4 Pool DB dédié pour le worker¶
Aujourd'hui le worker partage le pool principal avec les handlers HTTP. Un pool de 4-8 connexions dédié au worker éviterait qu'un import sature le pool et fasse timeouter les requêtes utilisateur.
4.5 Debouncer le SSE côté serveur¶
Le SSE SELECT toutes les 2 s même quand rien ne change. Alternative : PostgreSQL LISTEN/NOTIFY sur une chaîne import_job_{uuid}. Chaque UPDATE émet un NOTIFY, la connexion SSE fait LISTEN → push direct, zéro polling.
Coût : nécessite une connexion DB dédiée par SSE (LISTEN est stateful). Compromis à évaluer.
5. Optimisation majeure : shell-out vers git clone --bare¶
Indépendant des optimisations DB mais de loin le plus gros gain. libgit2 est structurellement 2-5x plus lent que git CLI sur gros clones HTTPS (pas de multiplexage, résolution de deltas mono-threadée, etc.).
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 :
- Vitesse native (parité avec git CLI)
- Progression native via stderr Receiving objects: 42% (368/876), 1.10 MiB | 2.05 MiB/s
- HEAD correctement positionné (plus besoin du workaround RepoBuilder)
- Moins de dépendance sur libgit2 pour le cas du clone initial
Contrepartie : binaire git requis sur le serveur (déjà le cas — utilisé par SbomService::git_archive).
6. Priorités recommandées¶
Si on voulait optimiser maintenant, ordre proposé :
- Shell-out
git clone --bare— gain réel pour l'utilisateur (clone 3-5x plus rapide). update_manypourupdate_progress— divise la pression DB par 2, changement mécanique de ~20 lignes.watchchannel + tâche 1 Hz — architecture plus propre, élimine les problèmes de pool.- Pool DB dédié worker — filet de sécurité opérationnel.
- LISTEN/NOTIFY pour SSE — seulement si beaucoup de clients simultanés.
Les gains 2-4 sont utiles même avec libgit2 ; le gain 1 est le plus visible pour l'utilisateur final.