Posts Marcados Desenvolvimento

Arquitetura de Microservices utilizando o Microsoft SQL Service Broker – Parte 3 – Final

Olá! Antes de iniciar a última parte desta série, uma notícia boa: a White Fox se tornou parceiro Microsoft Silver em Application Development e Gold em Devices and Deployment. Além do reconhecimento, a parceria com a Microsoft é importante para dar uma visibilidade maior para a White Fox e nos permitir acesso a muito mais recursos para nosso processo de desenvolvimento. Obrigado a todos que nos ajudaram durante a certificação! Em 2016 a White Fox deve focar mais no Microsoft Azure, sempre buscando trazer o melhor da tecnologia para nossos clientes com a melhor relação custo/benefício.

No último post, mostramos a estrutura montada no SQL Service Broker para suportar nossa arquitetura de Microservices. Nesta última parte vamos mostrar as estruturas em .NET que suportam o Thin Client (cliente) e os executores. Como estamos montando uma arquitetura expansível, criamos então uma classe estática, utilizando um container Microsoft Unity, que chamamos de ServiceBus, para registrar cada cliente e executor. O código dela pode ser visto abaixo.

public static class ServiceBus {
	private static IUnityContainer iocCcontainer;
	private static bool isInitialized;

	public static void InitializeSql(string connectionStringName) {
		isInitialized = true;
		iocCcontainer = new UnityContainer();
		iocCcontainer.RegisterInstance(typeof(IStorage), new SqlStorage(connectionStringName));
	}

	public static T GetService<T>() where T: class, IService {
		if (!isInitialized) throw new ServiceBusNotInitializedException();
		return iocCcontainer.Resolve<T>();
	}

	public static void RegisterService<T>(Type type) where T : class, IService {
		if (!isInitialized) throw new ServiceBusNotInitializedException();
		iocCcontainer.RegisterType(typeof(T), type);
	}

	public static void RegisterComponent<T>(T instance) where T : class, IComponent {
		if (!isInitialized) throw new ServiceBusNotInitializedException();
		iocCcontainer.RegisterInstance(typeof(T), instance);
	}
}

As interfaces IService são os serviços de mensageria, tanto do cliente quanto do executor. As interfaces de IComponent são para classes que são responsáveis por implementar as regras de negócio do executor. Abaixo um exemplo de uso do ServiceBus, onde o ResultMessagesReceiver (que implmenta IReceiver) é o responsável por processar as mensagens de retorno recebidas e o ClienteService (que implementa IClientService) é o Thin Client.

 ServiceBus.InitializeSql("ServiceBroker");
 ServiceBus.RegisterComponent<IReceiver>(new ResultMessagesReceiver());
 ServiceBus.RegisterService<IClientService>(typeof(ClientService));

Criamos também um ServiceBase, que é usado por executores e clientes para implementar as rotinas principais de processamento de mensagens. O código está abaixo. É praticamente um loop onde as mensagens são lidas do SQL Server, dentro de uma transação, e processadas. Em caso de qualquer problema, a transação pode ser desfeita e a mensagem permanece lá. É importante notar que caso aconteçam muitos rollbacks em sequência, o SQL Server desativa a Queue (isto nos causou alguns problemas de debug!). Neste caso, a Queue deve ser reativada antes que outras mensagens possam ser lidas.

protected void ProcessMessages(int maxMessages) {
	var counter = 1;
	do {
		if (++counter > maxMessages) break;
		storage.BeginTransaction();
		try {
			var message = storage.ReadMessage(endPoint);
			if (message == null) {
				storage.Commit();
				break;
			}
			if (string.IsNullOrEmpty(message.Handle)) {
				storage.Commit();
				break;
			}
			if (initiator && message.MessageType == EndMessageType) {
				storage.EndConversation(message.Handle);
				storage.Commit();                        
				continue;
			}
			if (message.MessageType == ErrorMessageType) {
				// todo: logar o erro
				storage.EndConversation(message.Handle);
				storage.Commit();                        
				continue;
			}
			message.Date = DateTime.Now;
			if (!ProcessMessage(message)) {
				storage.Rollback();
				break;
			}
			if (!initiator) storage.EndConversation(message.Handle);
			storage.Commit();                    
		}
		catch (Exception) {
			storage.Rollback();
			throw;
		}                               
	} while (true); 
}

Abaixo está a parte relevante do código que implementa o storage do SQL Server. Optamos por simplesmente encapsular chamadas a stored procedures que ficam no banco de dados utilizado para as filas de mensagens. A seguir estão também as 3 procedures de leitura, envio e final de conversação.

 public string SendMessage(EndPointConfiguration endPoint, string data, string conversationHandle = null) {
	var parameters = new Parameters()
		.Add("initiatorService", endPoint.Initiator).Add("targetService", endPoint.Target)
		.Add("contract", endPoint.Contract).Add("messageType", endPoint.MessageType)
		.Add("data", data);
			
	if (!string.IsNullOrEmpty(conversationHandle))
		parameters.Add("handle", conversationHandle);
	StoredProcedureFacility.ExecuteScalar<string>(connectionStringName, "SendMessage", parameters)	
}

