Nunit: Travamento reproduzível sem código de usuário ao encerrar threads

Criado em 28 jun. 2019  ·  45Comentários  ·  Fonte: nunit/nunit

Finalmente estou resolvendo um problema de travamento de longa data com nossas compilações de CI no trabalho. O que me faz ter certeza de que este é um bug do NUnit é que sempre que os testes são interrompidos e eu anexei um depurador, não há código de usuário na pilha de chamadas de qualquer thread. Sempre há entre 5 e 7 threads de trabalho, bloqueados na seguinte chamada:

mscorlib.dll!System.Threading.ManualResetEventSlim.Wait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken)
nunit.framework.dll!NUnit.Framework.Internal.Execution.WorkItemQueue.Dequeue()
nunit.framework.dll!NUnit.Framework.Internal.Execution.TestWorker.TestWorkerThreadProc()
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)
mscorlib.dll!System.Threading.ThreadHelper.ThreadStart()
[Native to Managed Transition]

O fato de que sempre há menos de 9 threads parece indicar que o NUnit está no processo de desligamento dos oito normais mais um thread de STA toda vez que isso acontece. Pelo menos dois threads de trabalho foram encerrados em todos os casos que vi até agora.

Tenho um projeto que reproduz o problema em cerca de 2% das vezes. Ainda são 39 testes. Estou tentando remover coisas.

done bug

Todos 45 comentários

Eu realmente espero que isso não seja ambiental. Eu estava fazendo um progresso constante removendo coisas sem perder a reprodução, mas agora não consigo mais fazer a reprodução, mesmo se eu construir commits mais antigos ao longo do caminho que costumava ser reproduzido.

Ok, não é que eu esteja perdendo a reprodução, felizmente. Está ficando cada vez mais raro. Corri o último ciclo durante a noite e vejo esta manhã que levou uma hora de corridas consecutivas para reproduzir.

Ainda removendo pequenas partes dos 39 testes restantes, mas é difícil fazer muito sem perder a reprodução. Verificar se ainda o tenho leva de alguns segundos a não muito mais do que uma hora no máximo. Mas uma hora é muito tempo para esperar por algum feedback sobre o que acabei de experimentar.

Todos os meus palpites em como aumentar a probabilidade do jeito não garimpou para fora, tampouco, mas se @CharliePoole ou @oznetmaster ou qualquer outra pessoa tem uma teoria sobre o que poderia fazer o jeito mais provável, eu estaria animado para experimentá-lo.

Como o travamento está em WorkItemQueue.Dequeue, pensei em começar a procurar lá. As três coisas que vejo são:

  1. https://github.com/nunit/nunit/blob/a56e6859f3609128128ef87b0226472eb63ea5d4/src/NUnitFramework/framework/Internal/Execution/WorkItemQueue.cs#L358 -L365

  2. vou demorar um pouco para entender por que ele salva e restaura filas.

  3. partes móveis suficientes dentro do WorkItemQueue (para não mencionar fora dele) que é um pouco além da minha cabeça para descobrir se há um problema de segurança de thread.

Veja o tópico separado sobre por que eu acho que isso está pronto para ser reescrito. :piscadela:

Essas linhas no loop Dequeue podem entrar em leituras obsoletas em cada iteração, uma vez que não estão usando Volatile.Read ( volatile para .NET 3.5)?

https://github.com/nunit/nunit/blob/a56e6859f3609128128ef87b0226472eb63ea5d4/src/NUnitFramework/framework/Internal/Execution/WorkItemQueue.cs#L253 -L254

@CharliePoole Não tenho certeza se sou a melhor pessoa para reescrevê-lo, e vi o comentário de Rob sobre começar do zero ser assustador, com o qual tendo a concordar. Eu gosto da sua sugestão de mudança de recurso. No entanto, você estava falando sobre reescrever o despachante. Seria possível reescrever a classe WorkItemQueue sem reescrever mais nada?

