Importar un repositorio externo¶
Referencia del flujo de importación del repositorio externo en gitrust: rol de la base de datos, diagrama de secuencia completo y vías de optimización.
1. Papel de la base de datos¶
Durante una importación, la base de datos cumple cuatro funciones distintas:
| 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 |
El clon en sí nunca pasa a través de la base de datos: solo los metadatos y los contadores de progreso lo atraviesan. Los objetos Git se escriben directamente en el disco en {GIT_REPOS_BASE_PATH}/{owner}/{slug}.git/.
¿Por qué persistir el Estado?¶
- Reanudar después del reinicio: el servidor puede fallar durante una clonación que dura varios minutos. La tabla permite que estos trabajos se marquen como "fallidos" al reiniciar.
- SSE: la interfaz de usuario lee el progreso a través de un punto final SSE que sondea la base de datos cada 2 s. Sin DB, necesitaría un canal en memoria más un mecanismo de enrutamiento al cliente correcto.
- Multinavegador: si el usuario cierra la pestaña y la vuelve a abrir, encontrará el estado exacto del trabajo.
- Auditoría/historial: conservación de importaciones pasadas con sus estadísticas.
2. Diagrama de flujo actual¶
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 de BD por importación¶
Para un clon de 90 segundos con aceleración de 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 solicitudes de importación, de las cuales 120+ son actualizaciones de progreso.
4. Rutas de optimización (sin cambiar libgit2)¶
4.1 Eliminar el SELECT antes de ACTUALIZAR en update_progress¶
update_progress actualmente hace load_model (SELECCIONAR) y luego active.update(db) (ACTUALIZAR + REGRESAR). SELECT es redundante: conocemos el ID, solo queremos parchear 4 columnas.
Ganancia: divide las solicitudes de progreso por 2 (~60 solicitudes menos para un clon de 90 s).
Rediseño sugerido con UpdateMany (patrón ya utilizado en 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?;
El mismo principio para mark_running, mark_success, mark_failed, mark_cancelled → todos dividen sus viajes de ida y vuelta a la base de datos por 2.
4.2 Reducir la progresión del trabajador (dedicado)¶
Plan alternativo: la devolución de llamada simplemente escribe en tokio::sync::watch<TransferStats> (en memoria, cero DB). Una tarea dedicada consume este canal y ACTUALIZA la base de datos 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 transmisor DB único en lugar de N compitiendo tokio::spawn → cero contención en el grupo - Frecuencia determinista (1 Hz) independiente de la velocidad de la red - Código más claro (la tarea es asíncrona, podemos "esperar" directamente)
4.3 Eliminar el "RETORNO" implícito de SeaORM¶
ActiveModel::update(db) devuelve el modelo completo mediante ... RETURNING *. Para actualizaciones de progreso, no utilizamos comentarios. UpdateMany::exec no emite RETURNING → menos bytes en el cable, menos deserialización.
4.4 Grupo de bases de datos dedicado para el trabajador¶
Hoy el trabajador comparte el grupo principal con los controladores HTTP. Un grupo de 4 a 8 conexiones dedicadas al trabajador evitaría que una importación sature el grupo y provoque que las solicitudes de los usuarios expiren.
4.5 Antirrebote del SSE del lado del servidor¶
El SSE SELECT cada 2 s incluso cuando nada cambia. Alternativa: PostgreSQL ESCUCHAR/NOTIFY en una cadena import_job_{uuid}. Cada ACTUALIZACIÓN emite una NOTIFICACIÓN, la conexión SSE ESCUCHA → envío directo, cero sondeo.
Costo: requiere una conexión de base de datos dedicada mediante SSE (LISTEN tiene estado). Compromiso a evaluar.
5. Optimización importante: desembolso de git clone --bare¶
Independiente de las optimizaciones de la base de datos pero, con diferencia, es la mayor ganancia. libgit2 es estructuralmente entre 2 y 5 veces más lento que la CLI git en clones HTTPS grandes (sin multiplexación, resolución delta de un solo subproceso, 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 :
- Velocidad nativa (paridad con la CLI git)
- Progreso nativo vía stderr Recepción de objetos: 42% (368/876), 1,10 MiB | 2,05 MB/s
- HEAD correctamente posicionado (no es necesaria la solución alternativa RepoBuilder)
- Menos dependencia de libgit2 para el caso del clon inicial
Contraparte: binario git requerido en el servidor (ya es el caso, usado por SbomService::git_archive).
6. Prioridades recomendadas¶
Si quisiéramos optimizar ahora, orden propuesto:
- Shell-out
git clone --bare: ganancia real para el usuario (clon 3-5 veces más rápido). update_manyparaupdate_progress: divide la presión de la base de datos por 2, cambio mecánico de ~20 líneas.- canal
watch+ tarea de 1 Hz: arquitectura más limpia, elimina problemas de agrupación. - Grupo de bases de datos de trabajadores dedicados: red de seguridad operativa.
- ESCUCHAR/NOTIFICAR para SSE: solo si hay muchos clientes simultáneos.
Las ganancias 2-4 son útiles incluso con libgit2; La ganancia 1 es la más visible para el usuario final.