Compartilhar via


Corrigir falhas intermitentes de build

Lidar com falhas de build que não acontecem sempre é uma experiência frustrante. Este artigo ajudará você a identificar a causa raiz e a fazer alterações que permitirão corrigir falhas intermitentes de build, para que os builds sejam executados de modo consistente todas as vezes.

O MSBuild dá suporte a builds paralelos executando diferentes processos de nó de trabalho em diferentes núcleos de CPU. Embora muitas vezes haja benefícios significativos de desempenho decorrentes da compilação em paralelo, isso também pode introduzir o risco de erros que ocorrem quando vários processos tentam usar o mesmo recurso ao mesmo tempo. Essa situação é um tipo de condição de corrida. Uma condição de corrida pode manifestar comportamentos diferentes de build para build. Por exemplo, um processo pode estar à frente ou atrás de outro por diferentes quantidades de tempo.

Mensagens de erro que surgem da contenção de E/S de arquivo sempre incluem uma falha de E/S de arquivo do sistema operacional, mas podem ter códigos de erro do MSBuild diferentes dependendo do que estava acontecendo no build quando ocorreu o erro de E/S de arquivo. Alguns exemplos podem ser semelhantes aos seguintes na plataforma Windows:

error MSB3677: Unable to move file "source" to "dest".
Cannot create a file when that file already exists. [{project file}] 
The process cannot access the file 'file' because it is being used by another process.

A corrida de contenção de arquivo pode ocorrer quando é solicitado o build de um projeto específico com mais de uma combinação de configurações de propriedades. Normalmente, o MSBuild faz um build separado para um projeto referenciado sempre que as configurações de propriedade diferem, caso a saída também possa ser diferente. Dependendo do tempo dos builds em execução simultânea, operações de movimentação ou cópia poderão falhar se um arquivo já estiver presente no mesmo local, ou poderão falhar porque o arquivo de destino está sendo usado por outro processo do MSBuild. Além disso, operações de leitura de arquivo poderão falhar se outro processo do MSBuild estiver lendo ou gravando o mesmo arquivo.

Você pode corrigir permanentemente a maioria dos problemas de contenção de arquivo de build entendendo a causa e fazendo as alterações apropriadas nos arquivos de projeto, mas somente se a causa estiver em seu código. Condições de corrida também podem ser causadas por bugs no código do SDK. Nesse caso, o problema precisa ser relatado e investigado pelos proprietários do SDK em questão.

Causas das condições de corrida de build

Esta seção descreve diferentes tipos de problemas que podem ocorrer e levar a condições de corrida. A próxima seção, Diagnosticar e corrigir condições de corrida, descreve o que fazer para resolver esses problemas.

Configurações inconsistentes da propriedade ProjectReference

Diferentes builds do mesmo projeto são uma parte normal de muitos processos de build; eles ocorrem quando MSBuild compila a saída para mais de uma combinação de configurações. Por exemplo, uma solução pode ter várias estruturas de destino (como net472 e net7) ou várias arquiteturas de plataforma de destino (como Arm64 e x64). Esse requisito de build é atendido especificando uma pasta de saída diferente para cada combinação de saídas. Dessa forma, a versão Arm64 net472 de um assembly é gerada em uma pasta diferente das outras combinações e nenhum conflito ocorre. As configurações padrão do SDK já lidam com os exemplos mencionados aqui, mas às vezes a ocorrência de várias combinações de configurações não é tão óbvia e precisa ser investigada.

Propriedades de ProjectReference entram em conflito com propriedades globais

Propriedades globais, ou seja, quando você define uma propriedade na linha de comando com a opção /p ou /property, são usadas implicitamente para builds de projeto referenciados. No entanto, usando RemoveGlobalProperties ou GlobalPropertiesToRemove, você pode omitir algumas ou todas as configurações de propriedade global para qualquer referência de projeto, portanto, se essas propriedades não forem usadas consistentemente, você poderá ter uma situação em que mais de uma versão de um projeto referenciado é compilada com a propriedade global definida, e outra em que ela não é definida ou tem um valor diferente.

