Javascript e um Sistema Tolerante à Falhas

Sistema tolerante à falhas é uma das matérias da Ciência da Computação que eu menos gostei, ao mesmo tempo foi o que mais me chamou a atenção, porque embora fosse uma matéria muito maçante, muito baixo nível, ela tem uma visão que pode ser perfeitamente usada no front-end, no Javascript.

Javascript Exceptions

Toda vez que depara com um erro de javascript no browser significa que uma Exception foi lançada e nenhum mecanismo no seu sistema soube tratar ou capturar esta exception, no final sobra para o Browser resolver o que fazer com aquilo, geralmente é “logando” no console a mensagem de erro.

Tudo bem, pode ser que isso não seja novidade para a maioria, mas o que a gente não pára pra analisar é.

  • Como poderíamos tratar os erros no javascript
  • Como identificar os pontos do seu código que pode quebrar
  • Qual a importância disso?

Consequência de um erro não tratado

Bom, o porque de ser importante no mínimo pensar sobre o assunto é fácil de entender usando bom e velho exemplo de uma aplicação do Twitter.

Eu não conheço nada da regra de negócios da aplicação do Twitter e a abstração a seguir é apenas para ilustrar, digamos que separemos os módulos da aplicação como mostrado na imagem, numa arquitetura modular:

Screen Shot 2015-11-09 at 20.50.39

Imagine que o seu primeiro módulo, lá na barra de status no topo esquerdo quebre, ou seja, lance uma exception não tratada no momento que é iniciado. Se sua arquitetura executa os módulos de forma sequencial, isto significa que este é o primeiro módulo a ser executado e portanto, nenhum outro vai ser executado à partir deste ponto.

Ruim né? Ou seja, NADA irá funcionar e no pior das hipóteses, dependendo de como implementa o conteúdo da parte central, se esta for totalmente dependente de Javascript, ficará comprometida e totalmente em branco!!!.

Isso acontece em parte porque o Javascript é interpretado, de forma sequencial, ele não é compilado e portanto seus erros  ocorrerão em tempo de execução, a partir dali todo o resto do código é comprometido e o interpretador não sabe o que fazer com aquilo.

O que poderíamos fazer?

Eu não tenho a pretensão de ensinar você como tratar erros da melhor forma no Javascript, primeiro porque não tenho tanta competência no assunto e outra porque é extremamente subjetivo, cada caso é um caso, mas posso mostrar algumas idéias à partir de alguns pontos de STF ( Sistema Tolerante à Falha ) que aprendi, de maneira muito artificial e um pouco mais didática e prática.

Não vou usar os jargões da matéria como confiabilidade, resiliência etc etc.. isso é bem chato, eu boto um link no final para caso queiram aprofundar no assunto.

No caso do Twitter, imagina que sua arquitetura modular possui padrões, e uma delas é que o módulo sempre é iniciado através de uma chamada .init(). Já é uma pista certo? Na situação anterior, identificamos que o carregamento destes módulos é uma situação crítica, portanto precisamos tratar TODOS os possíveis erros que acontecem no momento da inicialização, essa é a segunda pista.

Então você pode pensar que isso é a coisa mais fácil de fazer, você conhece seu código, “sabe” onde pode dar bosta, certo? Claro que não.

Um módulo pode iniciar uma série de scripts, inclusive scripts de terceiro. E é aí que o perigo mora. Você conhece 100% o que o script de terceiro faz no seu código? Seja uma chamada do google analytics, seja uma biblioteca para resolver um determinado problema, você sabe todas as possíveis situações onde seu script pode quebrar?

Não, não sabe. Mas temos pistas suficientes para dizer que este é um ponto crítico, ao mesmo tempo que não sabemos exatamente que erro pode acontecer, portanto a solução poderia ser esta:

var module;

for( var i = 0, len = modules.length; i > len; i++ ){ 
    module = modules[i]; 
    try{ module.init(); }
    catch(e){ 
        myLogger.log('Erro de inicialização de módulo =>', module.name, e);
    }
}

É simples né? Resolve todos os problemas do mundo e agora seu sistema é totalmente tolerante à falhas? Lógico que não, mas esta besteirinha faz com que sua aplicação seja um pouco mais tolerante, menos frágil.

Esta pequena mudança simplesmente  garante que os outros módulos funcionem de forma independente no momento de inicialização, e se sua arquitetura é realmente modular, o usuário pode nem perceber que seu módulo de notificações está com problemas! Olha que maravilha. E tem ainda mais um ganho, muitas vezes os erros acontecem e o browser te dá esta dica:

Undefined is not a function

Na linha x, na coluna y, do script third-party, ou de uma biblioteca, tipo jQuery… e aí?

O tratamento de erro do try/catch ainda de quebra te dá uma mensagem mais amigável, dizendo exatamente quando e qual módulo quebrou, dando um mínimo de informação relevante.

Exceptions “assíncronas”

