WCF – Gerenciamento de Instâncias


O gerenciamento de instância é uma técnica que é utilizada pelo WCF ou qualquer outra tecnologia de computação distribuída que determina como e por quem as requisições dos clientes serão atendidas. A escolha do modo de gerenciamento de instâncias interfere diretamente na escalabilidade, performance e transações de um serviço/componente, além de termos algumas mudanças à nível de implementação de contrato, que precisamos nos atentar para garantir que o mesmo funcione sob o modelo de gerenciamento escolhido.

A finalidade do artigo consiste, basicamente, em mostrar cada uma das três técnicas disponíveis pelo WCF mas, também, abordando os seus respectivos benefícios e algumas técnicas que circundam esse processo e que, de alguma forma, estão ligadas e influenciam na escolha e/ou implementação. O artigo também abordará os cuidados que devemos ter na escolha e implementação de cada uma das técnicas fornecidas.

Em primeiro lugar, é importante dizer que a escolha do modo de gerenciamento de instância não compromete diretamente o desenho do contrato do serviço. A escolha do modo aplica-se à classe de implementação do serviço que, por sua vez, faz o uso (implementação) da referida Interface. A escolha do modo de gerenciamento deverá ser realizada a partir de um behavior* de serviço pré-definido dentro do .NET Framework como um atributo, chamado de ServiceBehaviorAttribute.

* Em uma resposta curta, um behavior customiza a execução do serviço. Para ser mais específico, um behavior consiste em um pedaço de código que implementa uma determinada Interface (dependendo de onde o mesmo será aplicado) que permite ao runtime do WCF adicioná-lo no processo de execução e, conseqüentemente, executar o código customizado por você. Eles podem ser “plugados” via atributos ou arquivo de configuração. A criação de behaviors customizados está fora do escopo deste artigo.

Esse atributo possui uma propriedade chamada InstanceContextMode que recebe uma das opções fornecidas pelo enumerador InstanceContextMode. Entre as opções fornecidas por ele temos: PerSession, PerCall e Single. São essas três opções que abordaremos extensivamente a partir de agora.

PerSession

O modo PerSession que, quando omitido é o modo utilizado pelo WCF, cria uma instância da classe que corresponde ao serviço para cada cliente. A ativação é realizada quando o primeiro método é invocado por ele e a instância ficará ativa, respondendo à todas as requisições subseqüentes até que o cliente feche a conexão, que tipicamente acontece quando o método Close do proxy (ClientBase) é invocado (lembrando que o método Dispose também invoca o método Close). Justamente pelo objeto ficar ativo entre a abertura e o fechamento, ele é tipicamente referido como Session do lado do cliente e como Private Session do lado do serviço.

Cada instância ativa do lado do serviço correspondente à uma instância ativa do proxy que está do lado do cliente, ou seja, se um único cliente criar cinco instâncias do mesmo proxy, mesmo que ele aponte para o mesmo serviço, será criada uma classe para cada um deles do lado do serviço, para atender as respectivas chamadas, não podendo uma Session ser compartilhada com outra, mesmo sendo oriunda do mesmo cliente.

Mesmo que o cliente crie várias threads, utilizando a mesma instância do proxy para fazer várias chamadas, o serviço conseguirá atender somente uma única thread e, sendo assim, as outras threads terão que aguardar a liberação para serem processadas. É possível mudarmos esse comportamento utilizando as técnicas de concorrências fornecidas pelo WCF, mas que estão fora do escopo deste artigo.

O trecho de código abaixo ilustra como podemos aplicar o atributo ServiceBehaviorAttribute na classe de implementação do serviço, definindo ele como sendo PerSession mas, se omitirmos o atributo, o comportamento será o mesmo pois, como dito acima, é a configuração padrão do WCF:

using System;
using System.ServiceModel;

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class ServidoDeClientes : IClientes
{
    private Dictionary<int, string> _clientes;

    //Construtor

    public int Adicionar(string nome)
    {
        int key = this._clientes.Count + 1;
        this._clientes.Add(key, nome);

        return key;
    }
}

Como podemos notar, a classe está definida como PerSession. Neste caso, como a instância ficará ativa enquanto durar o proxy, podemos armazenar os dados entre as requisições na própria classe, ou seja, em seus membros internos, como é o caso do objeto _clientes. Quando o proxy for encerrado, ele notificará o serviço que a sessão foi finalizada e, caso o mesmo implemente a Interface IDisposable, o método Dispose será invocado*.

