feat(scrapper): implementar concorrência por provider - #231

Merged
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider
Aug 28, 2026
Merged

feat(scrapper): implementar concorrência por provider#231
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider

Conversation

@RuhanFreitas

@RuhanFreitasRuhanFreitas commented Aug 25, 2026

Copy link
Copy Markdown
Member

Card

  • PAV-120

O que foi feito

  • Controla a concorrência intra-execução do scraper-go com dois tetos: global (SCRAPER_MAX_CONCURRENCY, padrão 12) e por provider (SCRAPER_PROVIDER_MAX_CONCURRENCY, padrão 2), com overrides opcionais em SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES no formato provider=limite. Config inválida, vazia, duplicada, desconhecida ou acima do teto global impede a inicialização.
  • Substitui o fan-out adapters × keywords por um scheduler round-robin com fila limitada (maxConcurrency * 2) e workers fixos. O orçamento é cancelável, o release é idempotente e o limite efetivo é min(provider, global). Providers com várias instâncias (Greenhouse/Lever) compartilham o mesmo limite.
  • Declara capacidades por fonte: IDs canônicos (linkedin, adzuna, themuse, gupy, inhire, jooble, greenhouse, lever) e modos batch (LinkedIn, Adzuna, Jooble, Gupy), catalog (The Muse, InHire, Greenhouse, Lever) e keyword para fontes ainda não migradas. Catálogos são buscados uma vez e agregam keywords, sem duplicar jobs.
  • Remove concorrência interna dos adapters (LinkedIn, Adzuna, Gupy, InHire, The Muse); loops passam a ser seriais e canceláveis. INHIRE_DETAILS_CONCURRENCY foi removida. Logs agregados: scraper concurrency budget no início da run e scraper provider execution summary por provider. A fórmula da cache key permanece inalterada.

Validação

  • go test ./... em scraper-go
  • go test -race ./... em scraper-go
  • Conferir .env.example, docker-compose.yml e SCRAPER.md com SCRAPER_PROVIDER_MAX_CONCURRENCY=2 e SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES=""
  • Confirmar que INHIRE_DETAILS_CONCURRENCY não aparece mais em env/Compose/docs
  • Subir o scraper e validar nos logs o budget no início da run e o summary por provider
  • Testar override inválido (provider desconhecido, limite 0, acima do global) e confirmar fail-fast na inicialização

Benevanio
Benevanio previously approved these changes Aug 26, 2026

@hltavhltav left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A implementação principal está bem estruturada e segue corretamente a direção da PAV-120: orçamento global e por provider, scheduler round-robin, fila limitada, backpressure, capacidades explícitas e remoção da concorrência interna dos adapters.

Antes do merge, precisamos concluir os critérios ainda não demonstrados:

aguardar explicitamente o produtor;
classificar corretamente cancelamentos com causa customizada;
preservar uma amostra estruturada dos erros e registrar limites efetivos;
completar os testes de cancelamento, liberação, lote e encerramento de goroutines;
estabilizar a suíte completa com race detector;
remover o comentário obsoleto do Adzuna.

Depois dessas correções e da execução bem-sucedida de go test ./... e go test -race ./..., a PR fica pronta para aprovação e merge, sem deixar esses pontos como débito técnico.

##Cobertura de Testes

A cobertura adicionada é boa, mas ainda faltam demonstrações explícitas de alguns critérios de aceite da PAV-120. Antes do merge, por favor, adicionar testes para:

limite global 1 executando múltiplas tarefas estritamente de forma sequencial;
permissões liberadas depois de sucesso, erro do adapter e cancelamento durante execução;
produtor e workers encerrados após sucesso, erro e cancelamento;
cancelamento enquanto o produtor está bloqueado por backpressure;
ausência de tarefas iniciadas depois do cancelamento;
cancelamento durante requisição HTTP em andamento;
cancelamento durante espera de retry;
cancelamento durante paginação;
limite do lote/slot respeitado por providers batch;
encerramento sem vazamento de goroutines.

A validação de goroutines pode ser feita com goleak ou por sincronização determinística com canais/WaitGroups, desde que o teste comprove que todas as goroutines criadas pela execução terminaram antes do retorno.

runStats.recordCompleted(task, err, time.Since(started))

metrics.ScrapeRunsTotal.WithLabelValues(source).Inc()
if err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precisamos preservar informações operacionais sobre a falha da tarefa. Atualmente o erro é contabilizado na métrica e no resumo agregado, mas seu conteúdo e a instância concreta do adapter são descartados. Com isso, o summary pode informar errors > 0, porém não permite identificar qual erro ocorreu nem qual fonte/instância falhou, especialmente em providers com vários adapters, como Greenhouse e Lever.

Antes do merge, por favor, registre uma amostra estruturada do erro por provider, incluindo pelo menos provider, source (task.adapter.SourceName()), mode e error. Isso pode ser incorporado ao resumo agregado para evitar um log por tarefa e não aumentar excessivamente a cardinalidade.

Aproveitar o mesmo summary para informar também o max_concurrency_effective do provider, pois o override registrado pode ser maior que o limite efetivo quando a requisição reduz a concorrência global.

Essa correção é necessária para atender aos critérios da PAV-120 de identificar provider e tarefa nos erros, registrar erros/timeouts por provider e informar o comportamento efetivo do pipeline.

break schedule
}

go produceTasks(ctx, tasks, adapterList, req.Keywords, runStats)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O produtor é iniciado em uma goroutine, mas não faz parte do WaitGroup. Atualmente aguardamos somente os workers antes de fechar results e retornar. Em caso de cancelamento, o produtor tende a encerrar ao observar o contexto, mas não existe garantia explícita de que ele terminou antes do retorno de runWithConcurrency.

A PAV-120 exige encerramento de produtores e workers, ausência de goroutines abandonadas e espera de todas as goroutines antes do retorno. Por favor, inclua o produtor no mecanismo de sincronização — via WaitGroup, producerDone ou errgroup — garantindo que ele tenha terminado antes de a execução retornar.

Adicionar também um teste que cancele a execução enquanto a fila estiver cheia e comprove que:

o produtor termina;
os workers terminam;
nenhuma nova tarefa começa após o cancelamento;
Run retorna somente depois do encerramento dessas goroutines.

s.summary(task).Produced++
}

func (s *providerRunStats) recordCompleted(task adapterTask, err error, duration time.Duration) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A classificação considera cancelamento apenas quando err é compatível com context.Canceled. Quando a execução é cancelada com uma causa customizada, como runlock.ErrLost, o adapter pode retornar essa causa e o summary contabiliza a tarefa como erro comum, não como cancelamento.

Isso torna incorretos os logs operacionais justamente no cenário de perda do lock distribuído, que é um critério explícito da PAV-120.

Por favor, faça recordCompleted receber ou considerar o estado/cause do contexto e diferencie:

cancelamento comum;
deadline/timeout;
perda do lock;
erro real do provider.

Adicionar teste com context.WithCancelCause usando uma causa customizada e confirmar que a tarefa é contabilizada como cancelada e que o motivo do encerramento é preservado.

@hltav

Copy link
Copy Markdown
Collaborator

Os três pontos levantados no review foram atendidos de forma satisfatória. A implementação agora aguarda produtor e workers, possui cobertura de lifecycle com goleak, classifica causas customizadas e registra erros estruturados com limite efetivo por provider.

Restou apenas uma melhoria não bloqueante na precedência de context.Cause(ctx) em outcomeReason, que pode fazer o summary registrar context canceled em vez da causa específica da perda do lock. Como isso não altera o comportamento funcional do pipeline, será registrado como débito técnico.

Testes, race detector e CI passaram. Aprovado.

@hltav
hltav self-requested a review August 28, 2026 13:21
@hltav
hltav merged commit e1de6de into developAug 28, 2026
1 check passed
@github-project-automationgithub-project-automationBot moved this from Backlog to Done in JobAtlas – KanbanAug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@RuhanFreitas@hltav@Benevanio
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(scrapper): implementar concorrência por provider - #231

