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);
    }
}

Propagando Transações

Como já sabemos, desde a versão 2.0 o .NET Framework possui uma API para criar e gerenciar transações dentro da aplicação. É o assembly System.Transactions.dll que disponibiliza um conjunto de APIs que podemos criar blocos transacionais, e assim alistar recursos que precisam de “proteção” para garantir que tudo seja efetivado ou desfeito caso algum problema ocorra (independente se o problema for de infraestrutura ou regra de negócio).

A principal classe que temos para isso é a TransactionScope, que ao ser envolvida em um bloco using, no método Dispose ela realiza o commit ou o rollback, dependendo se o método Complete foi ou não chamado. É dentro deste escopo criado que o .NET gerencia, e dependendo do que está envolvido nele, ele utiliza uma transação local (LTM) ou escala para uma transação distribuída (DTC).

Aqui estamos falando de um mesmo processo que é responsável por criar a transação, alistar os recursos, e por fim, finalizar a mesma (com sucesso ou falha). Mas como fazer quando a transação é iniciada pelo processo A e precisamos envolver as tarefas executadas por outro processo (B)? Ainda dentro deste mesmo assembly, temos uma classe chamada TransactionInterop, que expõe alguns métodos para “expandir” a transação entre processos.

Basicamente ele fornece métodos que geram uma chave e métodos que permitem recriar a transação a partir da mesma. Isso nos permitirá na aplicação A gerar a chave e importar na aplicação B, e com isso, ambas aplicações estarão envolvidas na mesma transação. É através do método estático GetTransmitterPropagationToken que geramos um array de bytes que representa o token de identificação da transação. Para exemplificar, vamos gerar este token a partir da aplicação A e armazenar este token um arquivo no disco para compartilhar com a aplicação B. No código abaixo estou removendo os logs para poupar espaço.

//Aplicação A
using (var ts = new TransactionScope())
{
    File.WriteAllBytes(@”C:TempTransactionToken.bin”,
        TransactionInterop.GetTransmitterPropagationToken(Transaction.Current));

    ts.Complete();
}

Do outro lado, utilizamos o método (também estático) GetTransactionFromTransmitterPropagationToken da classe TransactionInterop. Ele recebe como parâmetro um array de bytes (que estamos extraindo do arquivo) e recria a transação. Aqui o detalhe importante é reparar que o resultado deste método está sendo passado diretamente para um dos construtores da classe TransactionScope, que faz uso desta transação (criada pela outra aplicação) e envolve nela tudo o que está em seu próprio bloco using.

//Aplicação B
using (var ts =
    new TransactionScope(
        TransactionInterop.GetTransactionFromTransmitterPropagationToken(
            File.ReadAllBytes(@”C:TempTransactionToken.bin”))))
{
    ts.Complete();
}

Se repararmos as imagens abaixo, notamos que na a aplicação A possui o identificador de transação distribuída (DI) zerado, e quando a aplicação B se envolve na mesma transação, o identificador da transação distribuída na aplicação A reflete o mesmo identificador da aplicação B, ou seja, ambas aplicações estão envolvidas na mesma transação. Por fim, é possível ver na última imagem o log do DTC, indicando que a mesma foi promovida à uma transação distribuída.

Curiosidade: sabemos que o WCF fornece suporte à transações em serviços, podendo o cliente iniciar a mesma e o serviço participar. O id da transação viaja do cliente para o serviço através de headers do envelope SOAP, e quando o serviço recebe a informação, ele recria a transação utilizando os recursos (classes e métodos) que vimos neste artigo.

Propagando Transações em Métodos Assíncronos

Desde a versão 2.0 do .NET Framework existe um assembly chamado System.Transactions.dll. Dentro deste assembly há diversos tipos para trabalharmos com transações dentro de aplicações .NET. Basicamente passamos a ter o controle, através de uma linguagem .NET, de um ambiente transacionado, em que podemos delimitar o escopo e decidir quando e onde queremos efetivar (commit) ou desfazer (rollback) as alterações .