Vale a pena tentar. No entanto, estou em um ponto se acredito que é hora de nos certificarmos de que temos os testes adequados e começar a reimplementar o despachante.

Certo. Isso tem a vantagem de já ter sido bastante testado. Você pode querer adicionar mais alguns.

IMO, há muito valor. Estou descobrindo maneiras de fazer as coisas assustadoras sem prejudicar a produção.

Mais informações: executei um commit mais antigo da minha reprodução com mais testes e a reprodução aconteceu na segunda execução. Desta vez, apenas um trabalhador paralelo foi encerrado, mas todos os demais estão presos, esperando como de costume. Peguei um rastreamento interno e tenho um depurador anexado.

A fila paralela e a fila STA não paralela estão vazias, têm o estado Running e têm addId e removeId iguais. (Ambos 47 para a fila paralela e 77 para a fila STA.)

Três dos trabalhadores de teste têm um item de trabalho denominado OneTimeTearDown, apesar de eu não ter desmontagens únicas e o estado do item de trabalho é Pronto. Os cinco restantes (incluindo o trabalhador do STA) têm um item de trabalho com um nome de teste e o estado Concluído.

Não posso ler os logs de rastreamento internos até encerrar o processo.

Meu log ruim e errado. De qualquer forma, das quatro execuções, as duas que reproduziram o travamento têm logs que terminam com:

11:40:33.823 Debug [17] Dispatcher: Using Direct strategy for BindingSourceMustHaveMatchingViewModelType OneTimeTearDown
11:40:33.823 Debug [17] Dispatcher: Using Parallel strategy for TestBindingSourceTypes OneTimeTearDown
11:40:33.823 Info  [14] TestWorker: ParallelWorker#6 executing TestBindingSourceTypes OneTimeTearDown
11:40:33.823 Debug [14] Dispatcher: Using Direct strategy for Views OneTimeTearDown
11:58:44.773 Debug [17] Dispatcher: Using Direct strategy for BindingSourceMustHaveMatchingViewModelType OneTimeTearDown
11:58:44.773 Debug [17] Dispatcher: Using Parallel strategy for TestBindingSourceTypes OneTimeTearDown
11:58:44.773 Info  [12] TestWorker: ParallelWorker#4 executing TestBindingSourceTypes OneTimeTearDown
11:58:44.773 Debug [12] Dispatcher: Using Direct strategy for Views OneTimeTearDown

Para efeito de comparação, aqui está a aparência dos traços não suspensos exatamente no mesmo ponto:

11:58:35.847 Debug [17] Dispatcher: Using Direct strategy for BindingSourceMustHaveMatchingViewModelType OneTimeTearDown
11:58:35.847 Debug [17] Dispatcher: Using Parallel strategy for TestBindingSourceTypes OneTimeTearDown
11:58:35.847 Info  [14] TestWorker: ParallelWorker#6 executing TestBindingSourceTypes OneTimeTearDown
11:58:35.847 Debug [14] Dispatcher: Using Direct strategy for Views OneTimeTearDown
11:58:35.847 Debug [14] Dispatcher: Using Direct strategy for Tests OneTimeTearDown
11:58:35.847 Debug [14] Dispatcher: Using Direct strategy for [Redacted] OneTimeTearDown
11:58:35.847 Debug [14] Dispatcher: Using Direct strategy for Techsola OneTimeTearDown
11:58:35.847 Debug [14] Dispatcher: Using Direct strategy for [Redacted].Tests.dll OneTimeTearDown
11:58:35.851 Info  [14] WorkShift: Parallel shift ending
11:58:35.851 Debug [14] WorkItemQueue: ParallelQueue.0 pausing
11:58:35.851 Debug [14] WorkItemQueue: ParallelSTAQueue.0 pausing
11:58:35.852 Info  [14] WorkItemQueue: ParallelQueue.0 stopping - 48 WorkItems processed
11:58:35.852 Info  [14] WorkItemQueue: ParallelSTAQueue.0 stopping - 77 WorkItems processed
11:58:35.852 Info  [11] TestWorker: ParallelWorker#3 stopping - 12 WorkItems processed.
11:58:35.852 Info  [10] TestWorker: ParallelWorker#2 stopping - 12 WorkItems processed.
11:58:35.852 Info  [14] WorkItemQueue: NonParallelQueue.0 stopping - 0 WorkItems processed
11:58:35.852 Info  [ 9] TestWorker: ParallelWorker#1 stopping - 4 WorkItems processed.
11:58:35.852 Info  [16] TestWorker: ParallelWorker#8 stopping - 3 WorkItems processed.
11:58:35.852 Info  [13] TestWorker: ParallelWorker#5 stopping - 2 WorkItems processed.
11:58:35.852 Info  [14] WorkItemQueue: NonParallelSTAQueue.0 stopping - 0 WorkItems processed
11:58:35.853 Info  [15] TestWorker: ParallelWorker#7 stopping - 3 WorkItems processed.
11:58:35.853 Info  [12] TestWorker: ParallelWorker#4 stopping - 3 WorkItems processed.
11:58:35.853 Info  [14] TestWorker: ParallelWorker#6 stopping - 9 WorkItems processed.
11:58:35.852 Info  [17] TestWorker: ParallelSTAWorker stopping - 77 WorkItems processed.

De alguma forma, é evidente que três itens de trabalho OneTimeTearDown estão sendo retirados da fila por três trabalhadores de teste. Os três trabalhadores de teste sentam-se com cada item de trabalho em seus _currentWorkItem . Sem concluir seus _currentWorkItem (o estado de cada item de trabalho ainda é Pronto), todos os três trabalhadores de teste chamam o Desenfileiramento novamente. As filas estão todas vazias, mas ainda em execução, portanto, Desenfileirar nunca mais retorna.

Eu não estou vendo isso. Há apenas um TestWorker ativo no trecho de código que termina. O thread 17, que é de propriedade do trabalhador STA Paralelo, executa dois desmontagens únicas e, em seguida, o encadeamento 14 executa a desmontagem única para o último acessório restante, bem como para cada namespace delimitador e dll em si. Essas desmontagens únicas provavelmente não estão fazendo nada, exceto sinalizar a conclusão.

Em seguida, o thread 14 desliga o turno, fazendo com que todas as filas e trabalhadores parem. A linha 17 é a última a ser interrompida. Em sua situação de travamento, acho que algo está acontecendo na desmontagem única para Views há um SetUpFixture?

Eu acabei de notar enquanto cutucava outro travamento que os itens de trabalho OneTimeTearDown (que estão prontos e nunca são concluídos) têm STAAtualApartment e apartamento de destino Desconhecido, embora sejam _currentWorkItem de três trabalhadores paralelos.

@CharliePoole Não SetUpFixtures, há configurações de uma só vez ou teardowns. Você descreveu o que acontece quando não há travamento, mas e quando há travamento? Quando anexo um depurador, vejo três trabalhadores de teste em uma chamada para WorkItemQueue.Dequeue que nunca pode retornar, cada um com um item de trabalho OneTimeTearDown diferente no estado Pronto. Isso não significa que cada um dos três itens de trabalho foram colocados no _currentWorkItem cada trabalhador, mas nunca concluídos e, de alguma forma, todos os três trabalhadores voltaram e ligaram para Dequeue novamente apesar disso? De que outra forma posso explicar o que estou vendo no depurador?

Terei que ver isso quando puder entrar no código ... um pouco mais tarde esta noite.

Ah, neste repro commit eu tenho um atributo ITestAction aplicado ao assembly. Eu removi o atributo de commits posteriores que ainda eram reproduzidos, mas não com tanta freqüência.

