Há alguns dias eu escrevi sobre o Rewind VM, uma máquina virtual determinista onde cada execução de uma build Nix é uma função pura de suas entradas, incluindo o agendamento de threads! Estou usando para encontrar, reproduzir e resolver condições de corrida em builds Nix, mas está me deixando um pouco maluco. Que superpoder é essa, e por que ninguém mais está usando?
A ferramenta rapidamente ganhou um painel de código, quadros de pilha, marcadores, uma aba "Compare", suporte a muito mais GDB, faixas de threads que mostram quem segurou a CPU em cada passo, e "Check from here", que ajuda a encontrar o passo exato onde uma condição de corrida acontece. Eu entrei em cada nova função pensando que seria um grande levantamento de código, mas continuamente encontrando a mesma coisa: a parte difícil de cada função já estava feita, e o Nix já havia feito.
Por exemplo, um depurador precisa das entradas exatas do programa, seus símbolos de depuração, suas fontes, as fontes de cada biblioteca sob ele, e uma maneira de alguém mais obter tudo isso em sua máquina. Isso é exatamente o que uma derivação faz, e o Nix torna isso fácil.
Dois caixas, uma conta
Um exemplo clássico simples de condição de corrida é duas threads depositando em uma conta bancária. Cada depósito lê o saldo, escreve uma linha no razão, então armazena o saldo mais o depósito. Se um caixa corre entre a leitura e o armazenamento do outro, ele escreve um saldo desatualizado sobre os depósitos do outro.
static void deposit(int teller, long amount){long seen = balance;char line[64];int n = snprintf(line, sizeof line,"teller %d: %ld + %ld\n",teller, seen, amount);if (write(1, line, n) != n)return;balance = seen + amount;}
Um caixa que corre entre a leitura e o armazenamento do outro escreve um saldo desatualizado sobre os depósitos do outro.
11
No meu laptop de 16 cores, o banco perdeu dinheiro em 396 de 1.000 execuções. Preso a uma única core com taskset, não perdeu dinheiro em nenhuma de 1.000: uma core sozinha raramente troca threads no meio de um depósito, é por isso que o Rewind perturba o agendamento.
A VM do Rewind tem uma CPU, então sua primeira execução passa bem. rewind check executa a build novamente sob agendamentos perturbados, cada um pedindo ao kernel do guest para reagendar em passos diferentes, e reduz a primeira falha a um único passo:
$ rewind check --where github:fzakaria/rewindvm#bank...step 3237 decide: um reagendamento ali faz a execução falhar
passing: run 12feb5f832205a72, schedule 0
failing: run c2f1cfac0c288f3e, schedule 1 over steps 3237..3238
As duas execuções são a mesma máquina, saída por saída, até o passo 3237, onde apenas a que falha recebe um reagendamento.
Coisa gratuita um: as entradas
O argumento para check é uma derivação e pode ser uma referência flake. A beleza do Nix é que ele sabe as entradas de uma derivação, então é tudo o que precisamos para garantir que a execução seja reprodutível. As entradas da derivação são a fonte, o compilador, as bibliotecas, o kernel, e a configuração da VM. O rewind nix realiza as entradas da derivação, empacota o fechamento em uma imagem erofs somente leitura, e boota a VM nela. O ID da execução é um hash das suas entradas, da maneira que um caminho de store é, e rewind show imprime o comando que o faz novamente:
$ rewind show 12feb5f8
rewind nix /nix/store/cr8rl40rjb9cmmcd9sxn9mpdrhvp53jj-bank-0.1.0.drv --epoch --clock branches --name bank-0.1.0 # 12feb5f832205a72
Quando a build tem sucesso, o guest relata o hash NAR de cada saída, e rewind nix o compara com a cópia do host e com cada cache de substituição binária do Nix, buscando apenas o .narinfo:
$ rewind nix github:fzakaria/rewindvm#bank.../nix/store/27a4q7w90jzp4wg5kaaajvb64zll6yqg-bank-0.1.0 dbf0df3de84b7a84 matches your store
Isso nos ajuda a validate que a build dentro da VM é a mesma que no meu laptop, e que a execução da VM é reprodutível pelo Nix.
Fonte: Hacker News · Resumido por HeadlinesBriefing