You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reiniciar diretamente pela linha de comando um container gerenciado pelo Ruscker pode causar upstream error e deixar o container fora do registro de réplicas, embora ele volte a executar normalmente no Docker.
Exemplo:
docker restart <container-id-ou-nome>
Este cenário é relacionado à #1015, mas não é igual a docker rm -f: docker restart preserva o container, seu ID, labels e normalmente o binding de porta. O problema aqui é uma corrida entre a janela de parada/reinicialização e o tick do scaler.
Comportamentos possíveis
Restart termina entre dois ticks
Se o container parar e voltar antes da próxima reconciliação, a réplica permanece Ready no registro em memória. Durante a parada e o boot da aplicação, HTTP e WebSocket podem receber 502 upstream error, pois o Ruscker continua roteando para o upstream sem revalidar readiness.
Depois que a aplicação volta a escutar, o tráfego tende a se recuperar sozinho.
Scaler observa o container parado
Se o tick do scaler acontecer enquanto o container está parado/restarting:
list_managed_containers() encontra o container com running == false;
stopped_replica_ids() o classifica como morto;
o scaler remove a réplica do ReplicaRegistry;
as sessões da réplica são apagadas com sessions.drop_replica(...);
o Docker conclui o restart e volta a executar o mesmo container.
Nesse ponto o container está running e mantém os labels Ruscker, mas não volta para o registro em memória.
Análise da causa
A reconciliação em runtime é unidirecional:
o scaler consulta list_managed_containers() para remover réplicas observadas como stopped;
ContainerBackend::list() é usado para adotar containers etiquetados somente durante a inicialização do processo Ruscker;
não existe merge/readopção periódica de containers que voltaram a executar.
Trecho do liveness reconcile:
ifletOk(managed) = backend.list_managed_containers().await{let dead = stopped_replica_ids(®istry_snap,&managed);// remove do registry + drop_replica}
host multi-Docker indisponível: classificar como Unknown, sem podar ou duplicar.
O merge deve preservar contadores de sessão de réplicas que permaneceram vivas e nunca ultrapassar max-replicas considerando também containers readotáveis descobertos no backend.
2. Modelar restart como estado transitório
Não tratar imediatamente todo Stopped/Restarting como remoção definitiva. Usar uma janela de graça ou eventos Docker:
die/stop/restart: retirar temporariamente a réplica do roteamento;
start: inspecionar novamente o binding, executar readiness e devolver a réplica ao pool;
destroy: remoção definitiva e limpeza de sessões;
timeout sem retorno: podar e permitir substituição.
A reconciliação periódica deve continuar como fallback para eventos perdidos.
3. Readiness após restart externo
Um container que voltou para estado running não deve ser marcado imediatamente como Ready. Reutilizar a verificação de readiness usada no spawn antes de liberar HTTP/WebSocket para esse upstream.
4. Recuperação reativa no proxy
Em falha de transporte, confirmar o estado do container e acionar a mesma reconciliação. Replay automático somente para requisições seguramente reproduzíveis (GET/HEAD); nunca repetir cegamente métodos não idempotentes.
Critérios de aceite
docker restart do container não exige restart pela interface Ruscker.
O mesmo container, ao voltar, é readotado e fica Ready somente após readiness.
Há exatamente uma réplica registrada e um container gerenciado após o restart.
Nenhum container reiniciado fica órfão.
O scaler não cria uma substituta duplicada durante uma janela normal de restart.
Sessões antigas são invalidadas de forma consistente e um novo sticky binding pode ser criado.
Falha/timeout de um host em multi-host não remove nem duplica suas réplicas.
Restart prolongado além da janela de graça converge para poda + substituição.
Logs distinguem restart detected, replica readopted e restart timed out.
Testes sugeridos
Unitário: sequência Running → Stopped → Running com o mesmo ID termina com uma única réplica readotada.
Unitário: Stopped que não retorna até o timeout é podado e substituído.
Unitário multi-host: host Unknown não causa poda nem spawn duplicado.
Proxy: durante restart, upstream indisponível não permanece preso a uma réplica fantasma.
docker-it: spawn, docker restart <container-id>, aguardar readiness e verificar que o mesmo container voltou ao pool sem duplicata.
docker-it: restart suficientemente longo para atravessar um tick do scaler e validar a corrida real.
Verificação realizada
Busca de issues abertas: nenhuma equivalente encontrada.
Teste existente liveness_reconcile_prunes_only_stopped_containers: passa, mas documenta apenas a poda de stopped e não cobre readopção após restart.
Relato
Reiniciar diretamente pela linha de comando um container gerenciado pelo Ruscker pode causar
upstream errore deixar o container fora do registro de réplicas, embora ele volte a executar normalmente no Docker.Exemplo:
Este cenário é relacionado à #1015, mas não é igual a
docker rm -f:docker restartpreserva o container, seu ID, labels e normalmente o binding de porta. O problema aqui é uma corrida entre a janela de parada/reinicialização e o tick do scaler.Comportamentos possíveis
Restart termina entre dois ticks
Se o container parar e voltar antes da próxima reconciliação, a réplica permanece
Readyno registro em memória. Durante a parada e o boot da aplicação, HTTP e WebSocket podem receber502 upstream error, pois o Ruscker continua roteando para o upstream sem revalidar readiness.Depois que a aplicação volta a escutar, o tráfego tende a se recuperar sozinho.
Scaler observa o container parado
Se o tick do scaler acontecer enquanto o container está parado/restarting:
list_managed_containers()encontra o container comrunning == false;stopped_replica_ids()o classifica como morto;ReplicaRegistry;sessions.drop_replica(...);Nesse ponto o container está running e mantém os labels Ruscker, mas não volta para o registro em memória.
Análise da causa
A reconciliação em runtime é unidirecional:
list_managed_containers()para remover réplicas observadas como stopped;ContainerBackend::list()é usado para adotar containers etiquetados somente durante a inicialização do processo Ruscker;Trecho do liveness reconcile:
Readopção, apenas no startup:
O teste atual confirma apenas a poda
runningversusstopped; não cobre a sequência do mesmo container:Impacto
min-replicas > 0pode criar uma nova réplica, deixando o container reiniciado órfão.max-replicas, pois o cap consulta apenas o registro em memória.Por que o restart da interface funciona
O botão de restart do dashboard não executa o equivalente a
docker restart. Ele:spawn_replica().Esse fluxo é coordenado com o estado interno e, portanto, não sofre a corrida descrita aqui.
Proposta de solução
1. Reconciliação bidirecional em runtime
Em cada ciclo de liveness, reconciliar o snapshot do backend com o registro em memória, sem usar
resetdestrutivo:Unknown, sem podar ou duplicar.O merge deve preservar contadores de sessão de réplicas que permaneceram vivas e nunca ultrapassar
max-replicasconsiderando também containers readotáveis descobertos no backend.2. Modelar restart como estado transitório
Não tratar imediatamente todo
Stopped/Restartingcomo remoção definitiva. Usar uma janela de graça ou eventos Docker:die/stop/restart: retirar temporariamente a réplica do roteamento;start: inspecionar novamente o binding, executar readiness e devolver a réplica ao pool;destroy: remoção definitiva e limpeza de sessões;A reconciliação periódica deve continuar como fallback para eventos perdidos.
3. Readiness após restart externo
Um container que voltou para estado running não deve ser marcado imediatamente como
Ready. Reutilizar a verificação de readiness usada no spawn antes de liberar HTTP/WebSocket para esse upstream.4. Recuperação reativa no proxy
Em falha de transporte, confirmar o estado do container e acionar a mesma reconciliação. Replay automático somente para requisições seguramente reproduzíveis (
GET/HEAD); nunca repetir cegamente métodos não idempotentes.Critérios de aceite
docker restartdo container não exige restart pela interface Ruscker.Readysomente após readiness.restart detected,replica readoptederestart timed out.Testes sugeridos
Running → Stopped → Runningcom o mesmo ID termina com uma única réplica readotada.Stoppedque não retorna até o timeout é podado e substituído.Unknownnão causa poda nem spawn duplicado.docker-it: spawn,docker restart <container-id>, aguardar readiness e verificar que o mesmo container voltou ao pool sem duplicata.docker-it: restart suficientemente longo para atravessar um tick do scaler e validar a corrida real.Verificação realizada
liveness_reconcile_prunes_only_stopped_containers: passa, mas documenta apenas a poda de stopped e não cobre readopção após restart.