Se eu usar --inprocess , poderei ver uma TargetInvocationException. Além disso, a impressão do rastreamento de pilha é interrompida para imprimir outra mensagem de exceção, desta vez uma AccessViolationException tentando analisar o PDB para obter informações de linha para o rastreamento de pilha. Isso é selvagem.

Em seguida, o teste final foi executado e a reprodução aconteceu. Apenas 7 dos 9 threads de trabalho ainda estão em execução.

Talvez a reprodução seja lançar AccessViolationException em um método de teste?

A execução do teste autoexecutável é de até milhares de invocações de new FrameworkController (), LoadTests, RunTests e sem reprodução. Sem exceções também.

Parece um bug no .NET Framework, https://github.com/dotnet/coreclr/issues/19698 , "System.AccessViolationException while formatting stacktrace"

(Minha máquina tem HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\Release = 528040, o que significa .NET Framework 4.8 no Windows 10 1903.)

Esta violação de acesso intermitente pode ser reproduzida colocando simplesmente new StackTrace(true); como o corpo de alguns métodos de teste. A violação de acesso e o travamento sempre parecem andar juntos.

No entanto, isso é extremamente sensível ao tempo.

Heh, a exclusão de arquivos estava trabalhando contra mim porque quanto mais arquivos de origem são incorporados no PDB do assembly de teste, mais lento System.Diagnostics.StackTraceSymbols.GetSourceLineInfoWithoutCasAssert é e mais provável resultar no bug de violação de acesso devido à condição de corrida.

Todos os testes podem ser comentados, mas mais arquivos .cs no projeto de teste = melhor chance de reprodução

Eu tenho uma ferramenta que mostra a taxa de reprodução para cem execuções (executa quatro processos do console NUnit por vez) para que eu possa ver se uma alteração feita no projeto o torna melhor ou pior. É sempre 10% agora. Quero extrair muito código proprietário do repro que não pude tocar sem perder o repro.

De acordo com esta ferramenta, uma diferença de tempo de 5 ms nos testes anteriores aos testes new StackTrace(true); faz a diferença entre uma taxa de reprodução de 20% e uma taxa de 2%.

Desculpe, eu ia dar uma olhada, mas outras coisas aconteceram. Indo dormir agora.

Eu agradeço! Acho que a causa é um bug do .NET Framework que permite uma exceção envenenada que lança AccessViolationException sempre que o rastreamento de pilha é impresso ou recuperado. Isso mata um dos threads de trabalho de teste e grava diretamente no stderr.

Assim que obtiver uma reprodução compartilhável, começarei a procurar maneiras de proteger o NUnit contra esse bug do .NET Framework.

Enviado um repro independente (se ainda não completamente mínimo) para https://github.com/jnm2/NUnitHangRepro.

🎉 É repros em outra máquina (mostra taxa de reprodução de 13%)! Também .NET Framework 4.8, Windows 10 1903.

Remover <EmbedAllSources>true</EmbedAllSources> é uma solução alternativa se os únicos PDBs na pasta de saída forem seus.

Outra solução alternativa pode ser uma configuração única em um SetUpFixture para causar uma exceção propositalmente em cada montagem que possui um PDB. Ele será executado antes de todos os testes, portanto, não há chance de uma condição de corrida. O problema com essa solução alternativa é que ela requer muita previsão quando você tem muitos assemblies para testar.

Remover EmbedAllSources não é bom o suficiente em geral. O travamento ainda está acontecendo. Talvez remover isso apenas mude o tempo, ou talvez NUnit.TestAdapter.pdb esteja bagunçando isso.

Acabei de me lembrar de algo que vi no ano passado. Havia um problema de desempenho com a mesma coisa, o formato PDB portátil. Lembro que houve uma mudança para desativar as informações de arquivo e linha.
Isso me permitiu desenterrar isso. Podemos tentar isso nos arquivos app.config dos testes:

  <runtime>
    <AppContextSwitchOverrides value="Switch.System.Diagnostics.IgnorePortablePDBsInStackTraces=true" />
  </runtime>

