Simulando as condições de corrida em testes de unidade RSpec
-
19-09-2019 - |
Pergunta
Nós temos uma tarefa assíncrona que executa uma potencialmente longa de cálculo para um objeto. O resultado é então armazenada em cache no objeto. Para evitar que várias tarefas de repetir o mesmo trabalho, nós adicionamos bloqueio com uma atualização SQL atômica:
UPDATE objects SET locked = 1 WHERE id = 1234 AND locked = 0
O bloqueio é apenas para a tarefa assíncrona. O objeto em si pode ainda ser atualizados pelo usuário. Se isso acontecer, qualquer tarefa inacabada para uma versão antiga do objeto deve descartar seus resultados como eles estão propensos out-of-date. Esta é também muito fácil de fazer com um atualizar SQL atômica:
UPDATE objects SET results = '...' WHERE id = 1234 AND version = 1
Se o objeto foi atualizado, sua versão não irá corresponder e assim os resultados serão descartados.
Estas duas atualizações atômicas deve lidar com quaisquer condições de corrida possíveis. A questão é como verificar que em testes de unidade.
O primeiro semáforo é fácil de teste, como é simplesmente uma questão de criação de dois testes diferentes, com os dois cenários possíveis: (1) onde o objeto está bloqueado e (2) em que o objeto não está bloqueado. (Nós não precisamos de testar a atomicidade da consulta SQL como que deve ser da responsabilidade do fornecedor do banco de dados.)
Como é que um teste do segundo semáforo? O objeto precisa ser mudado por um terceiro algum tempo após o primeiro semáforo, mas antes do segundo. Isso exigiria uma pausa em execução para que a atualização pode ser confiável e consistente realizada, mas eu sei de nenhum apoio para injetar breakpoints com RSpec. Existe uma maneira de fazer isso? Ou há alguma outra técnica que eu estou com vista para simular essas condições de corrida?
Solução
Você pode pedir uma ideia de Electronics Manufacturing e ganchos de teste put diretamente no código de produção. Assim como uma placa de circuito pode ser fabricado com lugares especiais para equipamentos de teste para controle e sondar o circuito, podemos fazer a mesma coisa com o código.
Suponha que tenhamos algum código de inserir uma linha no banco de dados:
class TestSubject
def insert_unless_exists
if !row_exists?
insert_row
end
end
end
Mas este código está sendo executado em vários computadores. Há uma condição de corrida, então, uma vez mais processos podem inserir a linha entre o nosso teste e nossa inserção, causando uma exceção DuplicateKey. Queremos teste que nosso código lida com a ressalva de que os resultados de que condição de corrida. A fim de fazer isso, o nosso teste precisa inserir a linha após a chamada para row_exists?
mas antes da chamada para insert_row
. Então, vamos adicionar um gancho de teste ali:
class TestSubject
def insert_unless_exists
if !row_exists?
before_insert_row_hook
insert_row
end
end
def before_insert_row_hook
end
end
Quando executado no estado selvagem, o gancho não faz nada exceto comer um pouquinho de tempo de CPU. Mas quando o código está sendo testado para a condição de corrida, o teste de macaco-patches before_insert_row_hook:
class TestSubject
def before_insert_row_hook
insert_row
end
end
Não é dissimulado? Como uma larva de vespa parasita que seqüestrou o corpo de uma lagarta desavisado, o teste sequestrado o código sob teste para que ele irá criar a condição exata que precisamos testado.
Esta ideia é tão simples quanto o cursor XOR, então eu suspeito que muitos programadores ter inventado de forma independente. Eu tê-lo encontrado para ser geralmente útil para testar código com condições de corrida. Espero que ajude.