Exceções dentro de Recursos Voláteis

O .NET Framework já fornece nativamente suporte para a criação de contextos transacionais através da classe TransactionScope. Além da possibilidade de alistar automaticamente recursos como base de dados, message queue, etc., é possível customizar e alistar também recursos voláteis, onde podemos customizar e proteger algum código que queremos que seja efetivado quando ocorrer o commit, ou desfeito se o rollback ocorrer.

Para essa customização, basta implementarmos a interface IEnlistmentNotification, que fornece os métodos específicos e permite ao recurso interceptar cada um dos momentos definidos pelo protocolo 2PC (two phase commit): prepare, commit e rollback. Uma implementação simples desta interface é exibida abaixo. É importante notar que dentro do construtor já avaliamos se a classe está sendo instanciada dentro de um ambiente transacionado, e se sim, já alistamos o recurso para ser notificado quando os respectivos eventos ocorrer.

public class Teste1 : IEnlistmentNotification
{
    public Teste1()
    {
        if (Transaction.Current != null)
            Transaction.Current.EnlistVolatile(
                this, EnlistmentOptions.EnlistDuringPrepareRequired);
    }

    public void Commit(Enlistment enlistment) { }

    public void InDoubt(Enlistment enlistment) { }

    public void Prepare(PreparingEnlistment preparingEnlistment) { }

    public void Rollback(Enlistment enlistment) { }
}

Os métodos são autoexplicativos. Mas vamos focar no método Prepare. Este método é disparado durante a primeira fase do protocolo 2PC, onde o gerenciador interrogando o recurso se pode ou não realizar o commit. É importante falarmos sobre o disparo de exceções aqui. É importante envolvermos todo o código que está dentro deste método em um bloco try/catch, pois se houver uma exceção não tratada aqui, isso fará com que o .NET aborte os métodos de rollbacks de outros recursos que estejam também alistados. Imagine que o método Rollback acima esteja implementado da seguinte forma:

public void Prepare(PreparingEnlistment preparingEnlistment)
{
    throw new Exception("Erro no Teste1.");
}

Para provar que o problema de fato ocorre, vamos criar uma segunda classe que implemente esta mesma interface, e em seu método Rollback, simplesmente vamos escrever uma mensagem na tela. Conside o código abaixo, que instancia as duas classes (ambas implementam a interface em discussão aqui), e quando o método Prepare é chamado sobre o objeto teste1 disparando a exceção não tratada, o .NET aborta o resto e, consequentemente, o método Rollback do objeto teste2 não é disparado.

using (var transaction = new TransactionScope())
{
    var teste1 = new Teste1();
    var teste2 = new Teste2();

    transaction.Complete();
}

O comportamento que esperamos não é este, ou seja, temos também que garantir que o Rollback seja disparado com sucesso nos demais objetos que estão a seguir e fazem parte do mesmo contexto transacional. Para resolvermos isso, basta proteger o código do método Prepare em um bloco try/catch, e se algum problema ocorrer, basta chamar o método ForceRollback, indicando ao runtime do .NET que a transação deve ser desfeita. E ainda, se quiser prover informações adicionais sobre o problema ocorrido, basta capturar a exceção e informando-a para um overload deste mesmo método, que passa adiante o problema (via InnerException), protegendo toda stack trace e entregando a mensagem real do problema. Sendo assim, a implementação final é:

public void Prepare(PreparingEnlistment preparingEnlistment)
{
    try
    {
        throw new Exception("Erro no Teste1");

        preparingEnlistment.Prepared();
    }
    catch (Exception ex)
    {
        preparingEnlistment.ForceRollback(ex);
    }
}
Anúncios