public Message ReadMessage(EndPointConfiguration endPoint) {
	var root = StoredProcedureFacility.GetXml(connectionStringName, "ReadMessage", Parameter.Create("queueName", endPoint.QueueName));
	return new Deserializer<Message>(root)
		.Property(m => m.MessageType, "mt")
		.Property(m => m.Contents, "data")
		.Property(m => m.Date, "dt")
		.Property(m => m.Handle, "ch")
		.Instance();
}

public void EndConversation(string handle) {
	StoredProcedureFacility.ExecuteNoResults(connectionStringName, "EndConversation", Parameter.Create("handle", handle));
}

Stored procedures:

CREATE PROCEDURE [dbo].[SendMessage] (
	  @initiatorService sysname,
	  @targetService varchar(255),
	  @contract varchar(255),
	  @messageType varchar(255),
      @data varchar(MAX) = NULL,
	  @handle varchar(255) = null
)
AS 
BEGIN
	if @handle is null 
	begin   declare @id uniqueidentifier   BEGIN DIALOG CONVERSATION @id
            FROM SERVICE @initiatorService
            TO SERVICE @targetService
            ON CONTRACT @contract
            WITH ENCRYPTION = OFF, LIFETIME = 7200;
		set @handle = cast(@id as varchar(255));
	  end;
	  send on conversation @handle message type @messageType (@data)                  
      select @handle;	  
END
GO

CREATE procedure [dbo].[ReadMessage](@queueName varchar(255))
as 
begin
declare @ch varchar(255)
declare @mt varchar(255)
declare @data varchar(max);
declare @dt DateTime;

set nocount on

declare @Sql nvarchar(max) = N'RECEIVE TOP(1) @h = conversation_handle, @messageTypeName = message_type_name, @Packet = message_body, @date = message_enqueue_time FROM ' + @queueName + ';'

EXECUTE sp_executesql @Sql, N'@h UNIQUEIDENTIFIER OUTPUT, @messageTypeName varchar(255) OUTPUT, @Packet VARCHAR(max) OUTPUT, @date datetime OUTPUT'
		,@h = @ch OUTPUT
        ,@messageTypeName = @mt OUTPUT
        ,@Packet = @data OUTPUT
		,@date = @dt output;

select * from (select @ch ch, @mt mt, @data [data], @dt [dt]) M for xml auto

end
GO

CREATE procedure [dbo].[EndConversation](@handle varchar(255)) as
begin
;end conversation @handle;
end

Finalmente, a seguir está o exemplo de um Thin Client, com um envio de mensagem e a rotina que faz o processamento das mensagens de retorno. Para cada tipo de ação criada, deve haver um tratamento específico. O construtor recebe, por injection (o Unity cuida disto) o storage e a classe que irá tratar as regras de negócio.

public class ClientService: ServiceBase, IClientService {	 
    private readonly IReceiver receiver;

    public ClientService(IStorage storage, IReceiver receiver)
		: base(storage, true, Configuration.Requester.Service, Configuration.Executer.Service, Configuration.Contract, 
			Configuration.Requester.Message, Configuration.Requester.Queue) {
		this.receiver = receiver;
	} 
	
    public void RequestAction1() {
      SendMessage(new MessageContent { Action = Actions.Action1});
    }

    public override bool ProcessMessage(Message message) {
       var ser = new DataContractJsonSerializer(typeof (MessageContent));        MessageContent content;        using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(message.Contents))) {
		content = (MessageContent) ser.ReadObject(ms);        }        if (content == null) return true;
	try {      switch (content.Action) {
		case Actions.Action1:     return receiver.ReceiveMessage(content, message.Date);
		default:     return receiver.Error(content.Action, ErrorCodes.NotImplemented, null);
		}
	}
	catch (Exception ex) {    return receiver.Error(content.Action, ErrorCodes.Exception, ex);
    }
}

O executor não é diferente, como pode ser visto abaixo. Ele somente recebe a mensagem, faz algum tipo de ação de negócio específica e retorna os dados para o solicitante. Claro que isto é um código simplificado, já que em um código de produção é importante tratar todos os tipos de erros possíveis, evitando rollbacks da fila.

public override bool ProcessMessage(Message message) {
	var ser = new DataContractJsonSerializer(typeof(MessageContent));
	MessageContent message;
	using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(message.Contents))) {
		message = (MessageContent)ser.ReadObject(ms);
	}
	if (message == null) return true;
	var returnMessage = new MessageContent {Action = message.Action};
	try {
		switch (message.Action) {
			case Action.Action1:
				returnMessage.Data = MyBusinessRulesManager.ExecuteSomething(message).ToString();
				break;
		}
	}
	catch (Exception ex) {
		returnMessage.Error = ex.Message;
	}
	SendMessage(returnMessage, message.Handle);
	return true;
}

Bom, espero ter conseguido passar uma visão geral da arquitetura de Microservices que estamos utilizando. Claro que temos muitos outros cenários que fazem com que a complexidade desta arquitetura seja bem maior. Por exemplo, temos situações onde temos mensagens geradas em horários específicos ou que só podem ser executadas em horas úteis. Isto faz com que o Executor tenha toda uma lógica para armazenar as mensagens com agendamento de execução específica em outras filas. Mas tudo isto é feito com o fundamento que mostrei nesta série. Como sempre, fiquem à vontade para entrar em contato para tirar dúvidas ou conversar mais sobre este assunto. Até a próxima!

, , , , ,

Deixe um comentário

Arquitetura de Microservices utilizando o Microsoft SQL Service Broker – Parte 2