Merged
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider
Aug 28, 2026
Merged

feat(scrapper): implementar concorrência por provider#231
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider

Conversation

@RuhanFreitas

@RuhanFreitasRuhanFreitas commented Aug 25, 2026

Copy link
Copy Markdown
Member

Card

  • PAV-120

O que foi feito

  • Controla a concorrência intra-execução do scraper-go com dois tetos: global (SCRAPER_MAX_CONCURRENCY, padrão 12) e por provider (SCRAPER_PROVIDER_MAX_CONCURRENCY, padrão 2), com overrides opcionais em SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES no formato provider=limite. Config inválida, vazia, duplicada, desconhecida ou acima do teto global impede a inicialização.
  • Substitui o fan-out adapters × keywords por um scheduler round-robin com fila limitada (maxConcurrency * 2) e workers fixos. O orçamento é cancelável, o release é idempotente e o limite efetivo é min(provider, global). Providers com várias instâncias (Greenhouse/Lever) compartilham o mesmo limite.
  • Declara capacidades por fonte: IDs canônicos (linkedin, adzuna, themuse, gupy, inhire, jooble, greenhouse, lever) e modos batch (LinkedIn, Adzuna, Jooble, Gupy), catalog (The Muse, InHire, Greenhouse, Lever) e keyword para fontes ainda não migradas. Catálogos são buscados uma vez e agregam keywords, sem duplicar jobs.
  • Remove concorrência interna dos adapters (LinkedIn, Adzuna, Gupy, InHire, The Muse); loops passam a ser seriais e canceláveis. INHIRE_DETAILS_CONCURRENCY foi removida. Logs agregados: scraper concurrency budget no início da run e scraper provider execution summary por provider. A fórmula da cache key permanece inalterada.

Validação

  • go test ./... em scraper-go
  • go test -race ./... em scraper-go
  • Conferir .env.example, docker-compose.yml e SCRAPER.md com SCRAPER_PROVIDER_MAX_CONCURRENCY=2 e SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES=""
  • Confirmar que INHIRE_DETAILS_CONCURRENCY não aparece mais em env/Compose/docs
  • Subir o scraper e validar nos logs o budget no início da run e o summary por provider
  • Testar override inválido (provider desconhecido, limite 0, acima do global) e confirmar fail-fast na inicialização

Benevanio
Benevanio previously approved these changes Aug 26, 2026

@hltavhltav left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A implementação principal está bem estruturada e segue corretamente a direção da PAV-120: orçamento global e por provider, scheduler round-robin, fila limitada, backpressure, capacidades explícitas e remoção da concorrência interna dos adapters.

Antes do merge, precisamos concluir os critérios ainda não demonstrados:

aguardar explicitamente o produtor;
classificar corretamente cancelamentos com causa customizada;
preservar uma amostra estruturada dos erros e registrar limites efetivos;
completar os testes de cancelamento, liberação, lote e encerramento de goroutines;
estabilizar a suíte completa com race detector;
remover o comentário obsoleto do Adzuna.

Depois dessas correções e da execução bem-sucedida de go test ./... e go test -race ./..., a PR fica pronta para aprovação e merge, sem deixar esses pontos como débito técnico.

##Cobertura de Testes

A cobertura adicionada é boa, mas ainda faltam demonstrações explícitas de alguns critérios de aceite da PAV-120. Antes do merge, por favor, adicionar testes para:

limite global 1 executando múltiplas tarefas estritamente de forma sequencial;
permissões liberadas depois de sucesso, erro do adapter e cancelamento durante execução;
produtor e workers encerrados após sucesso, erro e cancelamento;
cancelamento enquanto o produtor está bloqueado por backpressure;
ausência de tarefas iniciadas depois do cancelamento;
cancelamento durante requisição HTTP em andamento;
cancelamento durante espera de retry;
cancelamento durante paginação;
limite do lote/slot respeitado por providers batch;
encerramento sem vazamento de goroutines.

A validação de goroutines pode ser feita com goleak ou por sincronização determinística com canais/WaitGroups, desde que o teste comprove que todas as goroutines criadas pela execução terminaram antes do retorno.

runStats.recordCompleted(task, err, time.Since(started))

metrics.ScrapeRunsTotal.WithLabelValues(source).Inc()
if err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precisamos preservar informações operacionais sobre a falha da tarefa. Atualmente o erro é contabilizado na métrica e no resumo agregado, mas seu conteúdo e a instância concreta do adapter são descartados. Com isso, o summary pode informar errors > 0, porém não permite identificar qual erro ocorreu nem qual fonte/instância falhou, especialmente em providers com vários adapters, como Greenhouse e Lever.

Antes do merge, por favor, registre uma amostra estruturada do erro por provider, incluindo pelo menos provider, source (task.adapter.SourceName()), mode e error. Isso pode ser incorporado ao resumo agregado para evitar um log por tarefa e não aumentar excessivamente a cardinalidade.

Aproveitar o mesmo summary para informar também o max_concurrency_effective do provider, pois o override registrado pode ser maior que o limite efetivo quando a requisição reduz a concorrência global.

Essa correção é necessária para atender aos critérios da PAV-120 de identificar provider e tarefa nos erros, registrar erros/timeouts por provider e informar o comportamento efetivo do pipeline.

break schedule
}

go produceTasks(ctx, tasks, adapterList, req.Keywords, runStats)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O produtor é iniciado em uma goroutine, mas não faz parte do WaitGroup. Atualmente aguardamos somente os workers antes de fechar results e retornar. Em caso de cancelamento, o produtor tende a encerrar ao observar o contexto, mas não existe garantia explícita de que ele terminou antes do retorno de runWithConcurrency.

A PAV-120 exige encerramento de produtores e workers, ausência de goroutines abandonadas e espera de todas as goroutines antes do retorno. Por favor, inclua o produtor no mecanismo de sincronização — via WaitGroup, producerDone ou errgroup — garantindo que ele tenha terminado antes de a execução retornar.

Adicionar também um teste que cancele a execução enquanto a fila estiver cheia e comprove que:

o produtor termina;
os workers terminam;
nenhuma nova tarefa começa após o cancelamento;
Run retorna somente depois do encerramento dessas goroutines.

s.summary(task).Produced++
}

func (s *providerRunStats) recordCompleted(task adapterTask, err error, duration time.Duration) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A classificação considera cancelamento apenas quando err é compatível com context.Canceled. Quando a execução é cancelada com uma causa customizada, como runlock.ErrLost, o adapter pode retornar essa causa e o summary contabiliza a tarefa como erro comum, não como cancelamento.

Isso torna incorretos os logs operacionais justamente no cenário de perda do lock distribuído, que é um critério explícito da PAV-120.

Por favor, faça recordCompleted receber ou considerar o estado/cause do contexto e diferencie:

cancelamento comum;
deadline/timeout;
perda do lock;
erro real do provider.

Adicionar teste com context.WithCancelCause usando uma causa customizada e confirmar que a tarefa é contabilizada como cancelada e que o motivo do encerramento é preservado.

@hltav

Copy link
Copy Markdown
Collaborator

Os três pontos levantados no review foram atendidos de forma satisfatória. A implementação agora aguarda produtor e workers, possui cobertura de lifecycle com goleak, classifica causas customizadas e registra erros estruturados com limite efetivo por provider.

Restou apenas uma melhoria não bloqueante na precedência de context.Cause(ctx) em outcomeReason, que pode fazer o summary registrar context canceled em vez da causa específica da perda do lock. Como isso não altera o comportamento funcional do pipeline, será registrado como débito técnico.

Testes, race detector e CI passaram. Aprovado.

@hltav
hltav self-requested a review August 28, 2026 13:21
@hltav
hltav merged commit e1de6de into developAug 28, 2026
1 check passed
@github-project-automationgithub-project-automationBot moved this from Backlog to Done in JobAtlas – KanbanAug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@RuhanFreitas@hltav@Benevanio
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(scrapper): implementar concorrência por provider - #231