* Como o método Dispose será invocado assíncronamente, quer dizer, em uma worker thread, o contexto da operação não será propagado para dentro do método Dispose e, sendo assim, o mesmo não pode depender destas informações contextuais.

Mas para que o suporte a sessão funcione corretamente, precisamos analisar sob qual binding a classe será exposta. Para que seja possível correlacionar as mensagens vindas de um determinado cliente para uma determinada instância, o WCF precisa ser capaz de identificar o cliente e, quando utilizamos bindings que fornecem isso nativamente através do protocolo (como é o caso do TCP ou IPC), não há problema, pois eles mantém uma conexão contínua, conhecida também como sessão à nível de transporte e, com isso, pode-se facilmente identificar quem é o cliente. Pela natureza do protocolo HTTP isso já é um pouco mais complexo, pois a cada chamada uma nova conexão é realizada e, com isso, o BasicHttpBiding não suporta Sessions; já o WSHttpBinding é capaz de emular a sessão à nível de transporte, incluindo um SessionID nos headers da mensagem que identifica unicamente o cliente, muito semelhante ao que acontece com o ASP.NET.

A SessionID é representada por uma GUID e tanto o cliente quanto o serviço são capazes de recuperá-la para que ela possa ser utilizada durante a execução, para alguma determinada tarefa, bem como para efeitos de depuração/logging. O WCF (assim como funcionava também no COM+) fornece uma classe contendo informações contextuais (falaremos mais sobre contexto abaixo), que estão relacionadas à chamada corrente que, além de trazer informações a respeito de transações e segurança, informa também a SessionID. Essa informação é acessada de forma diferente no cliente em relação ao servidor. O trecho de código abaixo ilustra como proceder para recuperar tal informação em ambos os lados:

//Serviço
Debug.WriteLine(OperationContext.Current.SessionId);

//Cliente
Debug.WriteLine(proxy.InnerChannel.SessionId);

Ainda há mais um tipo que precisamos nos atentar. Trata-se do enumerador SessionMode que pode ser atribuído à propriedade SessionMode do atributo ServiceContractAttribute que é aplicado na Interface de contrato do serviço. Essa informação será disponibilizada no contrato do serviço, para que o runtime do WCF saiba que ele deve utilizar uma Session. Esse enumerador fornece três opções, que estão descritas abaixo:

  • Allowed: Especifica que o contrato suporte sessão desde que o binding também suporte. Por padrão, esta opção é definida nos contratos quando a mesma não estiver explícita.
  • NotAllowed: Determina que o contrato não aceite bindings que iniciem sessões.
  • Required: Especifica que o contrato necessite de um binding que suporte sessões e, caso isso não aconteça, uma exceção do tipo InvalidOperationException será lançada durante a carga do serviço.

Como pudemos notar, a finalidade deste enumerador é garantir que o contrato será exposto a partir de um binding que suporte (ou não) as Sessions. A configuração padrão determina a propriedade SessionMode para Allowed e, com isso, se estiver utilizando algum binding que não suporte sessões, como é o caso do BasicHttpBinding, nenhuma exceção será lançada, mas ele não se comportará como o esperado, ou seja, não conseguirá manter o estado entre as chamadas.

Já quando escolhemos a opção NotAllowed e disponibilizamos o contrato a partir de um binding que, nativamente, traz suporte a sessões, como é o caso do TCP, então uma exceção será lançada durante a carga do serviço. Finalmente, a opção Required obriga que o contrato seja exposto sob um binding que suporte sessões e, caso não suporte, assim como a opção anterior, uma exceção será lançada durante a carga do serviço. O trecho de código abaixo ilustra como definir essa propriedade no contrato do serviço:

using System;
using System.ServiceModel;

[ServiceContract(SessionMode = SessionMode.Required)]
public interface ICliente
{
    [OperationContract]
    int Adicionar(string nome);
}

Ordem de Execução

Quando utilizamos contratos que suportam sessão, às vezes precisamos determinar a ordem de execução, garatindo assim que um método seja capaz de inicializar a sessão; já outro método poderá somente ser executado caso a sessão já esteja devidamente criada, e ainda há a possibilidade de um método encerrar a sessão.

