2.5 KiB
Solução: Debounce Dinâmico + Trava de Concorrência da IA
Objetivo
Resolver chamadas duplicadas para o LLM e estabilizar o agrupamento de mensagens (debounce) não-bloqueante na geração de respostas da IA.
Contexto
O Chatwoot apresentava falhas de agrupamento quando as mensagens chegavam muito perto do final do debounce. O uso do sleep 10s direto no Worker amarrava as threads do sistema. E quando uma "condição de corrida" acontecia (uma MSG2 enviava o Job enquanto a MSG1 já estava conectada no OpenAI), o sistema gerava 2 respostas para a mesma conversa.
Passos Implementados
-
Agendamento no Sidekiq (Debounce Dinâmico)
- Removemos o
sleep(10)da Thread. - Usamos o
perform_later(wait: X)dentro doHookExecutionService.schedule_captain_response. - Se uma MSG2 cai segundos depois, ela agenda o Job mais para a frente. Quando o Job1 acorda no passado, o método
debounce_requested?nota que não é a mais recente e aborta (Early Return), delegando a responsabilidade para a MSG2 agendada.
- Removemos o
-
Trava Distribuída (Mutex via
Rails.cache)- Injetamos um "Cadeado" atômico que só permite a IA responder a uma conversa por vez.
- Antes de iniciar a digitação no Chat/WhatsApp e chamar o OpenAI, o Job trava a chave
captain_response_lock_IDpor60.seconds. - Se outro Job (como o da MSG2) passar pelo relógio enquanto a primeira requisição anda na rede, ele baterá na trava e descartará rodar a IA 2 vezes, garantindo segurança de processamento.
- O
ensureno final apaga a trava obrigatoriamente independente do desfecho do código.
Principais Arquivos Alterados
enterprise/app/services/enterprise/message_templates/hook_execution_service.rbenterprise/app/jobs/captain/conversation/response_builder_job.rb
Callbacks ou APIs Utilizadas
Rails.cache.write(unless_exist: true): Cache atômico que serve de Mutex.Sidekiq::Worker.set(wait: X): ActiveJob Delay Queue.
Como Validar
Enviar N mensagens em uma mesma conversa pelo Whatsapp/Inbox em um curto período (Ex: Oi / Tudo bem? / Quanto custa?) dentro da janela de typing_delay de 10 segundos.
Apenas UMA requisição (a mais demorada) processará a IA após 10 segundos agrupando todo o histórico de uma vez, mantendo uma única e coesa mensagem final.
Como Reverter
- No
response_builder_job.rb, remover o bloco envolto comlock_key = "captain_response_loc..."re-adicionandosleephardcoded. - E no
HookExecutionServicevoltar a usar o.perform_laterdireto (sem.set(wait)).