Oi pessoal! Este post é continuação da série sobre a implementação de um barramento de microservices usando o SQL Server Service Broker (veja a primeira parte aqui).

O primeira passo é montar a infraestrutura no SQL Server. O barramento de microservices é baseado em filas, assim é necessário escolher um banco de dados para a criação das mesmas. Em tese, qualquer banco pode ser usado, mas recomendo a utilização de um banco criado especificamente para esta finalidade, para facilitar as tarefas de infra como backup, monitoração etc.

No nosso caso, criamos um banco novo, chamado, por exemplo, “MyMicroservicesBus”. Após a criação do banco, deve ser feito o comando abaixo, para que o Service Broker seja ativado no mesmo.

ALTER DATABASE MyMicroservicesBus SET ENABLE_BROKER

O próximo passo é criar as filas e contratos. Para quem não é muito familiar com os conceitos do SQL Server Broker, ver referência aqui. Recomendo também o livro “Pro Sql Server 2008 Service Broker”, bem completo. São necessárias duas filas, uma para servir de canal de comunicação da “Thin API” para o Executor (ExecuterQueue) e outra para que ele possa enviar as respostas de volta (InitiatorQueue). Para cada fila é necessário definir um contrato, que vai servir também para o versionamento das mensagens e um Service, para o roteamento de mensagens. Por último, não esquecer de dar as permissões de RECEIVE para os usuários apropriados. Um script completo é mostrado a seguir.

