Importe um repositório externo¶
Referência do fluxo de importação do repositório externo no gitrust: função do banco de dados, diagrama de sequência completo e caminhos de otimização.
1. Papel do banco de dados¶
Durante uma importação, o banco de dados atende quatro funções 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 |
O clone em si nunca passa pelo banco de dados: apenas metadados e contadores de progresso passam por ele. Os objetos Git são gravados diretamente no disco em {GIT_REPOS_BASE_PATH}/{owner}/{slug}.git/.
Por que persistir o estado?¶
- Retomar após reiniciar: o servidor pode travar durante uma clonagem que dura vários minutos. A tabela permite que esses trabalhos sejam marcados como
com falhana reinicialização. - SSE: a UI lê o progresso por meio de um endpoint SSE que pesquisa o banco de dados a cada 2 s. Sem o banco de dados, você precisaria de um canal na memória, além de um mecanismo de roteamento para o cliente certo.
- Multinavegador: se o usuário fechar a guia e reabri-la, ele encontrará o status exato do trabalho.
- Auditoria/histórico: conservação das importações passadas com suas estatísticas.
2. Diagrama de fluxo atual¶
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. Custo do banco de dados por importação¶
Para um clone de 90 segundos com aceleração de 1.500 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 solicitações de importação, das quais 120+ são atualizações de progresso.
4. Caminhos de otimização (sem alterar libgit2)¶
4.1 Elimine o SELECT antes de UPDATE em update_progress¶
update_progress atualmente faz load_model (SELECT) e depois active.update(db) (UPDATE + RETURNING). O SELECT é redundante: sabemos o ID, só queremos corrigir 4 colunas.
Ganho: divide as solicitações de progresso por 2 (~60 solicitações a menos para um clone de 90 s).
Redesenho sugerido com UpdateMany (padrão já usado em 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?;
O mesmo princípio para mark_running, mark_success, mark_failed, mark_cancelled → todos dividem suas viagens de ida e volta do banco de dados por 2.
4.2 Reduzir a progressão no trabalhador (dedicado)¶
Plano alternativo: o retorno de chamada simplesmente grava em tokio::sync::watch<TransferStats> (na memória, zero DB). Uma tarefa dedicada consome este canal e ATUALIZA o banco de dados apenas em 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 único transmissor DB em vez de N concorrentes tokio::spawn → zero contenção no pool
- Frequência determinística (1 Hz) independente da velocidade da rede
- Código mais claro (a tarefa é assíncrona, podemos aguardar diretamente)
4.3 Remover RETURNING implícito do SeaORM¶
ActiveModel::update(db) retorna o modelo completo via ... RETURNING *. Para atualizações de progresso, não usamos feedback. UpdateMany::exec não emite RETURNING → menos bytes na conexão, menos desserialização.
4.4 Pool de banco de dados dedicado para o trabalhador¶
Hoje o trabalhador compartilha o pool principal com os manipuladores HTTP. Um pool de 4 a 8 conexões dedicadas ao trabalhador impediria que uma importação saturasse o pool e causasse o tempo limite das solicitações do usuário.
4.5 Debounce do SSE do lado do servidor¶
O SSE SELECT a cada 2 s mesmo quando nada muda. Alternativa: PostgreSQL LISTEN/NOTIFY em uma string import_job_{uuid}. Cada UPDATE emite um NOTIFY, a conexão SSE faz LISTEN → direct push, zero polling.
Custo: requer uma conexão de banco de dados dedicada por SSE (LISTEN tem estado). Compromisso a ser avaliado.
5. Grande otimização: shell-out para git clone --bare¶
Independente de otimizações de banco de dados, mas de longe o maior ganho. libgit2 é estruturalmente 2 a 5x mais lento que a CLI git em grandes clones HTTPS (sem multiplexação, resolução delta de thread único, 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 :
- Velocidade nativa (paridade com git CLI)
- Progresso nativo via stderr Recebendo objetos: 42% (368/876), 1,10 MiB | 2,05 MiB/s
- HEAD posicionado corretamente (não há necessidade da solução alternativa RepoBuilder)
- Menos dependência da libgit2 para o caso do clone inicial
Contraparte: binário git necessário no servidor (já é o caso — usado por SbomService::git_archive).
6. Prioridades recomendadas¶
Se quiséssemos otimizar agora, ordem proposta:
- Shell-out
git clone --bare— ganho real para o usuário (clone 3-5x mais rápido). update_manyparaupdate_progress— divide a pressão do banco de dados por 2, mudança mecânica de aproximadamente 20 linhas.- canal
watch+ tarefa de 1 Hz — arquitetura mais limpa, elimina problemas de pooling. - Pool de banco de dados de trabalhadores dedicados — rede de segurança operacional.
- LISTEN/NOTIFY para SSE — somente se houver muitos clientes simultâneos.
Os ganhos 2-4 são úteis mesmo com libgit2; o ganho 1 é o mais visível para o usuário final.