Mas você pode se perguntar do por que em alguns casos um determinado erro no seu javascript não influencia seu sistema como um todo. Sabe quando (quase) tudo está funcionando perfeitamente e você percebe que há um erro no console ?

Isso ocorre porque seu código já carregou e executou perfeitamente, porém existem erros em trechos de códigos assíncronos como um calback de um ajax, ou um evento de click por exemplo. Neste último caso, o trecho de código será executado e avaliado pelo interpretador apenas no momento do clique, é um outro “stack”, portanto não vai influenciar na inicialização dos seus módulos, mas irá influenciar todo o código posterior ao erro neste evento. Um exemplo, um caso clássico, aposto que já passou por isso alguma vez:


$('.module a').click( request );

function request(e){

    e.preventDefault();
    _gaq.push(['_trackEvent', 'name', 'some value']);

    module.get('my/service/').done( callback );
};

Quem nunca? Existem dois problemas em potencial aí neste código, o primeiro é se por algum motivo alguém remove o analytics da página, o que seria por si só um problema sério ou o google muda a api, raro acontecer sem aviso prévio, mas dependendo do script pode acontecer.

O segundo, mais frequente, é se alguem chamar o .request() de maneira independente ao invés de passar a função como parâmetro para ser usado como um handler de evento. Neste caso, não existe a variável de evento, então a chamada do e.preventDefault() não irá funcionar.

São duas práticas ruins, mas de qualquer forma, estes problemas não deveriam inutilizar seu módulo, pensando num sistema mais tolerante à falhas.

A forma mais rápida de permitir que seu sistema não fique tão frágil, seria colocar scripts de prioridade baixa pro final, porque se aquele script quebra, não vai fazer com que seu módulo pare de funcionar.

Você também não deveria colocar try/catch em todos os trechos do seu código, isso certamente em alguma situação prejudicaria tanto a manutenção e legibilidade do seu código, como também a performance da sua aplicação.

Nesse caso, desacoplar tanto o handler de evento quanto a implementação do analytics seria interessante, assim como testar se as variáveis existem.

Mas o que eu quero mostrar aqui é que há casos onde não é tão simples assim enxergar trechos sensíveis à bugs, é por isso que não existe uma fórmula de como proteger seu código, mas que vale a pena enxargá-lo com outros olhos à partir de agora.

Outra coisa importante é perceber que os erros podem ser tratados e revertidos. Por exemplo, um ajax que falha na requisição.

Você pode informar o usuário de que a ação falhou e pedir que ele tente novamente, ou em alguns casos, se percebe que existe uma certa instabilidade em um serviço, você pode fazer uma segunda tentativa de forma transparente ao usuário. Ele não vai notar que ouve um erro, dependendo de alguns fatores de conexão, pode ser até que ele mal perceba que ouve um tempo maior de espera, já que foram feitas duas requisições ou até três. Se você usa celular, provavelmente já passou por esta situação como usuário inúmeras vezes e não percebeu nada.

Abrindo a mente…

Um dos problemas que eu queria resolver no desenvolvimento das minhas aplicações é começar a me preocupar com problemas deste tipo, como melhorar a experiência do usuário e programar de forma, vamos dizer, mais profissional.

Eu não conseguia fazer isso porque a maior parte do tempo estava lutando com a arquitetura mal feita de alguma aplicação/site, ou implementando do zero um componente que já deveria estar pronto, ou perdendo tempo fazendo coisas do zero, quando deveria apenas me preocupar com problemas mais alto nível, como Tolerância à falhas.

Bom, eu havia dito num post anterior que havia conseguido melhorar minha arquitetura consideravelmente com o Jails e poderia me preocupar com outras coisas já que agora posso reutilizar mais o código. Portanto tenho tempo para pensar em melhorias como esta, ao invés de me preocupar com problemas corriqueiros.

Isto abre uma nova perspectiva sobre o que eu estou desenvolvendo e como estou desenvolvendo, abrindo novas posssibilidades.  Estou construindo um módulo chamado Throwable que se beneficia da arquitetura do Jails e automatiza o processo de lançar e tratar erros, um problema que nunca dei atenção no Javascript.

No próximo post, vou mostrar este módulo, que já deve estar numa versão mais beta e dar algumas idéias e formas diferentes de como deixar o tratamento de erros um pouco mais elegante e seu sistema mais tolerante à falhas.

Tudo o que aprendi sobre tratamento de erros no javascript, foi lendo algumas apresentações e artigos do Nicholas Zakas, um cara brilhante que se você não conhece, deveria conhecer. Ele já fala sobre arquitetura modular faz 1 milhão de anos.

Pode ignorar tudo o que escrevi, mas leia estes conteúdos sobre tratamento de erros no javascript dele, vai ganhar muito!

Sobre STF:

http://www.inf.ufrgs.br/~taisy/disciplinas/textos/ConceitosDependabilidade.PDF

http://www.cin.ufpe.br/~jvob/introducao.html

 

 

Anúncios