https://github.com/Microsoft/dotnet/blob/master/Documentation/compatibility/Stack-traces-obtained-when-using-portable-PDBs-now-include-source-file-and-line-information-if- request.md

Usar o formato PDB do Windows também pode funcionar, embora, como o adaptador NUnit VSTest aparece próximo ao ponto de entrada em um rastreamento de pilha de exceção, seu PDB portátil ainda pode lançar coisas.

A opção app.config não pareceu surtir efeito. A última solução alternativa que posso pensar é: destruir os PDBs na saída de compilação dos testes.

Cometi um erro ao verificar a reprodução. Tirar os PDBs de todos os diretórios de montagem de teste antes de executar os testes parece ser uma boa solução por enquanto. Vou tentar o switch app.config novamente.

Acabei de demonstrar novamente que a solução alternativa app.config não funciona. Provavelmente foi bloqueado por https://github.com/nunit/nunit-console/pull/443/files (fyi: @ nunit / engine-team). Os IgnorePortablePDBsInStackTraces=false que adicionamos às configurações do console e do agente provavelmente estão substituindo o IgnorePortablePDBsInStackTraces=true que estou colocando em cada projeto de teste, apesar de ser um AppDomain separado. (?)

Repro atual. Isso dá em média uma taxa de reprodução de 23% em 1000. Para maximizar a janela de tempo em que a condição de corrida é mortal, o projeto precisa de <EmbedAllSources>true</EmbedAllSources> e um arquivo fonte muito grande como este .

using System.Diagnostics;
using System.Threading;
using NUnit.Framework;

[assembly: Parallelizable(ParallelScope.Children)]

// 6 or greater makes it more likely for A(35) and A(55) to start at the same time
[assembly: LevelOfParallelism(6)]

namespace HangRepro
{
    public static class ReproTests
    {
        [Test]
        public static void A([Values(35, 55)] int delay)
        {
            Thread.Sleep(delay);

            // Boom
            new StackTrace(true);
            // or e.g. any Shouldly method, since the first thing it does is grab a stack trace.
        }

        [Test]
        public static void B([Range(1, 20)] int _)
        {
            Thread.Sleep(20);
        }
    }
}

Atualmente, acho que a verdadeira correção é que o console, os agentes e o NUnitLite precisam de <legacyCorruptedStateExceptionsPolicy enabled="true"/> em seu app.config ou então o NUnit Framework não consegue capturar AccessViolationException, embora o framework já tenha [HandleProcessCorruptedStateExceptions] onde ele precisa.

Problema existente: https://github.com/nunit/nunit-console/issues/336

Espere, não é assim que funciona. [HandleProcessCorruptedStateExceptions] funciona independentemente do arquivo .config. <legacyCorruptedStateExceptionsPolicy> no console / agente .config corrige o problema, embora isso seja uma pista.

Não tenho ideia do porquê, mas o fio simplesmente desaparece apesar do [HandleProcessCorruptedStateExceptions] . O depurador mostra a AccessViolationException normalmente na linha 270. Quando você continua, o depurador mostra a exceção novamente, mas desta vez como não tratada, apesar do atributo!

https://github.com/nunit/nunit/blob/a56e6859f3609128128ef87b0226472eb63ea5d4/src/NUnitFramework/framework/Internal/Reflect.cs#L261 -L275

Se você continuar novamente, o console retoma a execução e a discussão simplesmente desaparece. Se [HandleProcessCorruptedStateExceptions] não tiver efeito, não há nada que possamos fazer para corrigir isso na estrutura. Ele deve ser corrigido no .config do console.

Aha, é por isso:

O atributo HandleProcessCorruptedStateExceptionsAttribute é ignorado quando é encontrado em código parcialmente confiável ou transparente, porque um host confiável não deve permitir que um suplemento não confiável capture e ignore essas exceções graves.