Para termos um controle sobre isso, utilizamos duas propriedades chamadas IsInitiating e IsTerminating do atributo OperationContractAttribute. Essas propriedades irão demarcar o limite da sessão dentro do serviço e uma consistência é realizada durante a carga do mesmo para garantir que ele esteja com a opção SessionMode definida como Required. Por padrão, a propriedade IsInitiating é definida como True e IsTerminating como False, permitindo que qualquer método inicie a sessão. O interessante é que esta técnica evitará que você faça consistências dentro do método para saber se a sessão já está ou não ativa, pois o próprio WCF garante isso, uma vez que você está tentando acessar um método que não permite inicializar a sessão e a mesma ainda não esteja criada.

Para exemplificar, vamos imaginar que temos um contrato que define algumas operações típicas para um comércio eletrônico. As operações consistem em identificar o cliente a partir de um login, possibilidade de adicionar produtos, recuperar o valor total dos produtos e, finalmente, um método para finalizar a compra. O código abaixo ilustra como devemos fazer para configurar esses métodos dentro da Interface para garantir a ordem das chamadas:

using System;
using System.ServiceModel;

[ServiceContract(SessionMode = SessionMode.Required)]
public interface IComercioEletronico
{
    [OperationContract(IsInitiating = true)]
    bool EfetuarLogin(string nome);

    [OperationContract(IsInitiating = false)]
    void AdicionarProduto(int codigoDoProduto, int quantidade);

    [OperationContract(IsInitiating = false)]
    decimal RecuperarValorTotal();

    [OperationContract(IsInitiating = false, IsTerminating = true)]
    void FinalizarCompra();
}

Apesar da propriedade IsInitiating ser definida, por padrão, como True, não seria necessário especificá-la no método EfetuarLogin; já os dois métodos intermediários, AdicionarProduto e RecuperarValorTotal não permitem inicializar ou finalizar a sessão (lembre-se de que a propriedade IsTerminating por padrão é sempre False) e, finalmente, o método FinalizarCompra encerra a sessão, pois define IsInitiating como False e IsTerminating como True.

Com este contrato devidamente implementado podemos, do lado do cliente, criar a referência para o serviço, instanciar o proxy e consumir os métodos. Caso você tente invocar um método que não crie a sessão antes de invocar o método que tem essa finalidade, você receberá uma exceção do tipo InvalidOperationException, informando que o método que está tentando invocar somente poderá ser disparado depois da sessão criada. O trecho de código abaixo mostra como invocar os métodos em sua seqüência correta (repare que isso não muda em nada o código cliente):

using (ComercioEletronicoClient proxy = new ComercioEletronicoClient())
{
    proxy.EfetuarLogin("Israel Aece");

    proxy.AdicionarProduto(12, 4);
    proxy.AdicionarProduto(12, 3);
    proxy.AdicionarProduto(3, 3);

    Console.WriteLine(proxy.RecuperarValorTotal());

    proxy.FinalizarCompra();
}

Se tentar chamar qualquer método antes do método EfetuarLogin, uma exceção será lançada, dizendo que isso não é possível, obrigando você a invocar os métodos na ordem correta. Além disso, é importante dizer que como o método FinalizarCompra está com a propriedade IsTerminating definida como True, ao invocá-lo, o WCF removerá a instância corrente, invalidando o proxy. Caso você necessite fazer novas chamadas, você terá que criar um novo proxy.

Apesar desta técnica estar sendo abordada dentro da seção que discute o modo PerSession, ela pode também ser adotada em um serviço que é exposto através do modo Single, que será abordado mais abaixo.

Desativação da Instância

O modelo PerSession permite que várias mensagens estejam correlacionadas à uma mesma instância, mantendo o objeto ativo do lado do serviço, atendendo às requisições de um determinado proxy. Na verdade as instâncias estão contidas dentro de um contexto e, quando uma sessão é criada, o host cria um novo contexto para acomodar a instância dentro dele. A imagem abaixo ilustra graficamente como esse processo acontece:

Figura 1 – A relação entre o contexto e a instância do serviço.

Como podemos notar, a instância sempre estará contida dentro de um contexto que, por sua vez, tem o mesmo tempo de vida do objeto. Entretanto, em um ambiente onde você quer ter um maior controle em relação ao gerenciamento dos contextos e também das instâncias, talvez por questões de performance ou mesmo de consistência, o WCF desacopla o gerenciamento da instância em relação ao contexto onde ela reside e, com isso, você poderá definir quando descartar a instância (antes ou depois da chamada do método), mas tendo ainda o acesso ao contexto corrente, algo que não era possível antes. Esse controle é útil em momentos em que o método utiliza recursos valiosos, onde não se pode esperar até que a sessão finalize para que eles sejam liberados.