Merged
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider
Aug 28, 2026
Merged

feat(scrapper): implementar concorrência por provider#231
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider

Conversation

@RuhanFreitas

@RuhanFreitasRuhanFreitas commented Aug 25, 2026

Copy link
Copy Markdown
Member

Card

  • PAV-120

O que foi feito

  • Controla a concorrência intra-execução do scraper-go com dois tetos: global (SCRAPER_MAX_CONCURRENCY, padrão 12) e por provider (SCRAPER_PROVIDER_MAX_CONCURRENCY, padrão 2), com overrides opcionais em SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES no formato provider=limite. Config inválida, vazia, duplicada, desconhecida ou acima do teto global impede a inicialização.
  • Substitui o fan-out adapters × keywords por um scheduler round-robin com fila limitada (maxConcurrency * 2) e workers fixos. O orçamento é cancelável, o release é idempotente e o limite efetivo é min(provider, global). Providers com várias instâncias (Greenhouse/Lever) compartilham o mesmo limite.
  • Declara capacidades por fonte: IDs canônicos (linkedin, adzuna, themuse, gupy, inhire, jooble, greenhouse, lever) e modos batch (LinkedIn, Adzuna, Jooble, Gupy), catalog (The Muse, InHire, Greenhouse, Lever) e keyword para fontes ainda não migradas. Catálogos são buscados uma vez e agregam keywords, sem duplicar jobs.
  • Remove concorrência interna dos adapters (LinkedIn, Adzuna, Gupy, InHire, The Muse); loops passam a ser seriais e canceláveis. INHIRE_DETAILS_CONCURRENCY foi removida. Logs agregados: scraper concurrency budget no início da run e scraper provider execution summary por provider. A fórmula da cache key permanece inalterada.

Validação

  • go test ./... em scraper-go
  • go test -race ./... em scraper-go
  • Conferir .env.example, docker-compose.yml e SCRAPER.md com SCRAPER_PROVIDER_MAX_CONCURRENCY=2 e SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES=""
  • Confirmar que INHIRE_DETAILS_CONCURRENCY não aparece mais em env/Compose/docs
  • Subir o scraper e validar nos logs o budget no início da run e o summary por provider
  • Testar override inválido (provider desconhecido, limite 0, acima do global) e confirmar fail-fast na inicialização

Benevanio
Benevanio previously approved these changes Aug 26, 2026

@hltavhltav left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A implementação principal está bem estruturada e segue corretamente a direção da PAV-120: orçamento global e por provider, scheduler round-robin, fila limitada, backpressure, capacidades explícitas e remoção da concorrência interna dos adapters.

Antes do merge, precisamos concluir os critérios ainda não demonstrados:

aguardar explicitamente o produtor;
classificar corretamente cancelamentos com causa customizada;
preservar uma amostra estruturada dos erros e registrar limites efetivos;
completar os testes de cancelamento, liberação, lote e encerramento de goroutines;
estabilizar a suíte completa com race detector;
remover o comentário obsoleto do Adzuna.

Depois dessas correções e da execução bem-sucedida de go test ./... e go test -race ./..., a PR fica pronta para aprovação e merge, sem deixar esses pontos como débito técnico.

##Cobertura de Testes

A cobertura adicionada é boa, mas ainda faltam demonstrações explícitas de alguns critérios de aceite da PAV-120. Antes do merge, por favor, adicionar testes para:

limite global 1 executando múltiplas tarefas estritamente de forma sequencial;
permissões liberadas depois de sucesso, erro do adapter e cancelamento durante execução;
produtor e workers encerrados após sucesso, erro e cancelamento;
cancelamento enquanto o produtor está bloqueado por backpressure;
ausência de tarefas iniciadas depois do cancelamento;
cancelamento durante requisição HTTP em andamento;
cancelamento durante espera de retry;
cancelamento durante paginação;
limite do lote/slot respeitado por providers batch;
encerramento sem vazamento de goroutines.

A validação de goroutines pode ser feita com goleak ou por sincronização determinística com canais/WaitGroups, desde que o teste comprove que todas as goroutines criadas pela execução terminaram antes do retorno.

runStats.recordCompleted(task, err, time.Since(started))

metrics.ScrapeRunsTotal.WithLabelValues(source).Inc()
if err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precisamos preservar informações operacionais sobre a falha da tarefa. Atualmente o erro é contabilizado na métrica e no resumo agregado, mas seu conteúdo e a instância concreta do adapter são descartados. Com isso, o summary pode informar errors > 0, porém não permite identificar qual erro ocorreu nem qual fonte/instância falhou, especialmente em providers com vários adapters, como Greenhouse e Lever.

Antes do merge, por favor, registre uma amostra estruturada do erro por provider, incluindo pelo menos provider, source (task.adapter.SourceName()), mode e error. Isso pode ser incorporado ao resumo agregado para evitar um log por tarefa e não aumentar excessivamente a cardinalidade.

Aproveitar o mesmo summary para informar também o max_concurrency_effective do provider, pois o override registrado pode ser maior que o limite efetivo quando a requisição reduz a concorrência global.

Essa correção é necessária para atender aos critérios da PAV-120 de identificar provider e tarefa nos erros, registrar erros/timeouts por provider e informar o comportamento efetivo do pipeline.

break schedule
}

go produceTasks(ctx, tasks, adapterList, req.Keywords, runStats)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O produtor é iniciado em uma goroutine, mas não faz parte do WaitGroup. Atualmente aguardamos somente os workers antes de fechar results e retornar. Em caso de cancelamento, o produtor tende a encerrar ao observar o contexto, mas não existe garantia explícita de que ele terminou antes do retorno de runWithConcurrency.

A PAV-120 exige encerramento de produtores e workers, ausência de goroutines abandonadas e espera de todas as goroutines antes do retorno. Por favor, inclua o produtor no mecanismo de sincronização — via WaitGroup, producerDone ou errgroup — garantindo que ele tenha terminado antes de a execução retornar.

Adicionar também um teste que cancele a execução enquanto a fila estiver cheia e comprove que:

o produtor termina;
os workers terminam;
nenhuma nova tarefa começa após o cancelamento;
Run retorna somente depois do encerramento dessas goroutines.

s.summary(task).Produced++
}

func (s *providerRunStats) recordCompleted(task adapterTask, err error, duration time.Duration) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A classificação considera cancelamento apenas quando err é compatível com context.Canceled. Quando a execução é cancelada com uma causa customizada, como runlock.ErrLost, o adapter pode retornar essa causa e o summary contabiliza a tarefa como erro comum, não como cancelamento.

Isso torna incorretos os logs operacionais justamente no cenário de perda do lock distribuído, que é um critério explícito da PAV-120.

Por favor, faça recordCompleted receber ou considerar o estado/cause do contexto e diferencie:

cancelamento comum;
deadline/timeout;
perda do lock;
erro real do provider.

Adicionar teste com context.WithCancelCause usando uma causa customizada e confirmar que a tarefa é contabilizada como cancelada e que o motivo do encerramento é preservado.

@hltav

Copy link
Copy Markdown
Collaborator

Os três pontos levantados no review foram atendidos de forma satisfatória. A implementação agora aguarda produtor e workers, possui cobertura de lifecycle com goleak, classifica causas customizadas e registra erros estruturados com limite efetivo por provider.

Restou apenas uma melhoria não bloqueante na precedência de context.Cause(ctx) em outcomeReason, que pode fazer o summary registrar context canceled em vez da causa específica da perda do lock. Como isso não altera o comportamento funcional do pipeline, será registrado como débito técnico.

Testes, race detector e CI passaram. Aprovado.

@hltav
hltav self-requested a review August 28, 2026 13:21
@hltav
hltav merged commit e1de6de into developAug 28, 2026
1 check passed
@github-project-automationgithub-project-automationBot moved this from Backlog to Done in JobAtlas – KanbanAug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@RuhanFreitas@hltav@Benevanio
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(scrapper): implementar concorrência por provider - #231