-- messages
create message type [http://mydomain.net/services/myservice/requestMessageV1] Validation = None
GO

create message type [http://mydomain.net/services/myservice/responseMessageV1] Validation = None
go

-- contracts
create contract [http://mydomain.net/services/myservice/contractV1]
(
	[http://mydomain.net/services/myservice/requestMessageV1] sent by initiator,
	[http://mydomain.net/services/myservice/responseMessageV1] sent by target
)
go

-- QUEUES
create queue MyServiceInitiatorQueue with status = on
go
create queue MyServiceExecuterQueue with status = on
go


-- SERVICES
create service MyInitiatorService on queue MyServiceInitiatorQueue ([http://mydomain.net/services/myservice/contractV1])
go
create service MyExecuterService on queue MyServiceExecuterQueue ([http://mydomain.net/services/myservice/contractV1])
go

-- permissions
grant RECEIVE ON MyServiceInitiatorQueue to public
go
grant RECEIVE ON MyServiceExecuterQueue to public
go

Uma vez que as filas estejam definidas, devemos decidir como as mensagens vão ser recebidas peles executores e clientes. Existem várias alternativas para isto, a mais simples seria usar stored procedures para processar mensagens de cada lado. No nosso caso, isto não atende pois temos regras de negócio complexas que devem ser executadas para cada mensagem, que estão implementadas em nosso Domínio e não faria sentido ou seria viável repeti-las em procedures. Assim precisamos de algo que seja capaz de chamar uma regra de negócio dentro do nosso ambiente transacional.

A segunda possibilidade é utilizar o Service Broker External Activator, que é uma ferramenta disponibilizada pela Microsoft para estes cenários. Ele fica “escutando” as filas e se encarrega de chamar um executável externo que, no nosso caso, seria um código em C#. Em nossos testes, funcionou perfeitamente nos nossos ambientes de desenvolvimento e homologação, mas qual a nossa surpresa ao ver que no ambiente de produção ele simplesmente não funcionou. Depois de muito pesquisar, descobrimos que ele não funciona em Clusters de SQL Server, que é exatamente o cenário usado no ambiente de produção do nosso cliente.

A terceira possibilidade, a mais complexa, é utilizar código CLR embutido dentro do SQL Server. Há várias versões do SQL Server é possível compilar uma DLL e registrá-la para uso dentro de um database como se ela fosse uma stored procedure. Esta foi a alternativa escolhida para nosso cenário.

Só recapitulando o procedimento para incluir um código CLR no SQL Server: primeiro, é necessário criar um projeto com uma classe estática e um método estático, que vai ser o ponto de entrada para a chamada no SQL Server, decorado com o atributo Microsoft.SqlServer.Server.StoredProcedure. Depois este assembly deve ser registrado no database e finalmente as queues devem ser modificadas para acionar este método quando uma mensagem chegar. Um exemplo de código CLR e script estão a seguir. Lembrando que caso o assembly não seja assinado com um certificado confiável para o servidor onde está o SQL Server, é necessário baixar o nível de confiança do banco (primeira instrução do script abaixo). Como nossos assemblies não estavam assinados, optamos por fazer isto. Esta decisão deve ser tomada com responsabilidade, pois este comando vai aumentar a vulnerabilidade do banco de dados.

using Microsoft.SqlServer.Server;

namespace MyNamespaceInAssembly {
    public static class StoredProcedures {

        [SqlProcedure]
        public static void ProcessMessages() {
            Domain.Services.ProcessMessages();
        }
    }
}

Script:

-- Allows unsigned Assemblies
ALTER DATABASE ServiceBroker SET TRUSTWORTHY ON
GO

-- Register Assemblies
CREATE ASSEMBLY MyServiceThinClient from 'c:\myservice\ThinClientProcessor.dll' WITH PERMISSION_SET = UNSAFE
go
CREATE ASSEMBLY MyServiceExecuter from 'c:\myservice\ExecuterProcessor.dll' WITH PERMISSION_SET = UNSAFE
go

-- procedures
CREATE PROCEDURE ProcessMessagesThinClient
AS
EXTERNAL NAME MyServiceThinClientAssembly.[MyNamespaceInAssembly.StoredProcedures].ProcessMessages
go
CREATE PROCEDURE ProcessMessagesExecuter
AS
EXTERNAL NAME MyServiceExecuterAssembly.[MyNamespaceInAssembly.StoredProcedures].ProcessMessages
go

-- permissions
grant execute on ProcessMessagesThinClient to public
go
grant execute on ProcessMessagesExecuter to public
go

-- activations
alter queue MyServiceInitiatorQueue with activation( procedure_name = ProcessMessagesThinClient, MAX_QUEUE_READERS = 1, status = on, execute as self)
go

alter queue MyServiceExecuterQueue with activation( procedure_name = ProcessMessagesExecuter, MAX_QUEUE_READERS = 1, status = on, execute as self)
go

Por enquanto é só pessoal. Na parte 3 vou detalhar melhor o código do ProcessMessages, tanto do ThinClient quanto do Executor. Se tiverem dúvidas específicas, podem entrar em contato. Até a próxima!

, , , , ,

Deixe um comentário

Arquitetura de Microservices utilizando o Microsoft SQL Service Broker – Parte 1

Olá pessoal, vou iniciar uma série de posts mais técnicos desta vez. Como mencionei no post anterior, estivemos analisando o uso do Microsoft SQL Server Service Broker como uma alternativa para implementação de Arquitetura de Microservicess. Aproveitando uma necessidade de um de nossos clientes, conseguimos definir e implementar um barramento de microservices com sucesso, que já está em execução em ambiente de produção! Nesta série de posts vou detalhar a arquitetura utilizada, tentando mencionar os desafios que enfrentamos e as soluções adotadas. Espero que sirva de apoio para outros desenvolvedores se aventurando por esta linha.

Inicialmente um pouco de contexto. Em um de nossos clientes, da área financeira, possuímos um grande sistema .NET, em operação há mais de 20 anos. Apesar dele ser relativamente bem estruturado (separação de camadas, mapeamento objeto-relacional com nHibernate, interfaces MVC etc.), ele acumulou, por todo este tempo de evolução, uma infinidade de regras de negócio para cada uma das áreas atendidas. Por ser um sistema central da empresa, praticamente todas as operações passam pelo mesmo e ele é integrado à vários outros, por diversas formas de comunicação. Assim, temos cada vez mais dificuldade para evoluí-lo e mantê-lo, já que sua arquitetura monolítica faz com que testes de integração e homologação sejam extremamente complexos e demorados. É com grande dificuldade que conseguimos manter um ritmo saudável de trocas de uma metodologia ágil, pois o tempo de homologação de usuários quase que inviabiliza nossas janelas semanais.

Outro grande problema de nosso sistema monolítico é a escalabilidade. Com o aumento de demanda, não temos opção a não ser aumentar o número e capacidade de máquinas, pois o sistema tem que ser replicado por inteiro. gerando aumento de custos e complexidade de infra-estrutura.

Este seria um dos típicos problemas resolvidos por um sistema que utiliza uma arquitetura de microservices. Cada área de um sistema de grande porte seria implementada por um serviço independente, que usa um barramento de mensagens para comunicação entre si (ver figura a seguir). Desta forma, a manutenção ou evolução de uma área não afetaria outras, tendo seu próprio ciclo de desenvolvimento independente.

ESB1

Porém, é raro termos o luxo de projetarmos um sistema do zero já incorporando este tipo de arquitetura. Então, nosso primeiro desafio foi achar uma maneira de fazer com que um sistema monolítico como o nosso pudesse utilizar microservices sem que ele tivesse que ser refeito do zero. A solução que encontramos está mostrada na figura a seguir. Inicialmente, isolamos uma área que possa ser implementada como um serviço isolado (A). Depois, quebramos este bloco em duas porções: uma pequena interface de comando e recepção de resultados (“Thin” API); e o módulo que implementa os executores de ações e regras de negócio específicas (B). Entre estes blocos, incluímos o suporte ao envio e recepção de mensagens, para que a API se comunique com o executor através do barramento de microservices (C).

ESB2

Desta forma, conseguimos alguns benefícios da arquitetura de microservices: 1) o executor é completamente isolado do sistema principal, podendo ser evoluído de maneira independente; 2) a execução de atividades passa a ser feita de forma assíncrona, liberando recursos para o sistema e permitindo a escalabilidade horizontal dos executores. Se conseguirmos isolar cada área desta forma poderemos, a longo prazo, quebrar nosso sistema monolítico em vários microservices, chegando bem próximo de um sistema que fosse projetado do zero para esta arquitetura.

Pode-se argumentar que outras arquiteturas poderiam gerar benefícios similares. Uma alternativa, por exemplo, seria fazer uso de componentes intercambiáveis. Embora isto garantisse o isolamento da área de negócio, não resolveria por completo a questão da substituição em produção. Com microservices, podemos parar um dos executores de serviço, ou mesmo todos eles e todo o sistema continua funcionando normalmente (as mensagens simplesmente se acumulam, sendo processadas posteriormente quando o serviço for restabelecido). No caso de componentes, seria muito mais complexo de resolver este cenário, pois há um acoplamento direto entre o mesmo e o sistema; se o componente parar o sistema também para. Com componentes também não teríamos solução trivial para o problema de escalabilidade.

Outra alternativa seria o uso de webservices tradicionais, baseados em SOAP ou outro protocolo. Webservices simplificam o problema da substituição em produção, pois podemos ter um cluster de servidores, com vários em paralelo, e ir substituindo aos poucos, com o uso de versionamento. Eles resolvem também o problema de escalabilidade, pois são de menor porte e podemos aumentar o cluster conforme necessário. O grande problema de webservices é a latência que existe para as chamadas de regras de negócio, já que eles normalmente se encontram em outros servidores. Para ações muito frequentes, em operações comuns do sistema, poderíamos ter uma demora excessiva para a execução, afetando o usuário. E operações assíncronas em sistemas web chamando webservices são extremamente difíceis de serem implementadas.

Analisando estes e outros cenários, a arquitetura de microservices parece ser a mais adequada. Da mesma forma que um webservice, ela separa a camada de negócios, permitindo escalabilidade e isolamento. E é tão rápido quanto um componente, já que a sua “Thin” API reside dentro da aplicação. É claro que o lado negativo é que todas as operações, que antes eram síncronas, passam a ser assíncronas. Este é o maior limitador, já que é necessário reimaginar o comportamento do sistema considerando que as ações não são mais imediatas, mofidifcando a usabilidade do mesmo.

Para a implementação do nosso barramento de serviços, foi considerado o uso vários produtos, open source e comerciais. O RabbitMQ foi uma das alternativas que foram melhor avaliadas. No entanto, no final, a escolha foi o Microsoft SQL Server Service Broker. As razões foram a facilidade de se montar um barramento de serviços simples e o fato dos nossos sistemas já o usarem o SQL Server como DBMS, o que simplificou bastante nossa infraestrutura.

Bom, por hoje é só. Em breve devo detalhar a parte técnica da solução, desde a implementação das filas de mensagens e processadores até o processo de ativação de executores. Até a próxima!

, , , , ,

3 Comentários

De volta 2!

Oi pessoal!! Depois de um looooongo tempo sem postar, finalmente consegui um tempinho. Olhando o último post, dá pra notar que estávamos extremamente ocupados no início do ano passado. Mal podíamos imaginar o que viria pela frente. Do ano passado para cá, iniciamos um enorme projeto com um de nossos clientes, o Sistema de Ensino Poliedro, que nos tomou praticamente 1 ano de contínua dedicação e deve ainda prosseguir por bastante tempo.

O projeto foi participar da construção de aplicativos móveis, iOS (iPhone e iPad) e Android (smartphones e tablets), para disponibilizar para alunos uma grande parte das informações até então só existentes no site, como agenda, notas, resultados de simulados etc. Nossa parte foi expor em serviços toda a infraestrutura do domínio que tínhamos em sistemas, de forma a funcionar tanto para os aplicativos móveis quanto para uma nova versão web, também a ser desenvolvida. Trabalhamos com empresas parceiras, que ficaram responsáveis pela construção das interfaces móveis e web.

O maior desafio foi a definição desta API de serviços, para ser o mais reusável possível sem comprometer a facilidade e uso pelas interfaces. E tinha que ser extremamente escalável, já que o número de alunos previstos para os aplicativos era superior a 30.000.

A nossa opção foi utilizar a ASP NET MVC Web API. Uma das grandes vantagens desta arquitetura foi que o Web API já é capaz de retornar dados em vários formatos, como XML e JSon, o que foi providencial para nossa meta de reusabilidade. E por ser bem leve, atendeu bem também a necessidade de escalabilidade.

Como o sistema é, em quase todas as telas, somente leitura, optamos por otimizar o acesso a dados com a criação de banco de dados construídos especialmente para este fim. Assim, desenvolvemos procedimentos para trazer dados dos nossos bancos transacionais e produzir uma informação desnormalizada, pronta para consumo pela interfaces. Embora isto tenha exigido uma grande capacidade de processamento para gerar esta informação consolidada (optamos por executar isto durante as madrugadas), o acesso de cada uma das interfaces ficou extremamente rápido e escalável.

O resultado foi um sucesso completo. Os aplicativos iOS e Android tiveram centenas de downloads nos primeiros dias e hoje, menos de 6 meses após o lançamento, já temos mais de 10.000 aparelhos, entre tablets e smartphones, com acessos de alunos. O resultado foi tão positivo que já continuamos no mesmo ritmo para construção de mais aplicativos, agora voltados para professores e gestores. Nosso cliente provavelmente é hoje uma das instituições mais modernas do país, em termos de software. Ficamos super orgulhosos de termos participado deste projeto!

Em paralelo a isto, outros clientes nossos estão nos demandando bastante. Com isto, a White Fox está totalmente ocupada e estamos em busca urgente por bons desenvolvedores. Se você quer saber mais ou se candidatar, envie-nos seu currículo para rh@whitefox.com.br

Espero não demorar mais um ano para o próximo post! Até breve!

P.S.: Para quem ainda não se inscreveu e está no rio, não percam o DNAD 2013, nestes dias 2 e 3 de agosto. A grade de palestras está excelente e a White Fox é uma das patrocinadoras!

, , , , , , , , ,

Deixe um comentário

C#, Closures e Resharper

Olá pessoal! Tivemos um problema esta semana, em tese de origem simples, mas que nos tomou um tempo enorme de debug. Quero compartilhar aqui para tentar evitar que outros sigam pelo mesmo caminho!

Para começar, imaginem este código ilustrativo abaixo. Uma rotina simples para enumerar duas vezes por uma coleção e retornar a mesma, modificada. Claro que isto é somente representativo de um código real, mas imaginem qualquer função que por alguma razão modifica elementos de uma lista e retorna a mesma ou um subconjunto da mesma. Trivial, certo? O resultado esperado desta execução são duas linhas com o número “2”.

class MinhaClase {
    public decimal Valor { get; set; }
}
    
class Program {
    static IList<MinhaClase> Calcular(IList<MinhaClase> classes) {
        foreach (var minhaClase in classes) {
            minhaClase.Valor = 1;
        }
        foreach (var minhaClase in classes) {
            minhaClase.Valor++;
        }
        return classes;
    }
    static void Main(string[] args) {
        var classes = new[] {
                                new MinhaClase { Valor = 0}, 
                                new MinhaClase { Valor = 0}, 
                                new MinhaClase { Valor = 1}
                            };
        var result = Calcular(classes.Where(c => c.Valor != 1).ToList());
        foreach (var minhaClase in result) {
                Console.WriteLine(minhaClase.Valor.ToString());
        }
        Console.ReadLine();
    }
}

Todo o problema começa quando a gente tenta refatorar e melhorar o código. Como todo bom desenvolvedor, nós usamos o Resharper, que é uma ferramenta fantástica da JetBrains para nos ajudar a gerar um bom código. Na White Fox, nós a usamos praticamente todo o tempo, ela nos poupa muito esforço de código e ajuda a evitar problemas graves de maneira completamente automática. Porém, neste caso, como a rotina “Calcular” simplesmente enumera, o Resharper vai sugerir que a gente mude o IList<> para sua classe base, um IEnumerable<>. A regra seguida por ele faz sentido pois se só estamos usando métodos da classe base, não seria necessário usar a classe derivada na assinatura. O código gerado então após a refatoração fica da seguinte forma:

class MinhaClase {
    public decimal Valor { get; set; }
}
    
class Program {
    static IEnumerable<MinhaClase> Calcular(IEnumerable<MinhaClase> classes) {
        foreach (var minhaClase in classes) {
            minhaClase.Valor = 1;
        }
        foreach (var minhaClase in classes) {
            minhaClase.Valor++;
        }
        return classes;
    }
    static void Main(string[] args) {
        var classes = new[] {
                                new MinhaClase { Valor = 0}, 
                                new MinhaClase { Valor = 0}, 
                                new MinhaClase { Valor = 1}
                            };
        var result = Calcular(classes.Where(c => c.Valor != 1));
        foreach (var minhaClase in result) {
                Console.WriteLine(minhaClase.Valor.ToString());
        }
        Console.ReadLine();
    }
}

Depois deste refactor, o resultado do programa não deveria mudar, certo? Errado! Os mais experientes irão saber que o resultado da execução deste segundo código é uma lista vazia!

A explicação para isto tem a ver com o suporte do C# para closures (ver este post, que explica o que são de maneira simples, ou esta explicação mais acadêmica aqui). As operações LINQ simplesmente geram predicados para uma closure que é executada no momento que a coleção é acessada. Tem a ver também com a diferença básica entre um IList<> e um IEnumerable<>, já que num IList<>, um GetEnumerator (executado em um foreach) sempre aponta pra coleção interna, já existente… Já em um IEnumerable<>, o GetEnumerator vai sempre acionar o container para criar coleção e aí enumerar os elementos… Isto significa que a coleção de um IEnumerable<> produzido por uma closure vai ser gerada A CADA foreach, e não uma única vez. Ou seja, cada foreach da rotina “Calcular” executa a closure, bem como o foreach do resultado final. Como neste caso a closure é um filtro de Valor != 1, no segundo foreach da rotina “Calcular” e no último (onde ele imprime o resultado no console) ele simplesmente vai retornar 0 elementos!

Onde erramos? Os mais experientes vão notar que erramos na hora que tiramos o .ToList() do final da closure que define o filtro. A razão da remoção foi que como a rotina “Calcular” agora aceita um IEnumerable<>, o ToList() não seria mais necessário. Sem o ToList(), a closure não é mais executada naquele momento e sim passada para a variável e aí toda a rotina Calcular para de funcionar. Basta colocar o .ToList() de volta que tudo passa a funcionar novamente. Este erro, aparentemente óbvio, na prática é muito fácil de ser cometido, pois o uso de expressões LINQ é cada vez mais comum e, em um código complexo de negócios, se a função sendo chamada tem parametros IEnumerable<>, uma closure pode acabar sendo enviada inadivertidamente.

Portanto, neste caso, o Resharper não só facilita os erros, como pode ainda produzir alguns muito difíceis de identificar (no nosso casso, foram horas gastas até achar onde estava a closure sendo passada). Claro que desligamos esta sugestão na nossa configuração do Resharper e o problema foi resolvido. É importante ressaltar que, mesmo apesar deste caso, continuamos a achar o Resharper uma ferramenta essencial de trabalho para nós.

Outro ponto que merece ser comentado, é que pesquisando este problema, entendemos melhor o funcionamento do IEnumerable<> e o IList<>. Como o IEnumerable<> sempre tem que gerar a coleção a cada GetEnumerator(), podemos ter um desempenho pior das coleções, na maioria dos cenários, comparado a um IList<>. Só isto já seria motivo suficiente para favorecer o uso do IList<> ao invés do IEnumerable<>.

Bom, espero ter ajudado a poupar o tempo de outras pessoas ao deparar com problemas similares. Por causa deste problema, acabamos tendo que conhecer bem mais sobre closures e coleções no .NET, assim, no final, a existência deste bug até que foi benéfica!

Até a próxima!

, ,

4 Comentários

Log de Negócio

Esta semana tive uma necessidade de log específica em um de nossos sistemas, que acabou gerando uma excelente adição para o nosso framework de desenvolvimento. A necessidade era saber o que acontecia em produção que acabava gerando um bug raro de valores no final do dia, e que fomos incapazes de reproduzir mesmo com uma grande quantidade de testes unitários. Aparentemente o bug está associado a uma condição de multi-usuário que é muito complicada de reproduzir. Para resolver este bug precisaríamos saber a seqüência exata em que as operações foram realizadas, e isto só seria possível com log.

A necessidade de log de negócio acaba surgindo vez por outra em aplicações. E, praticamente para cada sistema que eu já participei, foi feita uma solução diferente para log, com vantagens e desvantagens. Alguns usam log em banco, colocando triggers nas tabelas e replicando as informações, o que é extremamente simples de fazer mas gera grandes problemas de contenção e espaço utilizado. Outros usam versões do log4net gerando dados em arquivos, o que também é relativamente simples de fazer porém gera também dificuldade em ambientes massivamente multi-usuário – sem falar na dificuldade de processar os arquivos depois. Já fiz também log em banco, com e sem log4net, com bons resultados, mas sempre com uma solução específica para cada caso, o que causava um bom esforço de manutenção.

Outro ponto que sempre me incomodou é a questão da intrusividade das funções de log na regra de negócio. Tirando a solução em trigger de banco, qualquer implementação de log4net foi sempre feita com chamadas específicas de log dentro da rotina de negócio. Isto polui a rotina e dificulta a manutenção. Eu sempre tentei criar algo tipo AOP, mas nunca consegui uma solução boa. Mesmo atualmente, as bibliotecas conhecidas de AOP (PostSharp, Spring.Net etc.) não são uma boa opção para o nosso ambiente por serem um framework completo, de grande impacto…. Usar qualquer uma delas só pra log de algumas regras de negócio pra mim seria usar um canhão pra matar uma mosca.

Assim, resolvemos desta vez fazer algo decente, já incorporado ao framework, e que usasse uma abordagem AOP na medida do possível. Graças à tecnologia do Castle Windsor IoC usada atualmente no framework, isto foi relativamente simples de ser feito. Definimos na nossa arquitetura que o log de uma regra deveria ser expresso através da observação de parâmetros e retornos da função, antes e depois dela ser executada. Através do uso destas condições pré e pós, é possível identificar exatamente como aquela função afetou os dados.

Para implementar, o que fizemos foi usar o log4net para salvar eventos em banco. Até aí, solução padrão. O passo seguinte foi definir atributos AOP para marcar uma regra de negócio como sendo logada. O ideal teria sido definir as condições pré e pós no próprio atributo, porém o C# atualmente não suporta nada que não seja estático nos atributos; e usar strings eu acho uma prática ruim. A solução usada foi definir um tipo para o atributo que apontasse para uma classe que definiria quais são todas as condições, utilizando lambda.

O resultado ficou excelente. Abaixo está um exemplo do atributo na regra de negócio:

[Log(typeof(AtualizarValorDePosicaoLog))]
public void AtualizarValorAnterioDePosicao(Contrato contrato, 
                                    Posicao posicao, decimal valor) {
 

E no tipo de definição das condições, fica assim:

public class AtualizarValorDePosicaoLog : LoggerBase {

  public AtualizarValorDePosicaoLog() : base(
    Properties.PreCondition("valor"),
    Properties.PreCondition<Contrato>(
      c => c.Id, c => c.Tipo, c => c.TipoBacen, 
      c => c.Moeda, c => c.Posto, c => c.ValorMoedaEstrangeira, 
      c => c.ValorMoedaNacional, c => c.ValorUSD, c => c.TipoPagamento),
    Properties.PreCondition<Posicao>(
      p => p.Id, p => p.ValorPosicaoAnterior, p => p.Conformidade, 
      p => p.Baixa, p => p.Transferencias, 
      p => p.TotalCompra, p => p.TotalVenda, p => p.Moeda, p => p.Posto, 
      p => p.Tipo),

    Properties.PostCondition<Posicao>(
      p => p.Id, p => p.ValorPosicaoAnterior, p => p.Conformidade, 
      p => p.Baixa, p => p.Transferencias, 
      p => p.TotalCompra, p => p.TotalVenda, p => p.Moeda, p => p.Posto, 
      p => p.Tipo)) { }
    }
 

Na execução, o interceptor da classe de serviço busca o atributo, instancia o tipo de definição e coleta os dados, antes e depois da rotina executar. Para minimizar o impacto em tempo de execução, o logger simplesmente armazena um dicionário com todas as condições recolhidas, serializando num XML. Posteriormente, de maneira assíncrona e com baixa prioridade, um executor analisa os registros de log gerados e expande cada condição em tabelas do tipo dicionário.

Com isto o impacto ficou bem pequeno e conseguimos fazer algo quase puramente AOP e sem nenhuma definição em string! Finalmente conseguimos uma implementação boa para log, que deve nos atender por muito tempo.

Nesta solução, alem da equipe da WhiteFox, tivemos a colaboração do arquiteto Fernando Bichara, da Perlink.

, , , , ,

4 Comentários

Refatorando Grandes Sistemas

Olá, feliz 2010! Eu estou muito animado, as perspectivas para 2010 são muito boas! Fiquei agradavelmente surpreso com a reação das pessoas à White Fox, ela foi muito bem recebida e recebi várias mensagens de apoio, obrigado! Estou muito convicto de estar no caminho certo e construindo uma empresa que será um local excepcional para se trabalhar e prosperar. E o ano começa animado, este mês estamos entregando 2 primeiras versões de novos sistemas além de continuarmos com a manutenção dos existentes.

Conversando com um amigo na semana passada, caímos no assunto de grandes sistemas e do processo necessário à manutenção (evolutiva ou corretiva) para sistemas de grande porte em geral. O que me levou a refletir sobre isto pois, apesar de achar que processos e especialização por segmentos (silos) no desenvolvimento de software seja algo nocivo, não consegui achar nada de errado ou que eu faria diferente naquele caso específico.

O ponto é que grandes sistemas necessariamente exigem uma grande infra-estrutura. Por menor que seja uma mudança, para se garantir que um sistema não seja afetado por ela, é necessário avaliar todo o impacto, preparar uma sequência de testes específicos para a mudança e executar todo o conjunto de testes de integração e funcionais – muitos dos quais, até pelo volume, podem não ser totalmente automatizados. Isto sem falar no processo de liberação de versão em si, que envolve muitas vezes compilações e execuções de testes unitários que podem demorar várias horas. Em um time agile, mesmo com o máximo de automatização, este processo todo iria significar que o time iria passar a menor parte do tempo implementando a mudança e a maior parte do tempo, planejando, testando, integrando e verificando se tudo ficou ok. Não é algo viável, na minha visão. Acho que é por isto que o pessoal de Scrum afirma que Scrum é para novos projetos somente, não sendo adequado para manutenções.

Então o que fazer? Será que para grandes sistemas não é possível usar métodos ágeis? Ou que temos que assumir que para grandes sistemas, irão existir silos (testes e/ou gestão de configuração, por exemplo) e que isto é algo que não tem solução? Os que me conhecem sabem que esta resposta não é satisfatória pra mim. Porém, uma vez que o sistema tenha um certo tamanho, não consigo ver como fazer as coisas diferentes. Talvez então a saída seja não deixar os sistemas passarem de um tamanho crítico.

Claro que certas áreas exigem software de grande porte. O caso de um sistema operacional como o Windows, um aplicativo como o SAP ou mesmo um aplicativo como o Word, são sistemas que irão exigir toda uma infra-estrutura pesada para permitir manutenção e evolução. Não é à toa que o processo de testes da Microsoft, por exemplo, é algo extenso e complicado. Porém, não acho que isto deva ser usado para software de menor porte. Aliás, acho justamente o contrário, usar este tipo de técnica/processo para software de menor porte significa aumentar em ordens de grandeza o custo para desenvolvimento/manutenção e dificultar significativamente o uso de metodologias ágeis.

Assim, quando possível, acho que devíamos buscar quebrar grandes sistemas em aplicações menores. Usando os mesmos princípios aplicados em refatoração de código (tais como responsabilidade única, baixo acoplamento etc.), poderíamos tentar quebrar os sistemas em módulos para que, apesar de funcionarem em conjunto, sejam entidades separadas. Isto seria possível com uma definição clara de fronteiras, com o apoio de interfaces de serviços (SOA) e com um isolamento de interfaces com o usuário. Áreas de negócio distintas poderiam ser separadas de maneira que o domínio de negócio fosse separado, fazendo com que mudanças em um determinado setor do domínio não afetasse os demais. E mudanças na interface de serviços poderiam ser feitas de maneira versionada e evolutiva, no formato tipicamente usado em SOA.

Claro que uma refatoração de sistema só é possível com a participação direta do cliente, já que isto significa por si só uma mudança em processos, modo de trabalho e até mesmo na relação comercial, já que diversos módulos podem também significar mais de um fornecedor. Mas acredito que se isto gerar uma economia significativa no custo de desenvolvimento, será algo que terá grandes chances de ser adotado pelo cliente. O que mais uma vez ressalta a importância da área de contato com o cliente deter também todo o conhecimento técnico para propor e justificar mudanças como esta. Uma área puramente comercial teria muitas dificuldades em entender e vender algo deste tipo. Felizmente este não é o caso da White Fox.

Na White Fox nós temos alguns sistemas que estão começando a entrar no estágio em que a manutenção está começando a ficar cara, talvez seja este o momento de se pensar em uma refatoração deste tipo. Irei tentar colocar algo assim em prática e vou postando os resultados aqui. Novamente, um feliz 2010 para todos!

, , ,

1 comentário