Esse controle pode ser realizado de forma imperativa ou declarativa. Isso quer dizer que podemos, no interior de um método, invocar um método do WCF que libera a instância ou, da forma declarativa, onde podemos decorar os métodos que fazem parte do serviço a descartar a instância antes ou depois da execução do mesmo. Para a primeira forma, recorremos ao método ReleaseServiceInstance da classe InstaceContext, como mostra o exemplo de código abaixo:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class ServicoDeComercioEletronico : IComercioEletronico
{
    //Outros métodos

    public void FinalizarCompra()
    {
        //finalizar a compra
        OperationContext.Current.InstanceContext.ReleaseServiceInstance();
    }
}

Já no modelo declarativo, recorremos ao atributo OperationBehaviorAttribute. Este atributo fornece uma propriedade chamada ReleaseInstance que aceita os dois itens contidos dentro do enumerador ReleaseInstanceMode, que dizem ao WCF que a instância deve ser reciclada em um ponto específico do processo. As opções deste enumerador são abordadas abaixo:

  • None: É a opção padrão que recicla o objeto de acordo com o valor definido na propriedade InstanceContextMode (PerSession, PerCall e Single).
  • BeforeCall: Recicla o objeto antes da chamada da operação.
  • AfterCall: Recicla o objeto depois da chamada da operação. Semelhante ao método ReleaseServiceInstance.
  • BeforeAndAfterCall: Recicla o objeto antes e depois da chamada da operação. Esta opção é muito semelhante aos serviços configurados como PerCall.

Abaixo o mesmo exemplo que vimos acima, só que agora no modo declarativo. É importante notar que em ambos os casos essas configurações são realizadas na classe que implementa o contrato:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class ServicoDeComercioEletronico : IComercioEletronico
{
    //Outros métodos

    [OperationBehavior(ReleaseInstanceMode = ReleaseInstanceMode.BeforeCall)]
    public void FinalizarCompra()
    {
        //finalizar a compra
    }
}

Determinar qual das três opções de sessão escolher (Allowed, NotAllowed ou Required), dependerá muito do que o teu contrato se propõe a realizar. Serviços configurados como PerSession tem um alto custo e não escalam bem em relação aos serviços configurados como PerCall. Isso acontece porque enquanto o proxy permanecer ativo do lado do cliente, a classe correspondente que serve as suas requisições também ficará ativa do lado do serviço. Caso as chamadas forem feitas com bastante freqüência, manter o proxy pode ser uma boa solução; já quando as chamadas forem esporádicas ou se precisar de escalabilidade ou consistência, então o melhor é recorrer ao modo PerCall, que será abordado logo abaixo.

PerCall

Enquanto a característica de serviços configurados como PerSession é manter o objeto ativo do lado do serviço enquanto existir o proxy do lado do cliente, o PerCall é o oposto. Muitas vezes os clientes prendem a referência para o objeto por um longo período de tempo e, na verdade, fazem o uso dele em poucos momentos. Isso fará com que os objetos consumam memória desnecessária do lado do serviço, sem ao menos ser utilizada.

Serviços configurados como PerCall criarão uma nova instância do serviço a cada chamada para qualquer operação. Quando o método retornar, o objeto será removido da memória, existindo apenas durante a execução do método solicitado pelo cliente. Para classes que implementam a Interface IDisposable, momentos antes da classe ser removida, o método Dispose será disparado mas, ao contrário do modelo PerSession, o método Dispose será executado na mesma thread da operação e, conseqüentemente, você ainda terá acesso ao contexto.

Neste modo de gerenciamento, você precisa se atentar à alguns detalhes que mudam consideravelmente em relação ao modo PerSession. O primeiro deles é com relação a concorrência; como cada chamada criará uma nova instância dedicada, então você não precisará lidar com ambiente multi-threading, pois ele nunca irá acontecer. Outro detalhe importante é com relação ao gerenciamento de estado. Como a instância que servirá a requisição será destruída quando o método retornar, não será possível que você guarde alguma informação entre as requisições e, caso queria, você precisará criar e gerenciar um repositório próprio, além do que precisará também identificar unicamente o cliente a cada chamada e, na maioria das vezes, isso influencia em uma mudança no contrato do serviço.