Merged
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider
Aug 28, 2026
Merged

feat(scrapper): implementar concorrência por provider#231
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider

Conversation

@RuhanFreitas

@RuhanFreitasRuhanFreitas commented Aug 25, 2026

Copy link
Copy Markdown
Member

Card

  • PAV-120

O que foi feito

  • Controla a concorrência intra-execução do scraper-go com dois tetos: global (SCRAPER_MAX_CONCURRENCY, padrão 12) e por provider (SCRAPER_PROVIDER_MAX_CONCURRENCY, padrão 2), com overrides opcionais em SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES no formato provider=limite. Config inválida, vazia, duplicada, desconhecida ou acima do teto global impede a inicialização.
  • Substitui o fan-out adapters × keywords por um scheduler round-robin com fila limitada (maxConcurrency * 2) e workers fixos. O orçamento é cancelável, o release é idempotente e o limite efetivo é min(provider, global). Providers com várias instâncias (Greenhouse/Lever) compartilham o mesmo limite.
  • Declara capacidades por fonte: IDs canônicos (linkedin, adzuna, themuse, gupy, inhire, jooble, greenhouse, lever) e modos batch (LinkedIn, Adzuna, Jooble, Gupy), catalog (The Muse, InHire, Greenhouse, Lever) e keyword para fontes ainda não migradas. Catálogos são buscados uma vez e agregam keywords, sem duplicar jobs.
  • Remove concorrência interna dos adapters (LinkedIn, Adzuna, Gupy, InHire, The Muse); loops passam a ser seriais e canceláveis. INHIRE_DETAILS_CONCURRENCY foi removida. Logs agregados: scraper concurrency budget no início da run e scraper provider execution summary por provider. A fórmula da cache key permanece inalterada.

Validação

  • go test ./... em scraper-go
  • go test -race ./... em scraper-go
  • Conferir .env.example, docker-compose.yml e SCRAPER.md com SCRAPER_PROVIDER_MAX_CONCURRENCY=2 e SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES=""
  • Confirmar que INHIRE_DETAILS_CONCURRENCY não aparece mais em env/Compose/docs
  • Subir o scraper e validar nos logs o budget no início da run e o summary por provider
  • Testar override inválido (provider desconhecido, limite 0, acima do global) e confirmar fail-fast na inicialização

Benevanio
Benevanio previously approved these changes Aug 26, 2026

@hltavhltav left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A implementação principal está bem estruturada e segue corretamente a direção da PAV-120: orçamento global e por provider, scheduler round-robin, fila limitada, backpressure, capacidades explícitas e remoção da concorrência interna dos adapters.

Antes do merge, precisamos concluir os critérios ainda não demonstrados:

aguardar explicitamente o produtor;
classificar corretamente cancelamentos com causa customizada;
preservar uma amostra estruturada dos erros e registrar limites efetivos;
completar os testes de cancelamento, liberação, lote e encerramento de goroutines;
estabilizar a suíte completa com race detector;
remover o comentário obsoleto do Adzuna.

Depois dessas correções e da execução bem-sucedida de go test ./... e go test -race ./..., a PR fica pronta para aprovação e merge, sem deixar esses pontos como débito técnico.

##Cobertura de Testes

A cobertura adicionada é boa, mas ainda faltam demonstrações explícitas de alguns critérios de aceite da PAV-120. Antes do merge, por favor, adicionar testes para:

limite global 1 executando múltiplas tarefas estritamente de forma sequencial;
permissões liberadas depois de sucesso, erro do adapter e cancelamento durante execução;
produtor e workers encerrados após sucesso, erro e cancelamento;
cancelamento enquanto o produtor está bloqueado por backpressure;
ausência de tarefas iniciadas depois do cancelamento;
cancelamento durante requisição HTTP em andamento;
cancelamento durante espera de retry;
cancelamento durante paginação;
limite do lote/slot respeitado por providers batch;
encerramento sem vazamento de goroutines.

A validação de goroutines pode ser feita com goleak ou por sincronização determinística com canais/WaitGroups, desde que o teste comprove que todas as goroutines criadas pela execução terminaram antes do retorno.

runStats.recordCompleted(task, err, time.Since(started))

metrics.ScrapeRunsTotal.WithLabelValues(source).Inc()
if err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precisamos preservar informações operacionais sobre a falha da tarefa. Atualmente o erro é contabilizado na métrica e no resumo agregado, mas seu conteúdo e a instância concreta do adapter são descartados. Com isso, o summary pode informar errors > 0, porém não permite identificar qual erro ocorreu nem qual fonte/instância falhou, especialmente em providers com vários adapters, como Greenhouse e Lever.

Antes do merge, por favor, registre uma amostra estruturada do erro por provider, incluindo pelo menos provider, source (task.adapter.SourceName()), mode e error. Isso pode ser incorporado ao resumo agregado para evitar um log por tarefa e não aumentar excessivamente a cardinalidade.

Aproveitar o mesmo summary para informar também o max_concurrency_effective do provider, pois o override registrado pode ser maior que o limite efetivo quando a requisição reduz a concorrência global.

Essa correção é necessária para atender aos critérios da PAV-120 de identificar provider e tarefa nos erros, registrar erros/timeouts por provider e informar o comportamento efetivo do pipeline.

break schedule
}

go produceTasks(ctx, tasks, adapterList, req.Keywords, runStats)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O produtor é iniciado em uma goroutine, mas não faz parte do WaitGroup. Atualmente aguardamos somente os workers antes de fechar results e retornar. Em caso de cancelamento, o produtor tende a encerrar ao observar o contexto, mas não existe garantia explícita de que ele terminou antes do retorno de runWithConcurrency.

A PAV-120 exige encerramento de produtores e workers, ausência de goroutines abandonadas e espera de todas as goroutines antes do retorno. Por favor, inclua o produtor no mecanismo de sincronização — via WaitGroup, producerDone ou errgroup — garantindo que ele tenha terminado antes de a execução retornar.

Adicionar também um teste que cancele a execução enquanto a fila estiver cheia e comprove que:

o produtor termina;
os workers terminam;
nenhuma nova tarefa começa após o cancelamento;
Run retorna somente depois do encerramento dessas goroutines.

s.summary(task).Produced++
}

func (s *providerRunStats) recordCompleted(task adapterTask, err error, duration time.Duration) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A classificação considera cancelamento apenas quando err é compatível com context.Canceled. Quando a execução é cancelada com uma causa customizada, como runlock.ErrLost, o adapter pode retornar essa causa e o summary contabiliza a tarefa como erro comum, não como cancelamento.

Isso torna incorretos os logs operacionais justamente no cenário de perda do lock distribuído, que é um critério explícito da PAV-120.

Por favor, faça recordCompleted receber ou considerar o estado/cause do contexto e diferencie:

cancelamento comum;
deadline/timeout;
perda do lock;
erro real do provider.

Adicionar teste com context.WithCancelCause usando uma causa customizada e confirmar que a tarefa é contabilizada como cancelada e que o motivo do encerramento é preservado.

@hltav

Copy link
Copy Markdown
Collaborator

Os três pontos levantados no review foram atendidos de forma satisfatória. A implementação agora aguarda produtor e workers, possui cobertura de lifecycle com goleak, classifica causas customizadas e registra erros estruturados com limite efetivo por provider.

Restou apenas uma melhoria não bloqueante na precedência de context.Cause(ctx) em outcomeReason, que pode fazer o summary registrar context canceled em vez da causa específica da perda do lock. Como isso não altera o comportamento funcional do pipeline, será registrado como débito técnico.

Testes, race detector e CI passaram. Aprovado.

@hltav
hltav self-requested a review August 28, 2026 13:21
@hltav
hltav merged commit e1de6de into developAug 28, 2026
1 check passed
@github-project-automationgithub-project-automationBot moved this from Backlog to Done in JobAtlas – KanbanAug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@RuhanFreitas@hltav@Benevanio
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(scrapper): implementar concorrência por provider - #231

