7 de abr. de 2013

Fail-Fast (Falhe-Rápido), contra-intuitivo mas necessário

Naturalmente, os programadores não querem que seus programas caiam, desapareçam na frente do usuário. Contudo isto pode ser necessário na maior parte das vezes, para que o programa não siga operando em um estado inválido, ou seja, frequentemente é melhor fazer o programa cair do que deixá-lo operando de forma incorreta, o que pode aumentar os danos.

O princípio de projeto que argumenta esta ideia é chamado de Fail-Fast, que significa Falhe-Rápido. Tanto o conceito como implementação são realmente simples: fazer o programa cair ao detectar uma operação ilegal em vez de tentar contornar e seguir operando.

Como um pequeno exemplo, considere um método (ou função) que realiza operações de soma sobre números. Poderia ser qualquer operação, mas neste exemplo vou trazer a simples adição de dois números:

Método para somar dois números:
class Numeros {
 
    public static int soma(int a, int b) {
        return a + b;
    }
}
 
// situação de uso:
int s = Numeros.soma(34, 23); // s == 57

Esta implementação parece funcionar bem, em vários testes ela dá um resultado previsível e correto, mas se somarmos quaisquer números que dê como resposta um número maior que 2147483647 (o valor máximo aceito pelo tipo INT) então teremos uma surpresa.

Quando a operação está incorreta:
class Numeros {
 
    public static int soma(int a, int b) {
        return a + b;
    }
}
 
int s = Numeros.soma(1000000000, 2000000000); // deveria dar 3000000000 (3 bilhões)
System.out.println(s); // mas imprime -1294967296

Este tipo de erro é chamado de Buffer Overflow. Quando a soma ultrapassar o limite do Inteiro, acontece a mudança de sinal e a diferença de valor. Note que esta operação não causa falha, ou seja, se o programa depende deste cálculo então ele recebe o valor incorreto e segue funcionando com base neste valor, provavelmente levando a mais operações incorretas e causando mais danos.

Este exemplo é um candidato a aplicação do Princípio do Fail-Fast, onde tentaremos lançar uma exceção sempre que o valor transbordar (overflows).

É bem simples, sempre que forem somados dois números positivos então a resposta deve ser positiva e se forem somados dois números negativos então a resposta deve ser negativa. O método seria implementado assim:

Protegendo contra Buffer Overflow fazendo a aplicação cair (Exception):
class Numeros {
     
    public static int soma(int a, int b) {
         
        int r = a + b;
         
        if (a > 0 && b > 0 && r < 0) {
            throw new RuntimeException("BufferOverflow");
        }
         
        if (a < 0 && b < 0 && r > 0) {
            throw new RuntimeException("BufferOverflow");
        }
                 
        return r;
         
    }
     
}
 
int s = Numeros.soma(1000000000, 2000000000);

// deveria dar 3000000000 (3 bilhões),
// mas daria -1294967296, logo se o resultado não é correto
// então o ideal é lançar uma exceção e deixar o programa cair:

Exception in thread "main" java.lang.RuntimeException: BufferOverflow
    at javaapplication556.Numeros.soma(Main.java:58)
    at javaapplication556.Main.main(Main.java:17)
Java Result: 1

Existe um excelente artigo sobre Fail-Fast escrito por James Shore disponível aqui: http://martinfowler.com/ieeeSoftware/failFast.pdf.

James diz que algumas pessoas recomendam fazer um software robusto que contorna os problemas e resulta na falha gradativa. Isto é, o programa continua funcionando após o erro mas acaba falhando mais adiante, entretanto de uma maneira estranha. Um sistema que falha rápido faz justamente o contrário. As pessoas relutam em aplicar pois faz parecer que o software é mais frágil, mas é justamente o contrário.

Jim coloca um exemplo onde é considerada a leitura de uma propriedade em um arquivo de configuração, e pode ser que a propriedade não esteja presente então é devolvido um valor padrão:

Um método que NÃO falha rápido (em C#):
public int maxConnections() {
 
    string property = getProperty(“maxConnections”);
 
    if (property == null) {
        return 10;
    } else {
        return property.toInt();
    }
}

Em contraste, um método que falha rápido seria escrito assim:

Um método que FALHA RÁPIDO (em C#):
public int maxConnections() {
 
    string property = getProperty("maxConnections");
 
    if (property == null) {
        throw new NullReferenceException("maxConnections property not found in " +
                                         this.configFilePath);
    } else {
        return property.toInt();
    }
}

Escolher se as operações em um sistema devem falhar ou seguir operando mesmo com valores incorretos e resultados imprevisíveis é uma decisão de projeto importante. Cabe ao desenvolvedor avaliar os riscos envolvidos e utilizar a melhor estratégia.

Particularmente sou a favor do Fail-Fast, por tornar o sistema mais fácil de depurar, trazendo informações da falha imediatamente quando ela acontece, em vez de expor algum "erro loco" lá adiante.

Cabe salientar que nem sempre Fail-Fast se aplica, como em alguns programas que devem seguir operando mesmo quando há erro pois não tem uma segunda chance, como um aplicativo embarcado em um foguete que está tentando pousar. É melhor tentar pousar mesmo após o erro do que abortar a missão que custou milhões.

Nenhum comentário: