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 falha na 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:

  1. Shell-out git clone --bare — ganho real para o usuário (clone 3-5x mais rápido).
  2. update_many para update_progress — divide a pressão do banco de dados por 2, mudança mecânica de aproximadamente 20 linhas.
  3. canal watch + tarefa de 1 Hz — arquitetura mais limpa, elimina problemas de pooling.
  4. Pool de banco de dados de trabalhadores dedicados — rede de segurança operacional.
  5. 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.