Merged
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider
Aug 28, 2026
Merged

feat(scrapper): implementar concorrência por provider#231
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider

Conversation

@RuhanFreitas

@RuhanFreitasRuhanFreitas commented Aug 25, 2026

Copy link
Copy Markdown
Member

Card

  • PAV-120

O que foi feito

  • Controla a concorrência intra-execução do scraper-go com dois tetos: global (SCRAPER_MAX_CONCURRENCY, padrão 12) e por provider (SCRAPER_PROVIDER_MAX_CONCURRENCY, padrão 2), com overrides opcionais em SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES no formato provider=limite. Config inválida, vazia, duplicada, desconhecida ou acima do teto global impede a inicialização.
  • Substitui o fan-out adapters × keywords por um scheduler round-robin com fila limitada (maxConcurrency * 2) e workers fixos. O orçamento é cancelável, o release é idempotente e o limite efetivo é min(provider, global). Providers com várias instâncias (Greenhouse/Lever) compartilham o mesmo limite.
  • Declara capacidades por fonte: IDs canônicos (linkedin, adzuna, themuse, gupy, inhire, jooble, greenhouse, lever) e modos batch (LinkedIn, Adzuna, Jooble, Gupy), catalog (The Muse, InHire, Greenhouse, Lever) e keyword para fontes ainda não migradas. Catálogos são buscados uma vez e agregam keywords, sem duplicar jobs.
  • Remove concorrência interna dos adapters (LinkedIn, Adzuna, Gupy, InHire, The Muse); loops passam a ser seriais e canceláveis. INHIRE_DETAILS_CONCURRENCY foi removida. Logs agregados: scraper concurrency budget no início da run e scraper provider execution summary por provider. A fórmula da cache key permanece inalterada.

Validação

  • go test ./... em scraper-go
  • go test -race ./... em scraper-go
  • Conferir .env.example, docker-compose.yml e SCRAPER.md com SCRAPER_PROVIDER_MAX_CONCURRENCY=2 e SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES=""
  • Confirmar que INHIRE_DETAILS_CONCURRENCY não aparece mais em env/Compose/docs
  • Subir o scraper e validar nos logs o budget no início da run e o summary por provider
  • Testar override inválido (provider desconhecido, limite 0, acima do global) e confirmar fail-fast na inicialização

Benevanio
Benevanio previously approved these changes Aug 26, 2026

@hltavhltav left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A implementação principal está bem estruturada e segue corretamente a direção da PAV-120: orçamento global e por provider, scheduler round-robin, fila limitada, backpressure, capacidades explícitas e remoção da concorrência interna dos adapters.

Antes do merge, precisamos concluir os critérios ainda não demonstrados:

aguardar explicitamente o produtor;
classificar corretamente cancelamentos com causa customizada;
preservar uma amostra estruturada dos erros e registrar limites efetivos;
completar os testes de cancelamento, liberação, lote e encerramento de goroutines;
estabilizar a suíte completa com race detector;
remover o comentário obsoleto do Adzuna.

Depois dessas correções e da execução bem-sucedida de go test ./... e go test -race ./..., a PR fica pronta para aprovação e merge, sem deixar esses pontos como débito técnico.

##Cobertura de Testes

A cobertura adicionada é boa, mas ainda faltam demonstrações explícitas de alguns critérios de aceite da PAV-120. Antes do merge, por favor, adicionar testes para:

limite global 1 executando múltiplas tarefas estritamente de forma sequencial;
permissões liberadas depois de sucesso, erro do adapter e cancelamento durante execução;
produtor e workers encerrados após sucesso, erro e cancelamento;
cancelamento enquanto o produtor está bloqueado por backpressure;
ausência de tarefas iniciadas depois do cancelamento;
cancelamento durante requisição HTTP em andamento;
cancelamento durante espera de retry;
cancelamento durante paginação;
limite do lote/slot respeitado por providers batch;
encerramento sem vazamento de goroutines.

A validação de goroutines pode ser feita com goleak ou por sincronização determinística com canais/WaitGroups, desde que o teste comprove que todas as goroutines criadas pela execução terminaram antes do retorno.

runStats.recordCompleted(task, err, time.Since(started))

metrics.ScrapeRunsTotal.WithLabelValues(source).Inc()
if err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precisamos preservar informações operacionais sobre a falha da tarefa. Atualmente o erro é contabilizado na métrica e no resumo agregado, mas seu conteúdo e a instância concreta do adapter são descartados. Com isso, o summary pode informar errors > 0, porém não permite identificar qual erro ocorreu nem qual fonte/instância falhou, especialmente em providers com vários adapters, como Greenhouse e Lever.

Antes do merge, por favor, registre uma amostra estruturada do erro por provider, incluindo pelo menos provider, source (task.adapter.SourceName()), mode e error. Isso pode ser incorporado ao resumo agregado para evitar um log por tarefa e não aumentar excessivamente a cardinalidade.

Aproveitar o mesmo summary para informar também o max_concurrency_effective do provider, pois o override registrado pode ser maior que o limite efetivo quando a requisição reduz a concorrência global.

Essa correção é necessária para atender aos critérios da PAV-120 de identificar provider e tarefa nos erros, registrar erros/timeouts por provider e informar o comportamento efetivo do pipeline.

break schedule
}

go produceTasks(ctx, tasks, adapterList, req.Keywords, runStats)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O produtor é iniciado em uma goroutine, mas não faz parte do WaitGroup. Atualmente aguardamos somente os workers antes de fechar results e retornar. Em caso de cancelamento, o produtor tende a encerrar ao observar o contexto, mas não existe garantia explícita de que ele terminou antes do retorno de runWithConcurrency.

A PAV-120 exige encerramento de produtores e workers, ausência de goroutines abandonadas e espera de todas as goroutines antes do retorno. Por favor, inclua o produtor no mecanismo de sincronização — via WaitGroup, producerDone ou errgroup — garantindo que ele tenha terminado antes de a execução retornar.

Adicionar também um teste que cancele a execução enquanto a fila estiver cheia e comprove que:

o produtor termina;
os workers terminam;
nenhuma nova tarefa começa após o cancelamento;
Run retorna somente depois do encerramento dessas goroutines.

s.summary(task).Produced++
}

func (s *providerRunStats) recordCompleted(task adapterTask, err error, duration time.Duration) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A classificação considera cancelamento apenas quando err é compatível com context.Canceled. Quando a execução é cancelada com uma causa customizada, como runlock.ErrLost, o adapter pode retornar essa causa e o summary contabiliza a tarefa como erro comum, não como cancelamento.

Isso torna incorretos os logs operacionais justamente no cenário de perda do lock distribuído, que é um critério explícito da PAV-120.

Por favor, faça recordCompleted receber ou considerar o estado/cause do contexto e diferencie:

cancelamento comum;
deadline/timeout;
perda do lock;
erro real do provider.

Adicionar teste com context.WithCancelCause usando uma causa customizada e confirmar que a tarefa é contabilizada como cancelada e que o motivo do encerramento é preservado.

@hltav

Copy link
Copy Markdown
Collaborator

Os três pontos levantados no review foram atendidos de forma satisfatória. A implementação agora aguarda produtor e workers, possui cobertura de lifecycle com goleak, classifica causas customizadas e registra erros estruturados com limite efetivo por provider.

Restou apenas uma melhoria não bloqueante na precedência de context.Cause(ctx) em outcomeReason, que pode fazer o summary registrar context canceled em vez da causa específica da perda do lock. Como isso não altera o comportamento funcional do pipeline, será registrado como débito técnico.

Testes, race detector e CI passaram. Aprovado.

@hltav
hltav self-requested a review August 28, 2026 13:21
@hltav
hltav merged commit e1de6de into developAug 28, 2026
1 check passed
@github-project-automationgithub-project-automationBot moved this from Backlog to Done in JobAtlas – KanbanAug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@RuhanFreitas@hltav@Benevanio
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(scrapper): implementar concorrência por provider - #231

Merged
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider
Aug 28, 2026
Merged

