Importieren Sie ein externes Repository¶
Referenz des externen Repository-Importflusses in Gitrust: Rolle der Datenbank, vollständiges Sequenzdiagramm und Optimierungsmöglichkeiten.
1. Rolle der Datenbank¶
Während eines Imports erfüllt die Datenbank vier verschiedene Funktionen:
| 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 |
Der Klon selbst passiert nie die Datenbank: nur Metadaten und Fortschrittszähler passieren ihn. Git-Objekte werden direkt auf die Festplatte in „{GIT_REPOS_BASE_PATH}/{owner}/{slug}.git/“ geschrieben.
Warum am Staat festhalten?¶
- Fortsetzung nach Neustart: Der Server kann während eines mehrminütigen Klonvorgangs abstürzen. Die Tabelle ermöglicht es, diese Jobs beim Neustart als „fehlgeschlagen“ zu markieren.
- SSE: Die Benutzeroberfläche liest den Fortschritt über einen SSE-Endpunkt, der alle 2 s die Datenbank abfragt. Ohne DB bräuchten Sie einen In-Memory-Kanal und einen Routing-Mechanismus zum richtigen Client.
- Multibrowser: Wenn der Benutzer die Registerkarte schließt und dann erneut öffnet, findet er den genauen Status des Auftrags.
- Prüfung / Historie: Erhaltung früherer Importe mit ihren Statistiken.
2. Stromflussdiagramm¶
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. DB-Kosten pro Import¶
Für einen Klon von 90 Sekunden mit Drosselung 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 Anfragen für einen Import, davon 120+ Fortschrittsaktualisierungen.
4. Optimierungspfade (ohne libgit2 zu ändern)¶
4.1 Beseitigen Sie SELECT vor UPDATE in „update_progress“.¶
„update_progress“ führt derzeit „load_model“ (SELECT) und dann „active.update(db)“ (UPDATE + RETURNING) aus. Das SELECT ist redundant: Wir kennen die ID, wir wollen nur 4 Spalten patchen.
Gewinn: teilt die Fortschrittsanfragen durch 2 (~60 Anfragen weniger für einen 90-Sekunden-Klon).
Vorgeschlagene Neugestaltung mit „UpdateMany“ (Muster, das bereits in „ci_service.rs::cancel_running_pipelines“ verwendet wird):
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?;
Gleiches Prinzip für „mark_running“, „mark_success“, „mark_failed“, „mark_cancelled“ → alle teilen ihre DB-Roundtrips durch 2.
4.2 Downsampling des Fortschritts im Worker (dediziert)¶
Alternativer Plan: Der Rückruf schreibt einfach in ein „tokio::sync::watch
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 einzelner DB-Sender anstelle von N konkurrierenden tokio::spawn → keine Konkurrenz im Pool - Deterministische Frequenz (1 Hz), unabhängig von der Netzwerkgeschwindigkeit - Klarerer Code (die Aufgabe ist asynchron, wir können direkt „warten“)
4.3 Entfernen Sie implizites „RETURNING“ aus SeaORM¶
„ActiveModel::update(db)“ gibt das komplette Modell über „... RETURNING *“ zurück. Für Fortschrittsaktualisierungen verwenden wir kein Feedback. „UpdateMany::exec“ gibt kein „RETURNING“ aus → weniger Bytes auf der Leitung, weniger Deserialisierung.
4.4 Dedizierter DB-Pool für den Worker¶
Heute teilt der Worker den Hauptpool mit den HTTP-Handlern. Ein Pool mit 4 bis 8 Verbindungen, die dem Worker gewidmet sind, würde verhindern, dass ein Import den Pool überlastet und zu einer Zeitüberschreitung bei Benutzeranfragen führt.
4.5 Entprellen Sie das serverseitige SSE¶
Das SSE SELECT alle 2 s, auch wenn sich nichts ändert. Alternative: PostgreSQL LISTEN/NOTIFY für eine Zeichenfolge „import_job_{uuid}“. Jedes UPDATE gibt ein NOTIFY aus, die SSE-Verbindung führt LISTEN → Direct Push aus, keine Abfrage.
Kosten: erfordert eine dedizierte DB-Verbindung per SSE (LISTEN ist zustandsbehaftet). Kompromiss, der bewertet werden muss.
5. Große Optimierung: Shell-Out zu „git clone --bare“.¶
Unabhängig von DB-Optimierungen, aber mit Abstand der größte Gewinn. libgit2 ist strukturell 2–5x langsamer als „git“ CLI auf großen HTTPS-Klonen (kein Multiplexing, Single-Threaded-Delta-Auflösung usw.).
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 : - Native Geschwindigkeit (Parität mit „Git“-CLI) - Nativer Fortschritt über stderr „Objekte empfangen: 42 % (368/876), 1,10 MiB | 2,05 MiB/s` - HEAD richtig positioniert (keine Notwendigkeit für die „RepoBuilder“-Problemumgehung) - Geringere Abhängigkeit von libgit2 im Fall des ersten Klons
Gegenstück: git-Binärdatei auf dem Server erforderlich (bereits der Fall – wird von SbomService::git_archive verwendet).
6. Empfohlene Prioritäten¶
Wenn wir jetzt optimieren wollten, vorgeschlagene Reihenfolge:
- Shell-out
git clone --bare– echter Gewinn für den Benutzer (3-5x schnelleres Klonen). update_manyfürupdate_progress– dividiert den DB-Druck durch 2, mechanische Änderung von ~20 Zeilen.- Kanal beobachten + 1-Hz-Aufgabe – sauberere Architektur, eliminiert Pooling-Probleme.
- Dedizierter Worker-DB-Pool – betriebliches Sicherheitsnetz.
- LISTEN/NOTIFY für SSE – nur bei vielen gleichzeitigen Clients.
Die Gewinne 2-4 sind auch mit libgit2 nützlich; Verstärkung 1 ist für den Endbenutzer am sichtbarsten.