Empacotamento dispara acidentalmente builds de projeto

Se o build empacotar a saída de projetos que foram compilados anteriormente, você poderá encontrar uma condição de corrida quando a lógica de build de empacotamento especificar configurações de propriedade diferentes dos projetos originais usados quando eles foram compilados. Nesse caso, MSBuild normalmente dispararia uma recompilação desses projetos devido à incompatibilidade nas propriedades. Essa situação pode levar a condições de corrida. Considere definir BuildProjectReferences como false no projeto de empacotamento, de modo que nunca seja solicitada a compilação dos projetos que estão sendo empacotados. Isso significaria que o build de empacotamento só deve ser solicitado quando os builds do projeto jpa firam feitos e atualizados.

Diagnosticar e corrigir condições de corrida

Suspeite de um erro de condição de corrida quando operações de movimentação, operações de cópia ou gravações de arquivo para arquivos gerados pelo build falham intermitentemente.

A abordagem para resolver o problema depende do resultado desejado. Você precisa mesmo de duas versões diferentes do projeto compilado? Nesse caso, torne a pasta de saída diferente para as duas configurações de propriedade diferentes. Caso contrário, você pode alterar os elementos ProjectReference para garantir que as mesmas propriedades sejam definidas para cada referência.

Para diagnosticar e corrigir a condição de corrida, siga estas etapas.

  1. Verifique se você não tem nenhum outro programa em execução que esteja usando esses arquivos, como uma sessão de depuração do Visual Studio no mesmo computador.

  2. Descubra se o problema desaparecerá se você executar builds com a opção /m:1 de MSBuild. Confira Referência de linha de comando do MSBuild. Essa é a opção de linha de comando que informa ao MSBuild o número de nós a serem usados para builds. Se definida como 1, os builds continuarão em série e uma condição de corrida não poderá ocorrer. Usar a opção /m:1 é uma solução alternativa que você pode usar para evitar a condição de corrida, mas não é uma solução de longo prazo. A saída do build ainda é compilada mais de uma vez, com possíveis diferenças, o que é uma condição de erro chamada overbuild. Além disso, compilar em série aumenta significativamente o tempo necessário para concluir um build. Se o build demonstrar apenas a falha intermitente de E/S do arquivo quando o build paralelo estiver habilitado (o número de processadores for maior que 1), essa será uma condição de corrida de build.

  3. Gere um log. Execute um build com o detalhamento como Normal ou superior (por exemplo, use a opção de linha de comando -verbosity:Normal). Para solucionar problemas com a condição de corrida, é recomendável gerar um log binário (usar a opção /bl ou /binlog) e exibi-lo com o Visualizador de Log Estruturado. Para obter um log útil para diagnosticar uma condição de corrida, não é necessário que o log seja de uma execução com falha, pois você ainda pode localizar os vários locais onde a saída que produziu o erro é acessada.

  4. Quer essa execução específica tenha falhado ou não, abra o log (ou arquivo .binlog), pesquise o nome do arquivo que dispara a falha e localize todos os locais em que o arquivo é usado.

    A captura de tela a seguir mostra o Visualizador de Log Estruturado exibindo o log produzido pela compilação da solução no Exemplo. O que é mostrado são os resultados da pesquisa para o arquivo net5.0\Base.dll, que foi mencionado em uma mensagem de erro. O mesmo arquivo de saída aparece duas vezes mais do que o OutputAssembly para a tarefa Csc nos resultados da pesquisa, indicando que está sendo compilado mais de uma vez. Captura de tela mostrando os resultados da pesquisa no Visualizador de Log Estruturado.

  5. Observe as configurações de propriedade em vigor para cada instância do build desse projeto. O Visualizador de Log Estruturado facilita isso, pois cada build de projeto individual tem um nó Propriedades listando todas as configurações de propriedade em vigor para o build de projeto em questão. Se você estiver usando um log de texto, as propriedades definidas para um build serão geradas no texto quando a configuração de detalhamento for Normal ou maior. Compare as listas de propriedades para cada build do projeto que gera a saída com falha. Você deverá ver uma diferença se o problema realmente for uma condição de corrida.

    A captura de tela a seguir mostra o Visualizador de Log Estruturado, com o nó Propriedades expandido para um build de projeto. Observe que esse build está sob o nó ProjectReferences de outro projeto, o que significa que esse build foi disparado por um elemento ProjectReference em outro projeto. Se você seguir os nós até a árvore, poderá ver qual projeto a referenciou.

    Captura de tela do Visualizador de Log Estruturado mostrando as propriedades de um build de projeto.

    Compare a lista com o outro build do mesmo projeto e você poderá ver que SpecialMode está ausente. Esse é um build de nível superior, o que significa que foi compilado porque estava na solução em si, não porque foi referenciado por outro projeto.

    Captura de tela mostrando um conjunto diferente de propriedades para o mesmo projeto.

  6. Pesquise nos arquivos de projeto ProjectReference, em que o atributo Include especifica esse projeto. Procure pelos metadados SetConfiguration, SetPlatform, SetTargetFramework, AdditionalProperties, RemoveGlobalProperties ou GlobalPropertiesToRemove. Verifique se há diferenças nos valores definidos para esses metadados entre diferentes elementos ProjectReference em toda a solução. No exemplo, é a configuração de metadados inconsistente AdditionalProperties (em um lugar, mas não no outro) que é a origem do problema.

  7. Considere o significado das configurações de propriedade que são diferentes e se elas afetariam de fato a saída do build. Se as diferenças de configuração de propriedade forem significativas e a saída for distinta propositalmente, a correção será usar uma pasta diferente para cada variação da configuração, assim como o SDK do .NET faz para a plataforma, a configuração (por exemplo, Depuração ou Versão) ou a estrutura de destino. Se a diferença de configuração de propriedade não for intencional ou for insignificante, tente encontrar uma maneira de alterar o código do projeto para eliminar a diferença nas propriedades. No exemplo, isso pode ser feito adicionando os metadados AdditionalProperties="SpecialMode=true" ao ProjectReference em Middle2.csproj ou removendo os metadados AdditionalProperties de Middle1.csproj. A alteração apropriada depende dos requisitos específicos do aplicativo, serviço ou destino.

  8. Se o erro estiver em um SDK que não está sob seu controle, relate o problema ao proprietário do SDK.

Exemplo

Um caso simples que demonstra o padrão é apresentado aqui. Suponha que você tem uma solução com vários projetos, um cliente de front-end (App), duas bibliotecas de classes (Middle1 e Middle2) e uma biblioteca (Base), que é referenciada pelas duas bibliotecas de classes.

Os arquivos de projeto nas seções de código a seguir fazem parte de uma solução. Essa coleção de projetos resulta em dois builds diferentes de Base, um com SpecialMode=true e outro sem. Pode ocorrer um erro intermitente que referenciaria a saída Base.dll. Às vezes, você pode receber um erro indicando que Base.dll não pôde ser gravado "porque está sendo usado por outro processo".

<!-- Base.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\Base\Base.csproj" />
  </ItemGroup>

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>
<!-- Middle1.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\Base\Base.csproj" AdditionalProperties="SpecialMode=true" />
  </ItemGroup>

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>
<!-- Middle2.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\Base\Base.csproj" />
  </ItemGroup>

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>
<!-- App.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\Middle1\Middle1.csproj" />
    <ProjectReference Include="..\Middle2\Middle2.csproj" />
  </ItemGroup>

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>

Se o comportamento desejado for usar SpecialMode, a correção apropriada será adicionar o mesmo valor de metadados AdditionalProperties ao ProjectReference em Middle2.csproj:

<!-- Middle2.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\Base\Base.csproj" AdditionalProperties="SpecialMode=true" />
  </ItemGroup>

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>

O problema de build também poderá ser corrigido removendo os metadados AdditionalProperties de Middle1.csproj, se isso for adequado à intenção.