feat(scrapper): implementar concorrência por provider#231
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider

Conversation

@RuhanFreitas

@RuhanFreitasRuhanFreitas commented Aug 25, 2026

Copy link
Copy Markdown
Member

Card

  • PAV-120

O que foi feito

  • Controla a concorrência intra-execução do scraper-go com dois tetos: global (SCRAPER_MAX_CONCURRENCY, padrão 12) e por provider (SCRAPER_PROVIDER_MAX_CONCURRENCY, padrão 2), com overrides opcionais em SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES no formato provider=limite. Config inválida, vazia, duplicada, desconhecida ou acima do teto global impede a inicialização.
  • Substitui o fan-out adapters × keywords por um scheduler round-robin com fila limitada (maxConcurrency * 2) e workers fixos. O orçamento é cancelável, o release é idempotente e o limite efetivo é min(provider, global). Providers com várias instâncias (Greenhouse/Lever) compartilham o mesmo limite.
  • Declara capacidades por fonte: IDs canônicos (linkedin, adzuna, themuse, gupy, inhire, jooble, greenhouse, lever) e modos batch (LinkedIn, Adzuna, Jooble, Gupy), catalog (The Muse, InHire, Greenhouse, Lever) e keyword para fontes ainda não migradas. Catálogos são buscados uma vez e agregam keywords, sem duplicar jobs.
  • Remove concorrência interna dos adapters (LinkedIn, Adzuna, Gupy, InHire, The Muse); loops passam a ser seriais e canceláveis. INHIRE_DETAILS_CONCURRENCY foi removida. Logs agregados: scraper concurrency budget no início da run e scraper provider execution summary por provider. A fórmula da cache key permanece inalterada.

Validação

  • go test ./... em scraper-go
  • go test -race ./... em scraper-go
  • Conferir .env.example, docker-compose.yml e SCRAPER.md com SCRAPER_PROVIDER_MAX_CONCURRENCY=2 e SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES=""
  • Confirmar que INHIRE_DETAILS_CONCURRENCY não aparece mais em env/Compose/docs
  • Subir o scraper e validar nos logs o budget no início da run e o summary por provider
  • Testar override inválido (provider desconhecido, limite 0, acima do global) e confirmar fail-fast na inicialização

Benevanio
Benevanio previously approved these changes Aug 26, 2026

@hltavhltav left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A implementação principal está bem estruturada e segue corretamente a direção da PAV-120: orçamento global e por provider, scheduler round-robin, fila limitada, backpressure, capacidades explícitas e remoção da concorrência interna dos adapters.

Antes do merge, precisamos concluir os critérios ainda não demonstrados:

aguardar explicitamente o produtor;
classificar corretamente cancelamentos com causa customizada;
preservar uma amostra estruturada dos erros e registrar limites efetivos;
completar os testes de cancelamento, liberação, lote e encerramento de goroutines;
estabilizar a suíte completa com race detector;
remover o comentário obsoleto do Adzuna.

Depois dessas correções e da execução bem-sucedida de go test ./... e go test -race ./..., a PR fica pronta para aprovação e merge, sem deixar esses pontos como débito técnico.

##Cobertura de Testes

A cobertura adicionada é boa, mas ainda faltam demonstrações explícitas de alguns critérios de aceite da PAV-120. Antes do merge, por favor, adicionar testes para:

limite global 1 executando múltiplas tarefas estritamente de forma sequencial;
permissões liberadas depois de sucesso, erro do adapter e cancelamento durante execução;
produtor e workers encerrados após sucesso, erro e cancelamento;
cancelamento enquanto o produtor está bloqueado por backpressure;
ausência de tarefas iniciadas depois do cancelamento;
cancelamento durante requisição HTTP em andamento;
cancelamento durante espera de retry;
cancelamento durante paginação;
limite do lote/slot respeitado por providers batch;
encerramento sem vazamento de goroutines.

A validação de goroutines pode ser feita com goleak ou por sincronização determinística com canais/WaitGroups, desde que o teste comprove que todas as goroutines criadas pela execução terminaram antes do retorno.

runStats.recordCompleted(task, err, time.Since(started))

metrics.ScrapeRunsTotal.WithLabelValues(source).Inc()
if err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precisamos preservar informações operacionais sobre a falha da tarefa. Atualmente o erro é contabilizado na métrica e no resumo agregado, mas seu conteúdo e a instância concreta do adapter são descartados. Com isso, o summary pode informar errors > 0, porém não permite identificar qual erro ocorreu nem qual fonte/instância falhou, especialmente em providers com vários adapters, como Greenhouse e Lever.

Antes do merge, por favor, registre uma amostra estruturada do erro por provider, incluindo pelo menos provider, source (task.adapter.SourceName()), mode e error. Isso pode ser incorporado ao resumo agregado para evitar um log por tarefa e não aumentar excessivamente a cardinalidade.

Aproveitar o mesmo summary para informar também o max_concurrency_effective do provider, pois o override registrado pode ser maior que o limite efetivo quando a requisição reduz a concorrência global.

Essa correção é necessária para atender aos critérios da PAV-120 de identificar provider e tarefa nos erros, registrar erros/timeouts por provider e informar o comportamento efetivo do pipeline.

break schedule
}

go produceTasks(ctx, tasks, adapterList, req.Keywords, runStats)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O produtor é iniciado em uma goroutine, mas não faz parte do WaitGroup. Atualmente aguardamos somente os workers antes de fechar results e retornar. Em caso de cancelamento, o produtor tende a encerrar ao observar o contexto, mas não existe garantia explícita de que ele terminou antes do retorno de runWithConcurrency.

A PAV-120 exige encerramento de produtores e workers, ausência de goroutines abandonadas e espera de todas as goroutines antes do retorno. Por favor, inclua o produtor no mecanismo de sincronização — via WaitGroup, producerDone ou errgroup — garantindo que ele tenha terminado antes de a execução retornar.

Adicionar também um teste que cancele a execução enquanto a fila estiver cheia e comprove que:

o produtor termina;
os workers terminam;
nenhuma nova tarefa começa após o cancelamento;
Run retorna somente depois do encerramento dessas goroutines.

s.summary(task).Produced++
}

func (s *providerRunStats) recordCompleted(task adapterTask, err error, duration time.Duration) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A classificação considera cancelamento apenas quando err é compatível com context.Canceled. Quando a execução é cancelada com uma causa customizada, como runlock.ErrLost, o adapter pode retornar essa causa e o summary contabiliza a tarefa como erro comum, não como cancelamento.

Isso torna incorretos os logs operacionais justamente no cenário de perda do lock distribuído, que é um critério explícito da PAV-120.

Por favor, faça recordCompleted receber ou considerar o estado/cause do contexto e diferencie:

cancelamento comum;
deadline/timeout;
perda do lock;
erro real do provider.

Adicionar teste com context.WithCancelCause usando uma causa customizada e confirmar que a tarefa é contabilizada como cancelada e que o motivo do encerramento é preservado.

@hltav

Copy link
Copy Markdown
Collaborator

Os três pontos levantados no review foram atendidos de forma satisfatória. A implementação agora aguarda produtor e workers, possui cobertura de lifecycle com goleak, classifica causas customizadas e registra erros estruturados com limite efetivo por provider.

Restou apenas uma melhoria não bloqueante na precedência de context.Cause(ctx) em outcomeReason, que pode fazer o summary registrar context canceled em vez da causa específica da perda do lock. Como isso não altera o comportamento funcional do pipeline, será registrado como débito técnico.

Testes, race detector e CI passaram. Aprovado.

@hltav
hltav self-requested a review August 28, 2026 13:21
@hltav
hltav merged commit e1de6de into developAug 28, 2026
1 check passed
@github-project-automationgithub-project-automationBot moved this from Backlog to Done in JobAtlas – KanbanAug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@RuhanFreitas@hltav@Benevanio
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(scrapper): implementar concorrência por provider - #231

Merged
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider
Aug 28, 2026
Merged

