Posts Marcados Concorrência

ORM e Concorrência

Olá pessoal! Há algum tempo atrás sofremos um pouco na manutenção de um dos grandes sistemas da White Fox. Quero compartilhar a experiência para que outros não tenham que passar pelas mesmas dificuldades!

Só relembrando, conforme meu antigo post sobre a camada de domínio, nós utilizamos ORM (nHibernate ou Microsoft Entity Framework) para fazer o mapeamento de entidades para o banco de dados, de modo a abstrair os mecanismos de persistência. Isto tem funcionado muito bem para nós – utilizamos ORM em praticamente todos nossos sistemas de maior porte há mais de 10 anos com sucesso. Claro que por mais que tentemos compartimentalizar, ao longo do tempo os domínios acabam crescendo. Hoje temos sistemas com domínios de mais de 300 objetos e milhares de linhas de código de regras de negócio.

Pois bem, neste sistema em particular, começamos a perceber há algum tempo, problemas de dados somente no ambiente de produção. Valores que aparentemente ficavam errados do nada. E, obviamente, nenhum destes problemas aparecia em homologação ou durante a execução de testes unitários. Claro que, como todo acidente, nenhum erro mais grave tem uma única causa. Neste caso em específico, também temos uma sequência de eventos que levaram à falha. Olhando agora, são até que óbvios, mas gastamos um bom tempo no diagnóstico e solução.

O primeiro componente começou com uma prática usual de ORM. Quase todos possuem uma proteção para evitar que a persistência ocorra em um registro que foi alterado por outrem. Por exemplo, se o ORM carrega um objeto em memória, faz nele alguma alteração e ao salvar, detecta que o registro no banco não é o mesmo de quando o objeto foi carregado, ele gera uma exceção. Porém, este mecanismo deixa tudo lento, já que para implementá-lo, o ORM acaba tendo que fazer uma query a cada UPDATE. E como usamos transações, os LOCK do banco de dados acabam garantido a atomicidade da operação, assim esta proteção acaba ficando redundante. No nosso caso ela sempre é desligada.

O segundo componente é a criação de campos para contadores ou totalizadores. Sim, claro, isto é algo que se deve evitar, especialmente como atributo de uma entidade de domínio. Por exemplo, colocar o total de uma nota fiscal como atributo ao invés de calcular o total através da soma de seus elementos. Usualmente evitamos isto, mas, em algumas entidades, calcular o valor toda hora pode ser complicado, seja porque existe alguma regra de negócio muito complexa envolvida ou um número muito grande de elementos para compor o total. Então, em algum momento, alguém resolve que manter o totalizador oferece uma melhor razão custo/benefício. Obviamente que se usam transações para manter os totais e se criam testes unitários para garantir que os totalizadores funcionam em todos os cenários.

O último componente do problema é a concorrência. Dificilmente teste unitários são criados para simular uma carga de múltiplos usuários simultâneos, pela complexidade de se simular este tipo ambiente. Assim, no teste unitário, quase não há concorrência. Porém, em produção os nossos sistemas são usados por centenas de usuários simultâneos. Apesar dito, tipicamente um usuário faz transações em uma única grande “entidade” por vez (p. ex., vendendo um produto). Assim, mesmo vários usuários em paralelo dificilmente mexem na mesma entidade, simultaneamente, ao mesmo tempo.

Mas as exceções é que fazem a coisa desandar. Se desligamos a proteção de dados alterados, usarmos totalizadores em entidade e usuários alteram esta mesma entidade quase ao mesmo tempo, temos o nosso problema acontecendo! Para detalhar, vejam a figura a seguir. Imaginem 2 processos executando em paralelo, em tempos muito próximos um do outro.

image

Em um momento 1, ambos carregam a mesma entidade em memória, uma delas com um atributo totalizador. Como neste momento ainda não houve nenhuma transação, ambos conseguem carrega-la simultaneamente, e ambas possuem o mesmo valor para o atributo. No momento 2, ambos fazem algum processamento em que vão atualizar o campo totalizador. No momento 3, o primeiro processo abre uma transação, salva os objetos e faz o COMMIT. O segundo processo tenta fazer o mesmo, porém como o primeiro fez o LOCK, ele é bloqueado e fica em espera. Se não cair por timeout, quando o primeiro processo acabar, ele é liberado, começa sua transação, salva os seus objetos e faz o seu COMMIT. Como não há proteção para alteração, ele não vai perceber que o objeto foi alterado pelo primeiro processo e vai completar a ação achando que tudo correu bem.

Mas percebam que o segundo processo partiu objetos com totalizadores incorretos! Como o ele carregou os objetos no mesmo momento que o primeiro processo, ele não vai contemplar as alterações feitas por ele e vai salvar um total incorreto. Um exemplo, ambos carregam um atributo com um valor total de 10, ambos somam 1, o primeiro salva 11, o segundo também vai salvar 11, incorretamente!!!

Uma vez diagnosticado, o problema também não é simples de resolver. Não há solução trivial com o uso de ORM. Soluções como ativar a proteção de alteração, usar LOCK pessimista ou usar singletons, possuem pontos extremamente negativos e foram rejeitadas por nós. No final, a solução que adotamos foi, para campos totalizadores, ignorar totalmente o ORM e ir direto ao banco. Fizemos isto com o uso de um repositório especialmente projetado para este fim e com o uso de stored procedures para garantir que as alterações sejam feitas com as proteções adequadas. Esta solução conseguiu até mesmo melhorar o desempenho da aplicação, pois evitamos a manipulação de totais pelo ORM e transferimos todo o trabalho para o banco. O ponto negativo é que o sistema fica bem mais complexo de manter e uma porção das regras de negócio saiu do domínio e foi para o banco. Mas de todos os males possíveis, este foi o que achamos de menor impacto.

Moral da história é: conheça seu ORM, evite a todo custo campos totalizadores e, se tiver que usá-los, não se esqueça que poderá ter graves problemas em um ambiente com concorrência.

Este e outros eventos tem nos feito repensar o uso de ORM como um absoluto. Acho que já está na hora de refazer minha série sobre produtividade, atualizando-a com as tecnologias e práticas que temos adotado nos últimos anos. Tudo muda e TI muda ainda mais rápido. Acho que até que conseguimos ter uma relativa estabilidade nos nossos ambientes por muito tempo. Mas obviamente a evolução é necessária e tem hora que mudar paradigmas é essencial para mantermos nossa produtividade. Mais sobre isto em breve!

Até a próxima!

, , ,

1 comentário