Através deste mesmo conjunto de classes, temos a possibilidade de alistar vários tipos de recursos, e entre eles temos base de dados relacionais, filas de mensagens (message queue) e até mesmo, com algum trabalho, recursos voláteis. Além disso, este mecanismo é inteligente o bastante para determinar quando ele precisa apenas de uma transação local (quando envolve apenas um resource manager), e quando há mais que um envolvido, ele é capaz de escalar para uma transação distribuída de forma automática.

Apesar de funcionar bem, alguimas complicações começam a aparecer quando estamos trabalhando com aplicações assíncronas, mas que compartilham do mesmo escopo transacionado. O que quero dizer aqui é que uma vez que o escopo está criado (TransactionScope), se envolvermos chamadas à outros códigos de forma assíncrona, esperando que a transação seja propagada para essas outras threads, teremos um comportamento não desejado. Isso se deve ao fato de que, por padrão, a transação não é (automaticamente) propagada para essas outras threads que estão fazendo um trabalho complementar ao principal.

Para resolvermos este problema até então, devemos recorrer à classe DependentTransaction. Como o próprio nome sugere, esta classe é um clone da transação que rege o ambiente criado pelo TransactionScope, e garante que escopo principal não possa ser concluído antes que todos os trabalhos que estão sendo executados paralelamente estejam finalizados. A criação desta classe se dá através do método DependentClone da classe Transaction, que como parâmetro recebe uma das opções expostas pelo enumerador DependentCloneOption. No exemplo abaixo utilizaremos a opção BlockCommitUntilComplete, que garantirá que que transação não seja efetivada até que o trabalho dentro do método Metodo1 seja finalizado. O que sinaliza ao coordernador que o trabalho assíncrono foi concluído é a chamada para o método Complete da classe DependentTransaction.

private static void Executar()
{
    using (var scope = new TransactionScope())
    {
        LogTransactionInfo(“Main”);

        ThreadPool.QueueUserWorkItem(
            Metodo1,
            Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete));

        scope.Complete();
    }
}

private static void Metodo1(object root)
{
    using (var dependentTransaction = root as DependentTransaction)
    {
        using (var scope = new TransactionScope(dependentTransaction))
        {
            LogTransactionInfo(“Metodo1”);

            scope.Complete();
        }

        dependentTransaction.Complete();
    }
}

Se avaliarmos o log do identificador da transação, veremos que ambos possuem o mesmo ID:

Main: 5bb899d4-3428-42ce-9c45-d498900be040:1
Metodo1: 5bb899d4-3428-42ce-9c45-d498900be040:1

Como o .NET Framework em conjunto com as linguagens estão tentando tornar a construção de aplicações assíncronas mais simples, a Microsoft incluiu na versão 4.5.1 do .NET Framework um novo construtor na classe TransactionScope que aceita uma das duas opções expostas pelo enumerador TransactionScopeAsyncFlowOption. A opção Enabled que é utilizada abaixo indica ao .NET que o escopo transacionado deve ser propagado para os métodos assíncronos que são invocados dentro dele. Isso facilita a codificação, pois podemos tornar o código mais legível, sem a necessidade de ficar controlando detalhes de infraestrutura, e isso pode ser comprovado através do exemplo abaixo. O enumerador TransactionScopeAsyncFlowOption também possui a opção Supress, que é a configuração padrão e é indica que o contexto transacionado não seja propagado.

private async static Task Executar()
{
    using(var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        LogTransactionInfo(“Main”);

        await Metodo1();

        scope.Complete();
    }
}

private async static Task Metodo1()
{
    LogTransactionInfo(“Metodo1”);

    await Task.Delay(100);
}

E, finalmente, como já era de se esperar, os IDs das transações são idênticos, tanto na thread principal quanto na worker thread:

Main: d7f86e15-8bf8-4472-aeb7-be661a2c5703:1
Metodo1: d7f86e15-8bf8-4472-aeb7-be661a2c5703:1