myutils/hubclient.go — jitter reduzido:
HubClient.Get: jitter por chamada alterado de400+rand.Intn(500)ms (intervalo [400, 900], média 650ms) para200+rand.Intn(200)ms (intervalo [200, 400], média 300ms). Redução de ~54% no overhead de sleep por chamada API. O intervalo mínimo de 200ms mantém a evasão do tarpit Cloudflare; valores abaixo de 200ms aumentam a taxa de respostas 429. Ganho estimado: +65% de throughput por worker (~23 → ~38 repos/min/worker).
buildgraph/from_mongo.go — writes MongoDB assíncronos:
persistImages,getTags: substituição deUpdateTag/UpdateImagesíncronos por envio ao canalwritesCh chan func(). Uma goroutine dedicada (criada emStartFromMongo) drena o canal em background. OrepoWorkernão bloqueia aguardando writes de cache — avança imediatamente para o próximo repo. Ganho estimado: +5% (~80ms por repo devolvidos ao ciclo útil).- Assinaturas de
repoWorker,collectBatch,getTags,getImages,persistImagesatualizadas para propagarwritesCh. - Canal fechado ordenadamente após
wgRepo.Wait()e antes declose(cpCh), garantindo que todos os writes em flight completem antes do shutdown.
buildgraph/from_mongo.go — sleep de empty-poll:
repoWorker: sleep entre tentativas de claim sem resultado reduzido de 5s para 2s. Reduz latência de re-claim em cenários de contention com múltiplos workers.
myutils/hubclient.go:
GetTag(ns, name, tagName string) (*Tag, error)— busca metadados de uma tag específica pelo nome viaGetTagMetadataURL. Retornanil, nilem 404 (tag inexistente), permitindo que o caller decida se a ausência delatesté erro ou comportamento esperado.
buildgraph/from_mongo.go — seleção de tags:
getTagspassa a buscar sempre duas tags por repositório: (1) a tag mais recentemente atualizada (GetTagspágina 1, tamanho 1) e (2) a taglatestviaGetTag. Se as duas coincidirem pelo nome, apenas uma é retornada. Motivação:latestrepresenta a versão canônica usada pela maioria dos usuários; a tag mais recente captura o estado atual do repositório — frequentemente divergem (ex.: ubuntu temresolute-20260401como mais recente elatestapontando para a LTS estável).- Parâmetro
tagCnt intremovido degetTags,processRepo,repoWorkereStartFromMongo— a seleção de tags não é mais configurável via flag.
buildgraph/build.go:
- Assinatura de
Buildatualizada:Build(format, threshold, workers, ip, dataDir)— parâmetrotagCntremovido.
cmd/cmd.go:
- Flag
--tagsremovida do subcomandobuild(obsoleta após a remoção detagCnt). - Workers do subcomando
buildderivados automaticamente delen(im.Accounts)em vez de flag manual. Se o arquivo de contas estiver vazio, fallback para 1 worker. Taxa de requisições escala linearmente com o número de contas (1 worker/conta ≈ 1,8 req/s).
docker-compose.node3.yml:
- Flag
--tagse variávelTAGSremovidas do comando de entrada do containerbuilder.
myutils/mongo.go — índice stage2_queue substituído por stage2_partial:
- O índice composto
{graph_built_at: 1, pull_count: -1}comsparse: trueestava incorreto: o flagsparsenão tem efeito quando qualquer outro campo indexado (pull_count) está presente em todos os documentos. O índice indexava os 12M repositórios integralmente e nunca encolhia. - Substituído por índice parcial
{pull_count: -1}compartialFilterExpression: {graph_built_at: {$exists: false}}. Este índice indexa apenas os repositórios ainda não processados pelo Estágio II, encolhe progressivamente à medida queMarkRepoGraphBuilté chamado, e suportaCountPendingBuildReposcom varredura de índice pura (sem fetch de documentos).
myutils/hubclient.go — cliente HTTP autenticado compartilhado (novo arquivo):
- Interface
IdentityProvidercom métodosGetNextClient() (*http.Client, string, string)eClearToken(string)— abstração que permite usar o mesmoHubClientnos Estágios I e II sem dependência circular de pacotes. - Struct
HubClient— encapsula lógica de requisições autenticadas, reutilizável por qualquer goroutine que receba umIdentityProvider. Cada goroutine cria sua própria instância. NewHubClient(ip IdentityProvider) *HubClient— construtor.Get(url string) ([]byte, int, error)— 3 tentativas com rotação automática de identidade em respostas 401, 429 e 403; headers de navegador embutidos.GetInto(url string, dest interface{}) error—Getseguido dejson.Unmarshal.GetTags(ns, name string, pageNum, size int)— busca paginada de tags com autenticação.GetImages(ns, name, tag string)— busca de manifests de imagem com autenticação.setHeaders(req *http.Request)— injeta headers de navegador Chrome 145 (centralizado; antes duplicado emcrawler.go).rotate()— rotaciona identidade internamente, chamado após respostas de erro retriáveis.
buildgraph/metrics.go — rastreamento de progresso do Estágio II (novo arquivo):
- Struct
BuildMetricscom contadores atômicos:Processed,TagCacheHits,TagAPIFetches,ImageCacheHits,ImageAPIFetches,Neo4jInserts,Errors. newBuildMetrics(threshold int64) *BuildMetrics— capturareposTotalviaCountPendingBuildReposapósResetStaleBuildClaims.startReporter(dataDir string, done <-chan struct{})— goroutine que registra métricas em log e em arquivobuild_metrics.loga cada 60 segundos.- Linha de log formatada:
[METRICS HH:MM:SS] progresso=N/Total (%) | taxa=X repos/min | ETA=Xh | cache tags=% imgs=% | neo4j=N | erros=N | uptime=Xs. - ETA calculado após 30 segundos de dados coletados:
ETA = (total−processed) / (processed / elapsed_minutes).
docker-compose.node3.yml — orquestração do Nó 3 (novo arquivo):
- Serviço
builderpara execução exclusiva do Estágio II em máquina auxiliar. - Variáveis de ambiente:
MONGO_URI,NEO4J_URI,ACCOUNTS,THRESHOLD,TAGS. - Comando de entrada: compilação e execução do subcomando
buildcom flags--accounts,--threshold,--tags,--data_dir.
myutils/mongo.go — novos métodos:
ClaimNextBuildRepo(threshold int64) (*Repository, error)—FindOneAndUpdateatômico: aplica{build_claimed: true, build_started_at: now}no documento. Filtro:{pull_count >= threshold, graph_built_at: {$exists: false}, build_claimed: {$ne: true}}. Ordenação:{pull_count: -1}.ResetStaleBuildClaims()— liberação de claims órfãos na inicialização: documentos com{build_claimed: true, graph_built_at: {$exists: false}}têmbuild_claimedebuild_started_atremovidos via$unset.CountPendingBuildRepos(threshold int64) (int64, error)— contagem de repositórios sem claim e semgraph_built_at, utilizada pelo immortal worker pattern.FindImagesByDigests(digests []string) (map[string]*Image, error)— query em lote com operador$in; substitui N chamadas individuais aFindImageByDigest, reduzindo o padrão N+1 a uma única query.
cmd/cmd.go — novos flags do subcomando build:
--accounts— caminho paraaccounts.json(contas Docker Hub para autenticação JWT).--proxies— caminho para arquivo de proxies.--data_dir— diretório de destino parabuild_checkpoint.jsonlebuild_metrics.log.
Makefile — novos alvos:
start-build— inicia o Estágio II viadocker-compose.node3.yml.logs-build— exibe e acompanha os logs do containerditector_builder.
crawler/crawler.go:
- Método
setBrowserHeadersremovido; a lógica foi centralizada emHubClient.setHeaders(myutils/hubclient.go). worker()instanciahub := myutils.NewHubClient(pc.IM)localmente; os parâmetrosclient,tokeneuaforam removidos da assinatura das funções internas.fetchPage(hub, query, page)simplificada: delega retry e rotação de identidade aoHubClient.Get; mantém apenas o cooloff de 4 minutos para erros HTTP não retriáveis (comportamento específico do Estágio I).processTask(hub, prefix)não recebe nem retorna maisclient,tokenouua.- Arrays
uaWindowseuaLinuxMacremovidos; seleção de User-Agent por ROLE removida — o UA é gerenciado exclusivamente peloIdentityManager. - Immortal worker:
emptyCountagora usaCountDocumentspara confirmar que a fila está efetivamente vazia antes de encerrar o worker. - Timeout de
getNextTask: 10s → 30s.
buildgraph/from_mongo.go — reescrita completa:
repoWorkermigrado de cursor MongoDB paraClaimNextBuildRepo(claim atômico), idêntico ao padrãogetNextTaskdo Estágio I.- Cada goroutine
repoWorkercria seu próprioHubClientautenticado viaIdentityProvider. processRepousadefer markBuilt— repositório marcado como concluído em qualquer caso, inclusive aqueles com 0 tags, eliminando reprocessamento infinito.getTagsegetImagesrecebem e incrementam*BuildMetrics.graphWorker(jobChan, m)incrementam.Neo4jInsertsa cada inserção no Neo4j.- Goroutine
checkpointWriter— escritor single-writer que persiste linhas JSONL emdataDir/build_checkpoint.jsonl. - Funções renomeadas:
fetchTags→getTags,fetchImages→getImages. - Cache de imagens: padrão N+1 queries substituído por uma única query
$inviaFindImagesByDigests. persistImagesextraída como função independente.
myutils/mongo.go:
MarkRepoGraphBuiltatualizada: além de gravargraph_built_at, agora removebuild_claimedebuild_started_atvia$unset.
buildgraph/build.go:
- Assinatura de
Buildatualizada:Build(format, tagCnt, threshold, ip IdentityProvider, dataDir)— parâmetrospageepageSizeremovidos (eram ignorados na implementação anterior).
myutils/urls.go:
V2SearchURLTemplate: parâmetro&ordering=-pull_countremovido. O Docker Hub utilizabest_matchcomo modo de ordenação padrão, que prioriza correspondências exatas antes de resultados por popularidade.
docker-compose.yml:
- Volume do Neo4j alterado de named Docker volume (
neo4j_data:/data) para volume de host (./neo4j_data:/data), evitando perda de dados pordocker system prune -a --volumes. - Seção
volumes:(named volumes) removida do arquivo.
- Repositórios com 0 tags reprocessados indefinidamente: o Estágio II anterior não marcava
graph_built_atem repositórios que retornavam 0 tags da API, causando reprocessamento infinito desses registros. O uso dedefer markBuiltemprocessRepogarante que o campo seja gravado independentemente do resultado da busca de tags. - Duplicação de headers de navegador entre estágios:
setBrowserHeadersestava duplicada emcrawler/crawler.goe em chamadas do Estágio II. A centralização emHubClient.setHeaderselimina a divergência. - Claims órfãos após crash do Stage II: ao reiniciar,
ResetStaleBuildClaimslibera automaticamente todos os documentos combuild_claimed=truesemgraph_built_at, garantindo retomada sem intervenção manual. - N+1 queries para cache de imagens no Stage II: substituído por uma única query MongoDB com operador
$inemFindImagesByDigests.
Fila de tarefas persistente com prioridade (crawler/crawler.go):
ensureQueueInitialized(seeds)— inicializacrawler_keywordscom seeds do alfabeto se vazia; reverte automaticamente tarefas no estado"processing"para"pending"ao reiniciar (auto-healing de crashes anteriores). Se a fila já contém tarefas, retoma de onde parou sem re-inserção.getNextTask()—FindOneAndUpdateatômico com filtro{status: "pending"}e ordenação composta{priority: -1, _id: 1}: prefixos de maior prioridade são processados primeiro; empates resolvidos lexicograficamente.updateTaskStatus(id, status)— atualiza status efinished_atde uma tarefa; usado para transitar para"done"ou reverter para"pending"em falha.- Campo
priority intno documento de tarefacrawler_keywords, com semântica definida por tipo de correspondência:priority=2— filho sem hifén: correspondência genuína de substring, sem tokenização pelo ElasticSearch. Alta probabilidade de novos repositórios distintos.priority=1— filho quando o pai encontrou repositórios novos (newInPrefix > 0).priority=0— padrão (pai não encontrou novos repositórios).priority=-1— filho de "token-match plateau" (depriorizados; processados por último, mas sem perda de dados).
Detecção de token-match plateau e depriorização (crawler/crawler.go):
- Condição de plateau:
newInPrefix == 0 && res.Count >= 10000 && strings.Contains(prefix, "-") && len(prefix) > 1 - Semântica: o Docker Hub usa hifén como separador de token no ElasticSearch. Um prefixo com hifén retornando 10.000 resultados mas zero repositórios novos é uma correspondência de token completo sem novidade para o dataset. Aprofundar nessa direção com prioridade normal saturaria a fila com tarefas de baixo retorno.
- Ação: todos os filhos são inseridos com
priority=-1em vez depriority=0. Depriorização sem remoção — os dados não se perdem, apenas ficam para o final da fila. - Log explícito:
>>> DEPRIORITIZING [prefix]: token-match plateau (N results, 0 new). Children set to priority=-1.
Deduplicação de separadores consecutivos (crawler/crawler.go):
- Se o último caractere do prefixo é
-ou_, os filhos-e_são omitidos ao gerar a expansão. - Motivação: o ElasticSearch do Docker Hub trata
--,-_,_-,__como equivalentes ao separador simples. Gerar esses filhos causaria DFS de profundidade ilimitada retornando resultados idênticos ao pai, sem nenhum dado novo.
Aquecimento de cache em RAM (crawler/crawler.go):
PreloadExistingRepos()— executa ao iniciar, antes de qualquer worker. FazFindemRepoCollcom projeção{namespace, name}e populaseenRepos sync.Mapcom todos os repositórios já persistidos.- Impede chamadas à API e round-trips ao banco para repositórios já coletados em execuções anteriores. Deduplicação ocorre em microssegundos na RAM local em vez de sub-milissegundos via rede.
- Escala testada: 5,2 milhões de registros em ~300 MB de RAM. Log de progresso a cada 250.000 registros; log final com contagem e duração total.
Logging de eficiência por prefixo (crawler/crawler.go):
efficiency = (newInPrefix / pages×100) × 100%- Log ao final de cada tarefa:
[DONE] Prefix [prefix]: +N unique | Eff: X.X% | Found total: N - Métrica de diagnóstico: prefixos com eficiência próxima de 0% indicam regiões do espaço DFS já saturadas pelo cache.
Fingerprint de navegador — identidade por conta (crawler/auth_proxy.go):
globalUAPool: pool de 7 User-Agents cobrindo Chrome 121/119 (Windows/Mac/Linux), Edge 121, Firefox 122, Safari 17.- Cada conta recebe UA fixo no carregamento via round-robin:
acc.UserAgent = globalUAPool[i % len(globalUAPool)]. A mesma conta sempre usa o mesmo UA em login, busca e manifests — coerência de identidade observável pelo servidor. GetNextClient()retorna(*http.Client, token, ua)— os três atributos da identidade propagados juntos para garantir que cada requisição pertença à mesma "sessão de navegador".- Auto-login JWT protegido por
loginMu sync.Mutex— previne login paralelo da mesma conta por múltiplos workers. ClearToken(token)zera o token da conta correspondente, forçando re-autenticação na próxima chamada aGetNextClient.
Stack TLS e timeouts de rede (crawler/auth_proxy.go):
TLSClientConfig{MinVersion: tls.VersionTLS12, PreferServerCipherSuites: false}— emula o perfil de negociação TLS do Chrome (servidor escolhe a ordem das cifras).TLSHandshakeTimeout = 5s,ResponseHeaderTimeout = 5s, timeout total da requisição = 10s.- HTTP/2 não configurado no transporte — deliberadamente evitado para prevenir bloqueio de workers por conexões half-open durante tarpit de rate limit (com HTTP/1.1 cada conexão é atômica e o timeout funciona deterministicamente).
MaxIdleConns = 100,IdleConnTimeout = 90s,MaxIdleConnsPerHost = 10— pool de conexões TCP ativo.
Semântica do código HTTP 404 (crawler/crawler.go):
- HTTP 404 em
fetchPageretorna&V2SearchResponse{}(vazio) comok=true— não é falha. - Interpretação: 404 indica que
starting_indexexcede o total de resultados da query. O loop de paginaçãofor p := 2; p <= pages; p++aborta naturalmente ao receber slice vazio em qualquer página. - Antes desta versão, 404 causava re-enfileiramento da tarefa como
"pending"ou cooldown de 30s — comportamento incorreto que desperdiçava tempo e re-processava tarefas válidas.
Cooldown estendido para erros HTTP inesperados (crawler/crawler.go):
- Códigos HTTP fora do conjunto
{200, 404, 401, 429, 403}: log com os primeiros 200 bytes do body diagnóstico e cooldown de 4 minutos antes de retornar falha. - Antes desta versão o cooldown era de 30 a 90 segundos — insuficiente para o período de cache do WAF do Cloudflare em respostas de erro.
- DFS de profundidade infinita em separadores consecutivos: prefixo terminado em
-ou_gerava filhos--,-_cujos resultados eram idênticos ao pai, causando fan-out de profundidade ilimitada sem dados novos. - Terminação prematura de workers em fila temporariamente vazia: workers encerravam após N falhas consecutivas de
getNextTask, mesmo com tarefas genuinamente pendentes temporariamente claimed por outros workers no cluster. Corrigido: worker só encerra quandoCountDocuments({status: "pending"})confirma zero tarefas restantes. - Saturação DFS em regiões densas: sem priorização, prefixos de token-match acumulavam centenas de filhos que cobriam a mesma região lexical, bloqueando prefixos de alta utilidade de chegar à frente da fila.
crawler/ (implementação do Estágio I — stub sem Run no upstream):
crawler/crawler.go:ParallelCrawlercom DFS recursivo sobre espaço de prefixos do Docker Hub. N workers independentes, cada um executandocrawlDFSrecursivamente a partir de suas seeds. Deduplicação em memória viaseenRepos sync.Map. Aprofundamento forçado para prefixos de 1 caractere. GoroutinerepoWritercomBulkWriteao MongoDB a cada 2s ou 1.000 repositórios. Checkpointing post-order por keyword via coleçãocrawler_keywords.crawler/auth_proxy.go:IdentityManager— carrega contas Docker Hub deaccounts.jsone proxies de arquivo texto; auto-login JWT comsync.Mutex; rotação round-robin de identidades viaGetNextClient().
buildgraph/from_mongo.go (Estágio II reengenhado):
- Pipeline de três estágios desacoplados por buffered channels: Loader (MongoDB →
repoChanbuf 4.000), repoWorkers (max(NumCPU×16, 64)) e buildGraphWorkers (max(NumCPU×4, 16)). - Semáforo
tagConcurrency=4por repositório para controle de requisições paralelas de manifest. - Checkpointing
graph_built_atno MongoDB após conclusão de cada repositório.
myutils/mongo.go:
BulkUpsertRepositories: bulk write não-ordenado com upsert por{namespace, name}.KeywordsColl,IsKeywordCrawled,MarkKeywordCrawled: sistema de checkpoint para o Estágio I.MarkRepoGraphBuilt: checkpoint para o Estágio II.- Connection pool:
MaxPoolSize=100,MinPoolSize=5,MaxConnIdleTime=5min. - Timeout do ping inicial:
1s → 30s.
myutils/neo4j.go:
InsertImageToNeo4jreescrito: IDs de layer pré-computados localmente via SHA256 chain; toda a cadeia de layers inserida em uma única transaçãoExecuteWrite(O(1) round-trips por imagem, em vez de O(N layers)).
myutils/urls.go:
V2SearchURLTemplateeGetV2SearchURL— API V2 do Docker Hub comordering=-pull_count.
myutils/config.go:
- Override por variáveis de ambiente
MONGO_URIeNEO4J_URI. - Localização do config por
os.Getwd()(compatível comgo run). - Neo4j opcional na inicialização.
myutils/docker_hub_api_requests.go:
- Keep-alives habilitados (conexões TCP/TLS reutilizadas entre requisições).
- Connection pool:
MaxIdleConns=300,MaxIdleConnsPerHost=50,IdleConnTimeout=90s,Timeout=30s.
cmd/cmd.go:
- Subcomando
crawlcom flags--workers,--seed,--shard,--shards,--accounts,--proxies,--config.
Infraestrutura:
docker-compose.yml: MongoDB, Neo4j, crawler.docker-compose.node2.yml: Node 2 apontando para MongoDB do Node 1.automation/pipeline_autopilot.sh: orquestração sequencial dos 3 estágios.automation/test_e2e.sh: teste de integração end-to-end.
-
myutils/neo4j.go—findLayerNodesByRawLayerDigestFunc(crítico): query Cypher usava{id: $digest}para matchar nóRawLayer, mas a propriedade armazenada édigest. A propriedadeidnão existe emRawLayer. A query nunca retornava resultados, quebrando silenciosamente o rastreamento de imagens upstream. Corrigido para{digest: $digest}. -
scripts/calculate_node_dependent_weights.go(médio): branchif repoDoc.Namespace == "library"continhacontinuecomo primeira instrução, tornando todo o código subsequente inalcançável. Imagens oficiais Docker eram ignoradas no cálculo de dependency weight.continueremovido. -
myutils/docker_hub_api_requests.go(médio):DisableKeepAlives: trueimpedia reutilização de conexões TCP, acrescentando ~100–300ms de handshake+TLS por requisição desnecessariamente. Removido. -
automation/test_e2e.sh(baixo): sintaxe[ [inválida em bash substituída por[ "$(expr)" -gt N ].
Pipeline original com subcomando crawl declarado em cmd/cmd.go sem campo Run (stub sem implementação). Estágios II e III funcionais. Implementa: buildgraph/build.go (inserção síncrona por layer no Neo4j), myutils/, scripts/, analyzer/, cmd/, main.go.