Temos que escolher entre confiança parcial e HandleProcessCorruptedStateExceptionsAttribute.

@rprouse Nenhum dos HandleProcessCorruptedStateExceptionsAttributes que adicionamos teve qualquer efeito por causa de https://github.com/nunit/nunit/pull/1775. Devemos retirá-los (ou pelo menos marcá-los como ineficazes) ou devemos remover a confiança parcial?

Se escolhermos manter a confiança parcial, ficaremos à mercê dos arquivos .config do console / agente para evitar que o thread desapareça. Como o encadeamento desaparece, a única maneira que posso pensar para evitar um travamento é implementar um encadeamento de watchdog que percebe quando isso acontece, o que pode marcar o item de trabalho como com erro devido ao encadeamento ser eliminado por uma exceção não detectável. Mas mesmo este é um cenário estranho porque normalmente o console seria encerrado ao mesmo tempo que o thread, mas temos esta linha no .config do console para manter o processo vivo, apesar do thread morrer:

https://github.com/nunit/nunit-console/blame/4ae12183927747e2a35859ad520790f84ebaf5f9/src/NUnitConsole/nunit3-console/app.config#L17

Devo marcar @ nunit / framework-team e também @ggeurts (que solicitou e adicionou confiança parcial) e @endrih (que solicitou e adicionou tratamento de exceções como AccessViolationException). Não podemos ter as duas coisas.

Acho que é seguro dizer que sempre teremos exceções de estado corrompidas, mas nem sempre teremos confiança parcial, pois não é recomendado pela Microsoft. (Explorações recentes da CPU mostram o quão fraco é todo o paradigma também!)
O que você acha de remover o suporte para confiança parcial, agora que oferecemos suporte desde o framework NUnit v3.5?

Grande escavação aqui Joseph - acho que concordo com suas conclusões.

Esse problema seria outra duplicata, certo? https://github.com/nunit/nunit-console/issues/644 cc @samcook

Sim, parece que provavelmente é a mesma coisa que eu também encontrei.

A pergunta da equipe foi movida para https://github.com/nunit/nunit/issues/3301, pois este tópico é muito longo e a pergunta foi meio enterrada. Não há mais nada a fazer neste tópico até que https://github.com/nunit/nunit/issues/3301 seja decidido. Se a decisão for por um lado, esse problema será resolvido junto com aquele. Se for de outra maneira, teremos uma opção final para explorar neste problema: um thread de watchdog para evitar o travamento quando um thread de trabalho de teste desaparecer sem marcar o item de trabalho que ele possui.

(Um thread de watchdog só ajudará ao usar o runner do console, porque o comportamento padrão para uma exceção não tratada é matar o processo, não apenas um thread. O runner do console opta por manter o processo vivo, mas isso é incomum e, por exemplo, ReSharper e VSTest's os processos do host ainda serão eliminados.)

Para todos que estão assistindo a este tópico que estão enfrentando esse problema ao usar o executor do console: Use o 3.11.0-dev-04532 ou versão posterior de https://www.myget.org/feed/nunit/package/nuget/NUnit.ConsoleRunner. Essa é uma maneira de corrigir esse problema. (Correção: https://github.com/nunit/nunit-console/pull/660)

Não foi confirmado, mas parece provável que não seremos capazes de corrigir isso no lado do NUnit Framework até a v4.

Isso foi corrigido no .NET Framework na atualização de fevereiro:
https://devblogs.microsoft.com/dotnet/net-framework-february-2020-security-and-quality-rollup/

CLR

  • Há uma condição de corrida no cache do provedor de metadados PDB portátil que vazou provedores e causou travamentos na API StackTrace de diagnóstico. Para corrigir a corrida, detecte a causa pela qual o provedor não estava sendo descartado e descarte-o.
Esta página foi útil?
0 / 5 - 0 avaliações