Seguindo o mesmo exemplo que utilizamos no modo PerSession, mudaremos ligeiramente a classe ServidoDeClientes para, neste momento, utilizar o modelo PerCall. Tudo o que precisamos é definir a propriedade InstanceContextMode do atributo ServiceBehaviorAttribute para PerCall e tudo já funcionará com este novo comportamento:

using System;
using System.ServiceModel;

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class ServidoDeClientes : IClientes
{
    private Dictionary<int, string> _clientes;

    //Construtor

    public int Adicionar(string nome)
    {
        int key = this._clientes.Count + 1;
        this._clientes.Add(key, nome);

        return key;
    }
}

Como já é de se esperar, a cada chamada para o método Adicionar, ele sempre criará uma nova instância da classe e, conseqüentemente, todo o conteúdo será recriado, não mantendo as informações entre as solicitações. Como dito acima, caso você precise manter alguma informação entre as requisições, você precisará inicializar essas informações a partir de algum repositório, como banco de dados, sistema de arquivos ou até mesmo variáveis estáticas.

Single

Já no modelo Single (Singleton) o comportamento é muito diferente em relação às duas opções que vimos acima. A idéia deste modelo é ter uma única instância servindo à todas as requisições que chegarem para este serviço, independente de qual endpoint venha a requisição. Este objeto viverá enquanto o host que a hospeda viver.

Como uma das principais vantagens, o modo Single não cria para todo cliente ou para toda chamada uma nova instância do objeto mas, em contrapartida, há algumas desvantagens que são um pouco difícies de lidar. O primeiro caso é com relação à concorrência: como você tem múltiplos clientes acessando a mesma instância, objetos contidos dentro dela também poderão ser acessados ao mesmo tempo e, se não se preocupar com a forma de acessá-los, poderá ter problemas no resultado final do processo ou até mesmo deadlocks. Outro problema conhecido mas que não se pode fazer muito a respeito, justamente pela necessidade de ter um único objeto fornecendo todas as requisições, é que ela (instância) ficará ativa mesmo quando não há nenhuma tarefa para ela executar e, dependendo da quantidade de recursos que a mesma armazena, poderemos ter problemas futuros.

Para utilizar o modo Single, assim como os outros, devemos recorrer ao atributo ServiceBehaviorAttribute, definindo a propriedade InstanceContextMode como Single. Mas é importante ressaltar que, para membros internos, será necessário que você entenda os possíveis problemas que podem acontecer pelo fato de múltiplos clientes fazerem requisições simultâneas, e isso exige uma mudança na forma de programar e/ou acessar os recursos internos da classe. A concorrência não é tratada aqui, pois precisa de um artigo a parte para abordar isso. O código abaixo ilustra o mesmo exemplo (ServidoDeClientes), mas utilizando o modo Single:

using System;
using System.ServiceModel;

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class ServidoDeClientes : IClientes
{
    private Dictionary<int, string> _clientes;

    //Construtor

    public int Adicionar(string nome)
    {
        int key = this._clientes.Count + 1;
        this._clientes.Add(key, nome);

        return key;
    }
}

A classe ServiceHost, que é responsável pelo gerenciamento da instância da classe de serviço, possui alguns overloads do construtor que, em uma das versões, aceita uma instância do tipo (Type) do serviço; neste caso o ServiceHost é responsável por instanciar a classe, invocando o construtor padrão da mesma. Já em outra versão do construtor do ServiceHost, ele recebe um parâmetro do tipo object, permitindo que você instancie a classe de implementação do serviço. Isso é útil quando a implementação do serviço necessita de algumas informações adicionais (via construtor ou propriedades) para poder trabalhar. Abaixo há um exemplo que mostra essa técnica:

ServicoDeClientes srv = new ServicoDeClientes("connection_string");
srv.NumeroDeTentativas = 5;

using(ServiceHost host = new ServiceHost(srv, 
    new Uri[] { new Uri("net.tcp://localhost:9292") }))
{
    //...
}

Conclusão: Este artigo mostrou as três formas que temos para gerenciar a instância de uma classe que serve as requisições. É importante analisar as vantagens e desvantagens de cada uma delas para decidir qual delas é a ideal para o serviço que está sendo desenvolvido. Além disso, a escolha do modo de gerenciamento interfere diretamente no modo de escrita de código, justamente para possibilitar que o serviço se comporte bem em um ambiente multi-threading, já que os modos PerSession e Single permitem que isso aconteça.

GerenciamentoDeInstancias.zip (636.53 kb)

Anúncios

Um comentário sobre “WCF – Gerenciamento de Instâncias

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s