feat(scrapper): implementar concorrência por provider#231
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider

Conversation

@RuhanFreitas

@RuhanFreitasRuhanFreitas commented Aug 25, 2026

Copy link
Copy Markdown
Member

Card

  • PAV-120

O que foi feito

  • Controla a concorrência intra-execução do scraper-go com dois tetos: global (SCRAPER_MAX_CONCURRENCY, padrão 12) e por provider (SCRAPER_PROVIDER_MAX_CONCURRENCY, padrão 2), com overrides opcionais em SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES no formato provider=limite. Config inválida, vazia, duplicada, desconhecida ou acima do teto global impede a inicialização.
  • Substitui o fan-out adapters × keywords por um scheduler round-robin com fila limitada (maxConcurrency * 2) e workers fixos. O orçamento é cancelável, o release é idempotente e o limite efetivo é min(provider, global). Providers com várias instâncias (Greenhouse/Lever) compartilham o mesmo limite.
  • Declara capacidades por fonte: IDs canônicos (linkedin, adzuna, themuse, gupy, inhire, jooble, greenhouse, lever) e modos batch (LinkedIn, Adzuna, Jooble, Gupy), catalog (The Muse, InHire, Greenhouse, Lever) e keyword para fontes ainda não migradas. Catálogos são buscados uma vez e agregam keywords, sem duplicar jobs.
  • Remove concorrência interna dos adapters (LinkedIn, Adzuna, Gupy, InHire, The Muse); loops passam a ser seriais e canceláveis. INHIRE_DETAILS_CONCURRENCY foi removida. Logs agregados: scraper concurrency budget no início da run e scraper provider execution summary por provider. A fórmula da cache key permanece inalterada.

Validação

  • go test ./... em scraper-go
  • go test -race ./... em scraper-go
  • Conferir .env.example, docker-compose.yml e SCRAPER.md com SCRAPER_PROVIDER_MAX_CONCURRENCY=2 e SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES=""
  • Confirmar que INHIRE_DETAILS_CONCURRENCY não aparece mais em env/Compose/docs
  • Subir o scraper e validar nos logs o budget no início da run e o summary por provider
  • Testar override inválido (provider desconhecido, limite 0, acima do global) e confirmar fail-fast na inicialização

Benevanio
Benevanio previously approved these changes Aug 26, 2026

@hltavhltav left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A implementação principal está bem estruturada e segue corretamente a direção da PAV-120: orçamento global e por provider, scheduler round-robin, fila limitada, backpressure, capacidades explícitas e remoção da concorrência interna dos adapters.

Antes do merge, precisamos concluir os critérios ainda não demonstrados:

aguardar explicitamente o produtor;
classificar corretamente cancelamentos com causa customizada;
preservar uma amostra estruturada dos erros e registrar limites efetivos;
completar os testes de cancelamento, liberação, lote e encerramento de goroutines;
estabilizar a suíte completa com race detector;
remover o comentário obsoleto do Adzuna.

Depois dessas correções e da execução bem-sucedida de go test ./... e go test -race ./..., a PR fica pronta para aprovação e merge, sem deixar esses pontos como débito técnico.

##Cobertura de Testes

A cobertura adicionada é boa, mas ainda faltam demonstrações explícitas de alguns critérios de aceite da PAV-120. Antes do merge, por favor, adicionar testes para:

limite global 1 executando múltiplas tarefas estritamente de forma sequencial;
permissões liberadas depois de sucesso, erro do adapter e cancelamento durante execução;
produtor e workers encerrados após sucesso, erro e cancelamento;
cancelamento enquanto o produtor está bloqueado por backpressure;
ausência de tarefas iniciadas depois do cancelamento;
cancelamento durante requisição HTTP em andamento;
cancelamento durante espera de retry;
cancelamento durante paginação;
limite do lote/slot respeitado por providers batch;
encerramento sem vazamento de goroutines.

A validação de goroutines pode ser feita com goleak ou por sincronização determinística com canais/WaitGroups, desde que o teste comprove que todas as goroutines criadas pela execução terminaram antes do retorno.

runStats.recordCompleted(task, err, time.Since(started))

metrics.ScrapeRunsTotal.WithLabelValues(source).Inc()
if err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precisamos preservar informações operacionais sobre a falha da tarefa. Atualmente o erro é contabilizado na métrica e no resumo agregado, mas seu conteúdo e a instância concreta do adapter são descartados. Com isso, o summary pode informar errors > 0, porém não permite identificar qual erro ocorreu nem qual fonte/instância falhou, especialmente em providers com vários adapters, como Greenhouse e Lever.

Antes do merge, por favor, registre uma amostra estruturada do erro por provider, incluindo pelo menos provider, source (task.adapter.SourceName()), mode e error. Isso pode ser incorporado ao resumo agregado para evitar um log por tarefa e não aumentar excessivamente a cardinalidade.

Aproveitar o mesmo summary para informar também o max_concurrency_effective do provider, pois o override registrado pode ser maior que o limite efetivo quando a requisição reduz a concorrência global.

Essa correção é necessária para atender aos critérios da PAV-120 de identificar provider e tarefa nos erros, registrar erros/timeouts por provider e informar o comportamento efetivo do pipeline.

break schedule
}

go produceTasks(ctx, tasks, adapterList, req.Keywords, runStats)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O produtor é iniciado em uma goroutine, mas não faz parte do WaitGroup. Atualmente aguardamos somente os workers antes de fechar results e retornar. Em caso de cancelamento, o produtor tende a encerrar ao observar o contexto, mas não existe garantia explícita de que ele terminou antes do retorno de runWithConcurrency.

A PAV-120 exige encerramento de produtores e workers, ausência de goroutines abandonadas e espera de todas as goroutines antes do retorno. Por favor, inclua o produtor no mecanismo de sincronização — via WaitGroup, producerDone ou errgroup — garantindo que ele tenha terminado antes de a execução retornar.

Adicionar também um teste que cancele a execução enquanto a fila estiver cheia e comprove que:

o produtor termina;
os workers terminam;
nenhuma nova tarefa começa após o cancelamento;
Run retorna somente depois do encerramento dessas goroutines.

s.summary(task).Produced++
}

func (s *providerRunStats) recordCompleted(task adapterTask, err error, duration time.Duration) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A classificação considera cancelamento apenas quando err é compatível com context.Canceled. Quando a execução é cancelada com uma causa customizada, como runlock.ErrLost, o adapter pode retornar essa causa e o summary contabiliza a tarefa como erro comum, não como cancelamento.

Isso torna incorretos os logs operacionais justamente no cenário de perda do lock distribuído, que é um critério explícito da PAV-120.

Por favor, faça recordCompleted receber ou considerar o estado/cause do contexto e diferencie:

cancelamento comum;
deadline/timeout;
perda do lock;
erro real do provider.

Adicionar teste com context.WithCancelCause usando uma causa customizada e confirmar que a tarefa é contabilizada como cancelada e que o motivo do encerramento é preservado.

@hltav

Copy link
Copy Markdown
Collaborator

Os três pontos levantados no review foram atendidos de forma satisfatória. A implementação agora aguarda produtor e workers, possui cobertura de lifecycle com goleak, classifica causas customizadas e registra erros estruturados com limite efetivo por provider.

Restou apenas uma melhoria não bloqueante na precedência de context.Cause(ctx) em outcomeReason, que pode fazer o summary registrar context canceled em vez da causa específica da perda do lock. Como isso não altera o comportamento funcional do pipeline, será registrado como débito técnico.

Testes, race detector e CI passaram. Aprovado.

@hltav
hltav self-requested a review August 28, 2026 13:21
@hltav
hltav merged commit e1de6de into developAug 28, 2026
1 check passed
@github-project-automationgithub-project-automationBot moved this from Backlog to Done in JobAtlas – KanbanAug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@RuhanFreitas@hltav@Benevanio
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(scrapper): implementar concorrência por provider - #231

Merged
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider
Aug 28, 2026
Merged

feat(scrapper): implementar concorrência por provider#231
hltav merged 2 commits into
developfrom
feature/pav-120-implementar-concorrencia-por-provider

Conversation

@RuhanFreitas

@RuhanFreitasRuhanFreitas commented Aug 25, 2026

Copy link
Copy Markdown
Member

Card

  • PAV-120

O que foi feito

  • Controla a concorrência intra-execução do scraper-go com dois tetos: global (SCRAPER_MAX_CONCURRENCY, padrão 12) e por provider (SCRAPER_PROVIDER_MAX_CONCURRENCY, padrão 2), com overrides opcionais em SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES no formato provider=limite. Config inválida, vazia, duplicada, desconhecida ou acima do teto global impede a inicialização.
  • Substitui o fan-out adapters × keywords por um scheduler round-robin com fila limitada (maxConcurrency * 2) e workers fixos. O orçamento é cancelável, o release é idempotente e o limite efetivo é min(provider, global). Providers com várias instâncias (Greenhouse/Lever) compartilham o mesmo limite.
  • Declara capacidades por fonte: IDs canônicos (linkedin, adzuna, themuse, gupy, inhire, jooble, greenhouse, lever) e modos batch (LinkedIn, Adzuna, Jooble, Gupy), catalog (The Muse, InHire, Greenhouse, Lever) e keyword para fontes ainda não migradas. Catálogos são buscados uma vez e agregam keywords, sem duplicar jobs.
  • Remove concorrência interna dos adapters (LinkedIn, Adzuna, Gupy, InHire, The Muse); loops passam a ser seriais e canceláveis. INHIRE_DETAILS_CONCURRENCY foi removida. Logs agregados: scraper concurrency budget no início da run e scraper provider execution summary por provider. A fórmula da cache key permanece inalterada.

Validação

  • go test ./... em scraper-go
  • go test -race ./... em scraper-go
  • Conferir .env.example, docker-compose.yml e SCRAPER.md com SCRAPER_PROVIDER_MAX_CONCURRENCY=2 e SCRAPER_PROVIDER_CONCURRENCY_OVERRIDES=""
  • Confirmar que INHIRE_DETAILS_CONCURRENCY não aparece mais em env/Compose/docs
  • Subir o scraper e validar nos logs o budget no início da run e o summary por provider
  • Testar override inválido (provider desconhecido, limite 0, acima do global) e confirmar fail-fast na inicialização

Benevanio
Benevanio previously approved these changes Aug 26, 2026

@hltavhltav left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A implementação principal está bem estruturada e segue corretamente a direção da PAV-120: orçamento global e por provider, scheduler round-robin, fila limitada, backpressure, capacidades explícitas e remoção da concorrência interna dos adapters.

Antes do merge, precisamos concluir os critérios ainda não demonstrados:

aguardar explicitamente o produtor;
classificar corretamente cancelamentos com causa customizada;
preservar uma amostra estruturada dos erros e registrar limites efetivos;
completar os testes de cancelamento, liberação, lote e encerramento de goroutines;
estabilizar a suíte completa com race detector;
remover o comentário obsoleto do Adzuna.

Depois dessas correções e da execução bem-sucedida de go test ./... e go test -race ./..., a PR fica pronta para aprovação e merge, sem deixar esses pontos como débito técnico.

##Cobertura de Testes

A cobertura adicionada é boa, mas ainda faltam demonstrações explícitas de alguns critérios de aceite da PAV-120. Antes do merge, por favor, adicionar testes para:

limite global 1 executando múltiplas tarefas estritamente de forma sequencial;
permissões liberadas depois de sucesso, erro do adapter e cancelamento durante execução;
produtor e workers encerrados após sucesso, erro e cancelamento;
cancelamento enquanto o produtor está bloqueado por backpressure;
ausência de tarefas iniciadas depois do cancelamento;
cancelamento durante requisição HTTP em andamento;
cancelamento durante espera de retry;
cancelamento durante paginação;
limite do lote/slot respeitado por providers batch;
encerramento sem vazamento de goroutines.

A validação de goroutines pode ser feita com goleak ou por sincronização determinística com canais/WaitGroups, desde que o teste comprove que todas as goroutines criadas pela execução terminaram antes do retorno.

runStats.recordCompleted(task, err, time.Since(started))

metrics.ScrapeRunsTotal.WithLabelValues(source).Inc()
if err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precisamos preservar informações operacionais sobre a falha da tarefa. Atualmente o erro é contabilizado na métrica e no resumo agregado, mas seu conteúdo e a instância concreta do adapter são descartados. Com isso, o summary pode informar errors > 0, porém não permite identificar qual erro ocorreu nem qual fonte/instância falhou, especialmente em providers com vários adapters, como Greenhouse e Lever.

Antes do merge, por favor, registre uma amostra estruturada do erro por provider, incluindo pelo menos provider, source (task.adapter.SourceName()), mode e error. Isso pode ser incorporado ao resumo agregado para evitar um log por tarefa e não aumentar excessivamente a cardinalidade.

Aproveitar o mesmo summary para informar também o max_concurrency_effective do provider, pois o override registrado pode ser maior que o limite efetivo quando a requisição reduz a concorrência global.

Essa correção é necessária para atender aos critérios da PAV-120 de identificar provider e tarefa nos erros, registrar erros/timeouts por provider e informar o comportamento efetivo do pipeline.

break schedule
}

go produceTasks(ctx, tasks, adapterList, req.Keywords, runStats)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O produtor é iniciado em uma goroutine, mas não faz parte do WaitGroup. Atualmente aguardamos somente os workers antes de fechar results e retornar. Em caso de cancelamento, o produtor tende a encerrar ao observar o contexto, mas não existe garantia explícita de que ele terminou antes do retorno de runWithConcurrency.

A PAV-120 exige encerramento de produtores e workers, ausência de goroutines abandonadas e espera de todas as goroutines antes do retorno. Por favor, inclua o produtor no mecanismo de sincronização — via WaitGroup, producerDone ou errgroup — garantindo que ele tenha terminado antes de a execução retornar.

Adicionar também um teste que cancele a execução enquanto a fila estiver cheia e comprove que:

o produtor termina;
os workers terminam;
nenhuma nova tarefa começa após o cancelamento;
Run retorna somente depois do encerramento dessas goroutines.

s.summary(task).Produced++
}

func (s *providerRunStats) recordCompleted(task adapterTask, err error, duration time.Duration) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A classificação considera cancelamento apenas quando err é compatível com context.Canceled. Quando a execução é cancelada com uma causa customizada, como runlock.ErrLost, o adapter pode retornar essa causa e o summary contabiliza a tarefa como erro comum, não como cancelamento.

Isso torna incorretos os logs operacionais justamente no cenário de perda do lock distribuído, que é um critério explícito da PAV-120.

Por favor, faça recordCompleted receber ou considerar o estado/cause do contexto e diferencie:

cancelamento comum;
deadline/timeout;
perda do lock;
erro real do provider.

Adicionar teste com context.WithCancelCause usando uma causa customizada e confirmar que a tarefa é contabilizada como cancelada e que o motivo do encerramento é preservado.

@hltav

Copy link
Copy Markdown
Collaborator

Os três pontos levantados no review foram atendidos de forma satisfatória. A implementação agora aguarda produtor e workers, possui cobertura de lifecycle com goleak, classifica causas customizadas e registra erros estruturados com limite efetivo por provider.

Restou apenas uma melhoria não bloqueante na precedência de context.Cause(ctx) em outcomeReason, que pode fazer o summary registrar context canceled em vez da causa específica da perda do lock. Como isso não altera o comportamento funcional do pipeline, será registrado como débito técnico.

Testes, race detector e CI passaram. Aprovado.

@hltav
hltav self-requested a review August 28, 2026 13:21
@hltav
hltav merged commit e1de6de into developAug 28, 2026
1 check passed
@github-project-automationgithub-project-automationBot moved this from Backlog to Done in JobAtlas – KanbanAug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@RuhanFreitas@hltav@Benevanio