Mês: maio/2014

O que é Big Data – e por que você deveria estar desesperadamente interessado nisso

Word Cloud "Big Data"

Estima-se que 2,5 quintilhões de bytes de dados são criados todos os dias, criados em sua maioria por usuários na web de forma desestruturada. em diferentes meios (texto, áudio, vídeo, etc.) e fontes (Facebook, Twitter, Youtube, etc.).

O potencial desses dados é gigantesco, mas os bancos de dados relacionais não são capazes de lidar com algo dessa natureza. Qual a solução?

A história dos dados

A forma como os dados são gerados, processados e armazenados mudou drasticamente ao longo das últimas décadas e mesmo em relação aos últimos anos.

Os primeiros cientistas da computação preocupavam-se em criar estruturas de dados otimizadas para armazenar a maior quantidade de informação, da forma mais eficiente possível. Os bancos de dados relacionais ajudaram bastante nessa tarefa.

Porém, o volume de dados que existe no mundo hoje simplesmente não cabe mais nesse modelo. Com a ascensão da Internet disponível, virtualmente, a todo mundo e com a popularização dos sistemas web, incluindo as Redes Sociais, temos hoje um número inimaginável de informação, que aumenta a cada dia.

Agora pare e imagine o potencial desses dados se devidamente analisados. As pesquisas de mercado tradicionais consideram uma população de algumas centenas ou, no máximo, milhares de pessoas para testar alguma tendência.

E se conseguíssemos analisar os dados comportamentais de milhões ou até bilhões de pessoas? A chance de identificar tendências em grupos mais específicos aumentaria enormemente.

O grande problema é que esse volume monstruoso de dados está distribuído em inúmeros locais e em geral não é estruturado para transportarmos tudo para tabelas.

Pense no Twitter. Milhares de pessoas ao redor do mundo postando comentários, críticas, reclamações em forma de um texto com no máximo 140 caracteres. Se pudéssemos analisar sobre o que as pessoas estão mais comentando hoje e se estão falando bem ou mal, teríamos uma enorme vantagem competitiva, não é mesmo? Poderíamos responder “em tempo real” aos consumidores de acordo com o que eles estão querendo.

Texto, som, vídeo, imagem. Há um enorme potencial em tudo isso em busca de uma informação relevante.

O que é Big Data?

Big Data é um termo cunhado por Budhani em 2008 para descrever qualquer conjunto de dados que seja inviável de manipular por uma ferramenta tradicional em um tempo razoável.

Obviamente isso é um tanto vago, mas já existem algumas definições mais específicas.

Os V’s do Big Data

Ao contrário do que o termo pode levar-nos a pensar, Big Data não é apenas sobre o volume de dados. Em tese, um SGBDR tradicional pode tratar quantidades imensas de dados. Big Data envolve outras características relacionadas com velocidade e variedade.

Procurando definir o que é Big Data, alguns pesquisadores chegaram a um conceito de múltiplos atributos. Isso ficou conhecido como os 3 V’s do Big Data: Volume, Velocidade e Variedade.

Entretanto, o amadurecimento da tecnologia deu origem a uma definição mais completa, com 5 V’s: Volume, Velocidade, Variedade, Valor e Veracidade

Confira na imagem abaixo:

Volume

Big Data envolve uma quantidade de dados que começa na casa dos terabytes e chaga até os petabytes. Note que enquanto escrevo esta informação já pode estar desatualizada!

Velocidade

As informações precisam ser transmitidas, processadas e retornadas em tempo hábil para o negócio. Pense na busca do Google, ela precisa “ler a internet” em alguns milissegundos. Ou você usaria o Google se cada pesquisa levasse horas ou dias?

Variedade

Ao contrário dos sistemas de informação mais tradicionais, Big Data gira em torno de uma grande variedade de dados não estruturados e não normalizados.

Muitas informações podem ser obtidas através de processamento de vídeos do youtube, tweets, arquivos de log e páginas de blog.

Eu sei que você já deve estar cansado de eu usar o Google como exemplo, mas farei novamente. Já viu a legenda automática do YouTube? Houve um bom avanço nos algoritmos de análise de áudio, não é? Já pensou em como o Google ordena os resultados da pesquisa, priorizando o conteúdo mais adequado para os termos inputados?

Trabalhar com SEO nada mais é do que tentar influenciar os algoritmos de Big Data do Google!

Valor

Com o amadurecimento das soluções de Big Data, notou-se que as três características já apresentadas não eram razão suficientes para a utilização desta tecnologia.

É necessário que as informações extraídas da massa de dados proporcionem um benefício tangível.

Por exemplo, se uma empresa consegue identificar através de análises estatísticas certas tendências dos consumidores, ela poderá obter uma grande vantagem em relação aos seus concorrentes.

Veracidade

Desde que alguns institutos passaram a usar dados de redes sociais para verificar tendências, surgiu uma nova versão de ataque digital que procura manipular os resultados.

Vou dar um exemplo. Ouvi recentemente que foram criados bots (robôs) para gerar milhares de tweets e postagens na Internet sobre certo candidato a cargo político. O objetivo é fazer parecer que o candidato tem popularidade, sendo “o candidato mais comentado na internet”.

Uma preocupação que tem faltado nas implementações de Big Data é justamente verificar se os dados são oriundos de fontes confiáveis e se os mesmos são autênticos.

Não vou aprofundar-me nesse assunto, mas continuando com o exemplo, uma boa implementação de contagem de tweets poderia eliminar as mensagens repetidas ou muito parecidas. Isso porque spammers são capaz de gerar frases aleatórias com variações de palavras. Portanto a mesma técnica que filtros de spam utilizam deveria ser aplicada nesse caso.

Por quê Big Data?

Até aqui você já deve ter compreendido alguns conceitos básicos sobre Big Data. Porém, ao mesmo tempo, pode estar se perguntando essa tecnologia não teria seu uso limitado a empresas altamente técnicas como o Google.

Em teoria, sobre tudo o que se pode fazer estatística pode se beneficiar de Big Data.

Vejamos agora algumas aplicações reais de grandes e pequenas empresas.

Vendas

O Walmart, por exemplo, adotou Big Data desde muito cedo. Os dados de dez diferentes sites foram consolidados num cluster Hadoop, migrados a partir de bases Oracle e outras fontes de dados.

Além de diminuir o tempo das buscas o Walmart é capaz de verificar se um amigo seu lhe enviou informações sobre um determinado produto em uma rede social e então lhe mandar um e-mail com uma promoção daquele produto.

Ele também pode lhe indicar produtos de acordo com o perfil dos seus amigos no Facebook, de modo que você possa dar um presente adequado.

Telecom

Algumas empresas de Telecom usam Big Data para traçar perfis dos consumidores, unindo padrões de ligação, envio de mensagem e participação em redes sociais. Eles conseguem então descobrir quais consumidores estão mais propensos a trocar de operadora.

Esportes

No campo dos esportes, há várias iniciativas de monitoramento dos atletas. Isso permitirá a análise do desempenho de um time e de cada indivíduo, possibilitará encontrar tendências, prever resultados e até criar jogos mais reais com as características dos times verdadeiros.

Saúde

Na saúde, os pesquisadores poderão prever com mais precisão problemas de saúde baseando-se em históricos hospitalares.

Hoje já existem estudos sobre a coleta de informações de respiração e batimentos cardíacos de bebês nascidos prematuros e o uso de Big Data para identificar padrões de infecção. Isso possibilitará identificar antecipadamente quando um bebê realmente está com infecção antes dos sintomas mais visíveis aparecerem e, consequentemente, aplicar um tratamento mais efetivo.

Trânsito

No Japão, um aplicativo baseado em Big Data está ajudando a melhorar trânsito de uma cidade ao coletar informações de 12 mil táxis e vários sensores.

O serviço consegue analisar 360 milhões de informações sobre o trânsito instantaneamente para retornar a melhor rota naquele horário para o motorista. Com bancos de dados relacionais, o processamento levava vários minutos.

O que realmente é Big Data?

Parafraseando um outro artigo, “no fundo, no fundo, Big Data não se trata de dados nem de tamanho”. Também não se trata de novidades tecnológicas ou descobertas científicas.

Big Data é uma nova forma de ver o mundo, de usar estatísticas e de tomar decisões de negócio.

Uma metáfora seria como a descoberta do microscópio. Uma vez que se consegue enxergar as coisas numa escala completamente diferente de antes, novas descobertas inevitavelmente irão surgir.

A análise criteriosa de um grande volumes de dados é uma tendência que vai continuar e logo se expandirá para mais e mais esferas da vida humana.

Big Data é apenas mais uma modinha?

Algumas pessoas acham que Big Data é apenas mais uma moda tecnológica.

Como toda moda, existe um ciclo onde a princípio todos estão falando sobre aquilo e tem uma expectativa muito alta, depois começam a perceber que o investimento não está trazendo todos os benefícios imaginados, alguns desistem e outros persistem e começam a usar a tecnologia de forma mais adequada e ao final atingem um grau mais real de aproveitamento.

gartner_hype_cycle

Certamente os termos e as tecnologias podem mudar, mas é verdade também que a ideia principal está aí para ficar.

Big Data não é a mesma coisa que Data Warehouse ou Business Intelligence?

Quem conhece um pouco sobre Data Warehouse ou Business Intelligence pode ter encontrado muitos pontos em comum com o que foi apresentado sobre Big Data. Muitos, inclusive argumento que é a mesma coisa. Será?

Por um lado, podemos dizer que a ideia principal é a mesma, a saber, processar um grande volume de dados para ajudar em decisões de negócios e detectar padrões.

Por outro lado, as tecnologias de BI e DW são mais conservadores no que diz respeito às técnicas, tecnologias e estruturas de dados. O foco delas é processar e consolidar os dados estruturados das empresas em uma base de dados somente-leitura separada dos bancos de dados principais dos sistemas a fim de obter estatísticas relevantes.

Já Big Data tem por objetivo unir fontes heterogêneas, privadas e públicas, geralmente em bases NoSQL ou mesmo em arquivos, com dados modificáveis. Big Data também é usado diretamente “em produção”, provendo informações diretamente para os usuários. Os fundamentos também são diferentes, pois Big Data lida especialmente para processamento distribuído, como veremos em breve num artigo sobre MapReduce.

Quais são as tecnologias relacionadas com Big Data?

Não entrarei em detalhes, pois pretendo escrever outros artigos sobre essas tecnologias. Vou simplesmente citar as mais comuns que envolvem o ecossistema do Hadoop, de longe a solução Big Data mais conhecida e também usada como base para outras soluções comerciais.

Hadoop

É uma implementação opensource do framework MapReduce. O Hadoop é um projeto mantido pelo grupo Apache.

Ele é capaz de coordenar tarefas executadas processamento distribuído e paralelo de grandes conjuntos de dados em qualquer quantidade de nós de um cluster.

O Hadoop é implementado em Java, sendo executado numa JVM. Você pode escrever um programa para executar no Hadoop usando a API disponibilizada em jars ou no Maven, inclusive usando sua IDE predileta.

No entanto, Java não é o limite. O Hadoop nada mais é do que uma base de uma pilha de tecnologias, incluindo linguagens de mais alto nível para fins específicos.

HDFS

O Hadoop Distributed File System é um sistema de arquivos distribuído de alta velocidade usado no Hadoop.

YARN

Trata-se de um framework para agendamento de tarefas e gerenciamento do cluster.

HBase

Um banco de dados NoSQL com suporte a dados estruturadas e tabelas grandes.

Hive

O Hive é um tipo de linguagem SQL (HiveQL, para ser mais exato) próprio para realizar consultas em grandes quantidades de dados distribuídos.

Mahout

Este é um projeto que procura unir Inteligência Artificial com Big Data, falando especificamente de Aprendizado de Máquina.

Pig

Pig é uma linguagem de programação de alto nível que facilita na criação de tarefas e análise de dados distribuídos, com execução em paralelo.

Bem, eu sei que você pode ser especialista em “programação porca”, mas não é disso que se trata o Pig. Tá… todo mundo aqui no Brasil já fez essa piada, mas não pude perder a oportunidade! 😉

ZooKeeper

Para manter todos os bichos do Hadoop em suas jaulas é necessário alguém para administrar tudo.

O ZooKeeper é um serviço centralizado para manter a configuração, dar nomes, prover sincronização e agrupamento de serviços.

Mas quem vai operar tudo isso?

Big Data trouxe também um novo tipo de profissional ao palco: o Cientista de Dados.

Uma breve pesquisa sobre esse termo vai trazer muitos resultados, demonstrando que é uma carreira em alta no momento.

Os cientistas de dados devem unir diversas habilidades:

  1. Ser bom em estatística e matemática para analisar corretamente os dados;
  2. Dominar as bases da Ciência da Computação para implementar devidamente as soluções, que geralmente incluem algum tipo de programação e entendimento da arquitetura distribuída; e
  3. Conhecer o negócio da empresa para gerar benefícios tangíveis com os resultados do seu trabalho.

Considerações finais

Sendo uma moda ou não, as empresas estão investindo pesadamente e obtendo resultados concretos com Big Data. Quanto antes uma empresa obtiver os benefícios em potencial de uma solução Big Data, mais ela terá chances de aumentar sua participação no mercado.

Os profissionais que mais cedo dominarem essa tecnologia também terão mais chances de destacar-se nesse nicho de mercado.

Como em toda mudança, os que chegarem por último terão que se contentar com as sobras.

A maior consideração quanto a isso é não entrar no frenesi de implementar Big Data por um fim em si mesmo, nem criar expectativas irreais sobre as tecnologias.

Torna-se necessário então compreender o cenário atual para avaliar cautelosamente o caminho adequado para um investimento em Big Data.

Além disso, voltando ao exemplo do microscópio, a nova visão de mundo proporcionada pela análise de enormes massas de dados deve ser suplementar e não substituta da visão da realidade dos negócios e da realidade humana.

Apache POI: adicionando segurança em arquivos Excel

poi-original
A biblioteca Apache POI possibilita a leitura e gravação de documentos do Microsoft Office via código Java.

Por esses dias, fiz uma pesquisa sobre como essa biblioteca viabiliza (ou não) trabalharmos com arquivos protegidos do Excel.

Níveis de segurança do Office

Documentos do Excel podem ter diferentes níveis de proteção, a saber:

  • Proteção contra alteração: permite abrir o documento em modo somente-leitura, mas não deixa o usuário alterá-lo sem antes digitar a senha.
  • Proteção contra alteração de um trecho do documento: protege planilhas e células específicas com senha.
  • Proteção contra leitura: não permite abrir o documento sem a senha.

Todos esses modos podem ser usados ao mesmo tempo, inclusive com senhas diferentes.

Se o arquivo tiver proteção contra leitura, o Excel irá solicitar a senha antes de abrir o mesmo. Se houver proteção contra alteração ele irá solicitar a senha para desbloqueio da edição ou permitir acesso somente-leitura ao documento. Se planilhas ou células estiverem protegidas contra alteração, então o usuário deve acessar a função “Desproteger” no Excel e digitar a senha de desbloqueio para cada trecho.

As proteções contra alteração são simples travas no editor, pois o arquivo em si permanece aberto e não há restrições de alteração via programação ou com o uso de editores de terceiros. Podemos dizer que este mecanismo oferece um baixíssimo grau de segurança, que impedirá apenas o usuário mais leigo de efetivamente modificar o documento.

Por outro lado, a proteção contra leitura oferece um grau de segurança maior, pois o documento como um todo é criptografado. Isso significa que nenhum programa ou editor conseguirá sequer ler o documento sem antes descriptografar os bytes do arquivo com a senha original.

Versões dos documentos do Office

Quem acompanha o Office há algum tempo deve ter notado que, a partir da versão 2007, os documentos ganharam novas extensões, com o sufixo x. Por exemplo, doc passou a ser docx, xls mudou para xlsx e assim por diante.

Mas não foi apenas a extensão que mudou. O conteúdo foi completamente reformulado. O formato mais antigo era do tipo binário, enquanto os novos são baseados em XML.

As vantagens do formato XML são inúmeras, a começar pela capacidade de qualquer ferramenta que trabalha com XML conseguir processar o documento ou pelo menos parte dele.

No que se refere à biblioteca Apache POI, isso também nos afeta diretamente, pois há APIs diferentes para trabalhar com os diferentes formatos de documentos.

Como veremos adiante, nem todos os formatos suportam todos os tipos de segurança.

Capacidades do POI

A conclusão dos meus testes foi a seguinte:

Tipo de documento Operação Proteção contra alteração do documento Proteção contra alteração de uma planilha Proteção contra Leitura (criptografia)
XLS
Leitura OK
Criação N/D* OK N/D
XLSX
Leitura OK
Criação N/D OK OK

Na leitura de um arquivo XLS ou XLSX, as proteções contra alteração são ignoradas, então não há o que testar.
N/D = Não disponível, isto é, não há suporte na API.
* Existe um método para proteger o documento, mas ele não surte efeito (bug).

Como pode ser visto, para documentos no formato legado XLS o POI suporta apenas a leitura de arquivos criptografados. Na criação, ele apenas consegue proteger parte do documento contra alteração.

Já para o formato mais novo XLSX, o POI consegue ler e criar arquivos criptografados. Entretanto, na criação do documento, não há um método para proteger o documento todo contra alteração.

Alguns fóruns sugerem uma possibilidade de contornar a falta do recurso de proteger o documento como um todo contra alteração. Para isso, basta criar um arquivo Excel com tal proteção e usá-lo como template para geração de um novo.

Implementação

Criptografia em arquivos XLS

O POI suporta apenas ler arquivos criptografados neste formato, isto é, que estão protegidos contra leitura.

O segredo é a classe Biff8EncryptionKey. Basta definir a senha através do método estático setCurrentUserPassword e depois ler o arquivo normalmente.

Exemplo:

try {
    Biff8EncryptionKey.setCurrentUserPassword(password);
    new HSSFWorkbook(input);
} finally {
    Biff8EncryptionKey.setCurrentUserPassword(null);
}

É bom não esquecer de remover a senha ao final para não atrapalhar futuras leituras.

Mesmo sendo um método estático, não deve haver problemas de concorrências, pois a documentação afirma que a senha é armazenada usando ThreadLocal.

Criptografia em arquivos XLSX

O POI implementa tanto a leitura quanto a criação de documentos criptografados neste formato.

Veja minha implementação para a leitura:

public class XlsxDecryptor {

    public static XSSFWorkbook decrypt(InputStream input, String password)
            throws IOException {

        POIFSFileSystem filesystem = new POIFSFileSystem(input);
        EncryptionInfo info = new EncryptionInfo(filesystem);
        Decryptor d = Decryptor.getInstance(info);

        try {
            if (!d.verifyPassword(password)) {
                throw new RuntimeException("Unable to process: document is encrypted");
            }

            InputStream dataStream = d.getDataStream(filesystem);
            return new XSSFWorkbook(dataStream);
        } catch (GeneralSecurityException ex) {
            throw new RuntimeException("Unable to process encrypted document",
                ex);
        }

    }

    private XlsxDecryptor() {
    }

}

E para criação:

public class XlsxEncryptor {

    public static void encrypt(InputStream input, OutputStream output, 
            String password) throws IOException {

        try {
            POIFSFileSystem fs = new POIFSFileSystem();
            EncryptionInfo info = new EncryptionInfo(fs, EncryptionMode.agile);

            Encryptor enc = info.getEncryptor();
            enc.confirmPassword(password);

            OPCPackage opc = OPCPackage.open(input);
            OutputStream os = enc.getDataStream(fs);
            opc.save(os);
            opc.close();

            fs.writeFilesystem(output);
            output.close();
        } catch (GeneralSecurityException e) {
            throw new RuntimeException(e);
        } catch (InvalidFormatException e) {
            throw new RuntimeException(e);
        }

    }

}

Proteção de partes de um documento

O POI implementa a proteção de uma planilha de um documento Excel e a proteção de células. Testei apenas a proteção de planilhas.

A implementação é muito simples, basta usar o método protectSheet. Veja o exemplo para o formato XLSX:

XSSFSheet sheet = workbook.createSheet();
sheet.protectSheet("54321");

E agora o equivalente para o formato XLS:

HSSFSheet sheet = workbook.createSheet();
sheet.protectSheet("54321");

Testes e exemplo

Antes de encerrar o artigo, vou acrescentar aqui dois dos testes unitários que implementei de forma que possam servir como exemplo de uso do POI e do meu projeto.

O primeiro cria um novo arquivo XLSX cripgrafado e em seguida lê o mesmo arquivo verificando se o valor foi gravado corretamente:

@Test
public void createXLSXOpenAndModifyProtected() throws IOException {
    System.out.println("createXLSXOpenAndModifyProtected");

    //creates sheet
    XSSFWorkbook workbook = new XSSFWorkbook();
    XSSFSheet sheet = workbook.createSheet();
    sheet.protectSheet("54321");

    XSSFRow row = sheet.createRow(0);
    XSSFCell cell = row.createCell(0);
    cell.setCellValue("Gravado");

    //saves sheet
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    workbook.write(bos);
    bos.close();
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());

    new File("target/.output-file/xlsx").mkdirs();
    WorksheetEncryptor.encrypt(
        bis, 
        new FileOutputStream("target/.output-file/xlsx/OpenAndModifyProtected.xlsx"), 
        DocumentType.XML, 
        "54321");
    bis.close();

    //read again and check
    XSSFWorkbook workbook2 = (XSSFWorkbook) WorksheetDecryptor.decrypt(
        new FileInputStream("target/.output-file/xlsx/OpenProtected.xlsx"),
        DocumentType.XML, 
        "54321");
    Assert.assertEquals("Gravado", workbook2.getSheetAt(0).getRow(0).getCell(0).getStringCellValue());
}

Em seguida, o segundo lê um arquivo XLS criptografado e verifica o valor da célula:

@Test
public void testOpenAndModifyProtectedXLS() throws IOException {
    System.out.println("### testOpenAndModifyProtectedXLS");
    HSSFWorkbook workbook = (HSSFWorkbook) WorksheetDecryptor.decrypt(
        getClass().getResourceAsStream("xls/OpenAndModifyProtected.xls"),
        DocumentType.LEGACY, 
        "12345");
    String conteudoCelula = workbook.getSheetAt(0).getRow(0).getCell(0)
        .getStringCellValue();
    Assert.assertEquals("Teste criptografia", conteudoCelula);
}

Código-fonte

Todo o código, inclusive os testes, está disponível em um projeto no meu GitHub.

Com o Maven você será capaz de executá-lo sem nenhuma configuração adicional. Basta clonar o repositório.

Instalando e configurando o Ubuntu linux numa máquina virtual

virtualbox-ubuntu

Neste tutorial, vamos instalar o Ubuntu, uma das distribuições linux mais populares da atualidade, numa máquina virtual.

Você poderá usar isso para várias finalidades. Aqui no blog, em breve, usaremos em artigos sobre Hadoop e Big Data.

Virtualização

Para quem não está acostumado com virtualização, uma máquina virtual (Virtual Machine, em Inglês) é um ambiente que simula um computador, com sistema operacional próprio, mas que você pode executar dentro do seu sistema atual.

Isso significa que você pode executar um sistema operacional linux dentro do seu Windows e vice-versa. Eu mesmo uso primariamente o Windows 7, mas tenho imagens com XP e diversas distribuições linux.

O sistema operacional principal da máquina é chamado de hospedeiro (host). Os sistemas operacionais usados dentro de máquinas virtuais no sistema hospedeiro são chamadas de sistemas convidados (guests).

Essa técnica tornou-se viável num passado não tão distante quando o hardware atingiu um bom nível de eficiência, inclusive hoje com tecnologia que torna a virtualização quase tão eficiente como um sistema tradicional.

A virtualização traz vários benefícios. O principal é possibilitar a criação da tão famigerada computação em nuvem (cloud computing). Além disso, as empresas que dependem de infraestrutura de TI tanto para desenvolvimento quanto para produção podem usufruir de maior facilidade para a criação de novos ambientes e servidores virtuais, além de flexibilidade para o gerenciamento. Desenvolvedores ou mesmo usuários domésticos como eu podem ter vários servidores com diferentes tecnologias em seu notebook pessoal, inicializados apenas de acordo com a demanda.

Existem ainda sites que disponibilizam ambientes com diversas tecnologias prontos para os administradores usarem em servidores. Um deles é o TurnKey Linux. Baixando imagens de discos virtuais relativamente pequenas, você tem um sistema pronto para uso e somente com o que é necessário para executar a tecnologia escolhida. Enfim, você pode ter um servidor pronto em uma máquina virtual em apenas alguns minutos.

VirtualBox ou VMWare Player?

Os programas gratuitos de virtualização para usuários domésticos mais conhecidos são o VirtualBox da Oracle e o VMWare Player. Ambos são bons produtos, maduros e em constante evolução. Mas com funcionalidades específicas um pouco diferentes, além de vantagens e desvantagens.

Como sou usuário de ambos posso dizer que na prática não há um ganhador absoluto. Depende do uso que fizermos deles. O VMWare, por exemplo, permite copiar e copiar um arquivo do sistema hospedeiro para o convidado e vice-versa. O VirtualBox, por sua vez, traz várias funcionalidades que o VMWare só disponibiliza na versão paga.

Para quem faz questão de uma solução mais completa e possui condições de arcar com as despesas, o melhor seria adquirir uma versão paga do VMWare. Já o usuário doméstico que está começando se dará muito bem com qualquer versão gratuita.

Aqui usaremos o VirtualBox. Mas se alguém optar pelo concorrente não encontrará tanta dificuldade em atingir o mesmo objetivo.

Funcionalidades interessantes do VirtualBox

Existem algumas funcionalidades bem legais quando usamos uma máquina virtual. Irei descrever algumas nas próximas linhas que estão disponíveis no VirtualBox.

Por exemplo, você pode pausar uma máquina virtual a qualquer momento através do menu Máquina > Pausar.

Também é possível salvar snapshots da máquina através do menu Máquina > Criar Snapshot. Sabe o que significa isso? Ao criar um snapshot, você tira uma “fotografia” ou “instantâneo” do sistema naquele momento. Então pode “pintar e bordar”, realizar testes, instalação de programas ou até vírus. Quando cansar da brincadeira, basta restaurar o snapshot e o sistema (disco e memória) voltarão ao estado salvo como se nada tivesse acontecido.

Caso em algum momento você deixe a máquina virtual em tela cheia ou o cursor do mouse seja capturado por ela de forma que você não consiga sair, não se desespere. A tela usada para liberar o mouse e também para algumas teclas de atalho é o CTRL da direita do seu teclado. Este é o padrão e você pode mudá-lo. Essa tecla especial é chamada tecla do hospedeiro, isto é, que permite acessar comandos no sistema hospedeiro. Por exemplo, CTRL+F alterna a máquina virtual entre modo de tela cheia e janela.

Outra funcionalidade interessante, embora deva ser usada com cuidado, é o modo Seamless. Com ele, os programas abertos no sistema dentro da máquina virtual “misturam-se” com a área de trabalho do sistema hospedeiro, dando a impressão de haver apenas um sistema operacional. Veja o seguinte exemplo de um terminal aberto no Ubuntu e exibido em seamless mode:

seamless-mode

Configuração de Hardware

Máquinas mais novas, como o Intel i7, possuem suporte em nível de hardware para virtualização. Entretanto, até algum tempo atrás essas capacidades eram desativadas por padrão. Isso chegava a impedir a virtualização de sistemas operacionais convidados de 64 bits.

Leia o manual da sua placa mãe e do seu processador e verifique se eles possuem suporte nativo para virtualização. Procure por algo como VT-x (Intel) ou AMD-V. Veja um exemplo da BIOS para um processador AMD:

5x3YW

E aqui outro exemplo para um processador Intel:

bios_enablt_vtx1

Lembre-se, sem o suporte nativo, você não será capaz de instalar um sistema operacional de 64 bits como convidado no VirtualBox. Entretanto, se não estou enganado, o VMWare consegue emular via software a virtualização de sistemas 64 bits, mas de qualquer forma o desempenho será sofrível.

Instalando o VirtualBox e as extensões

Acesse a página de downloads e baixe a versão correspondente ao seu sistema operacional.

dowload-virtualbox

Baixe também as extensões para o sistema convidado.

dowload-virtualbox-extension

As extensões trarão várias facilidades, tais como: redimensionamento automático da tela, melhor integração do mouse, compartilhamento de pastas automático entre o sistema hospedeiro e o convidado, uso da USB dentro da máquina virtual e muito mais.

Execute o primeiro arquivo baixado para instalar o VirtualBox. Em geral você não precisa alterar nenhuma configuração, então simplesmente avance até o final da instalação

virtualbox-install

Confirme ainda a instalação de todos os drivers, que serão usados para integrar seus dispositivos como mouse, teclado e rede com a máquina virtual.

Após concluir, execute também o outro arquivo para instalar as extensões do convidado (Guest Additions). O nome deve ser algo como Oracle_VM_VirtualBox_Extension_Pack-4.3.12-93733.vbox-extpack. O programa VirtualBox será aberto. Aceite o contrato para concluir a instalação.

Dando tudo certo, não se esqueça que o atalho adicionado no Menu Iniciar é “Oracle Virtual Box”.

virtualbox-open

Criando uma máquina virtual

Na tela principal do VirtualBox, clique no botão Novo.

virtualbox-main

Na tela de criação, digite “Ubuntu 14”. Note que os demais campos serão preenchidos automaticamente.

configure-vm-01

Clique em Próximo e selecione a quantidade de memória para seu novo ambiente. Aqui vou deixar com 2 Gigabytes (2048 Megabytes), mas uma dica é não ultrapassar 50% da memória total do seu computador.

configure-vm-02

Clique em Próximo. Nesta tela, você poderá criar um novo disco rígido virtual. Um HD virtual é simplesmente um arquivo grande que ficará no seu sistema de arquivos, o qual funcionará como se fosse um HD para o sistema da máquina virtual. A não ser que tenha outros planos, deixe marcada a opção para criar um disco novo.

configure-vm-03

Clique em Criar. Na próxima tela, você poderá escolher o formato do arquivo desse novo disco. Vamos deixar o formato nativo do VirtualBox, o VDI.

configure-vm-04

Clique em Próximo. Nesta tela você pode escolher entre duas opções:

  1. Dinamicamente alocado: nesta opção, o arquivo do disco virtual vai aumentando de tamanho somente quando novos arquivos forem gravados. Isso significa que se você criar um disco de 30 Gigabytes, mas a instalação do SO e os demais arquivos ocuparem apenas 2 Gigabytes, então o arquivo terá apenas 2 Gigabytes. O disco vai aumentando de tamanho na medida do uso até alcançar o limite de 30 Gigabytes.
  2. Tamanho fixo: nesta opção, um disco virtual de 30 Gigabytes vai ocupar todo esse tamanho no seu disco verdadeiro.

Já que economizar espaço nunca é demais, vamos deixar a primeira opção selecionada.

configure-vm-05

Clique em Próximo. Agora vamos selecionar o nome do arquivo e o tamanho do disco virtual.

configure-vm-06

Caso tenha mais de uma partição ou HD no seu computador, você pode mudar o local do arquivo do disco virtual. Em algumas situações já criei máquinas virtuais no meu HD externo. Porém, para este tutorial, vamos apenas deixar tudo como está, pois o padrão é suficiente.

Finalmente, clique em Criar.

Agora você tem um computador virtual para brincar!

virtualbox-virtualmachine-created

Instalando o Ubuntu

Antes de mais nada, acesse a página de downloads da versão desktop do Ubuntu e baixe a versão adequada para o seu computador. Neste tutorial, fiz o download da versão 64 bits, cujo nome do arquivo baixado é ubuntu-14.04-desktop-amd64.iso e possui 964 Megabytes.

ubuntu-download

Com a imagem do disco de instalação do nosso novo sistema operacional, podemos então iniciar a máquina virtual e a instalação.

Na tela principal, selecione a VM (máquina virtual) criada e clique em Iniciar.

Antes da inicialização da VM, o VirtualBox vai saudá-lo com uma tela solicitando o disco de boot. Isso ocorre porque ele verificou que o disco virtual está vazio.

Clique no botão à direita do campo e selecione o arquivo do Ubuntu anteriormente baixado.

vm-boot-disk-select

Clique em Iniciar e aguarde a inicialização da instalação do Ubuntu.

ubuntu-install-01

Você pode selecionar sua língua materna ou deixar em Inglês. Eu prefiro o Inglês porque em TI as traduções acabam por confundir mais que ajudar. Clique em Install Ubuntu ou Instalar Ubuntu, dependendo da sua escolha.

A próxima tela irá informar se o Ubuntu vai executar bem na máquina onde está sendo instalada. Além disso, há opções para já instalar as últimas atualizações e alguns softwares de terceiros. Selecione todas as opções e clique em Continue.

ubuntu-install-02

Agora há opções para formatar ou particionar o disco antes da instalação. Como temos um disco virtual dedicado, simplesmente selecione a primeira opção para formatá-lo e executar uma instalação limpa.

ubuntu-install-03

Clique em Install Now.

Na verdade, a instalação não vai começar ainda. Isso deve ter sido uma grande falha de design. A próxima tela contém a seleção da sua localidade. Digite o nome da capital do seu estado. Coloquei “Sao Paulo”.

ubuntu-install-04

Clique em Continue.

Na próxima tela você pode selecionar o tipo do seu teclado. Teste-o para ver se está ok e clique novamente em Continue.

ubuntu-install-05

Finalmente, digite seus dados de usuário, incluindo a senha, e clique em Continue para iniciar a instalação de verdade.

ubuntu-install-06

Aguarde o processo de instalação.

ubuntu-install-07

Ao final, uma caixa de diálogo vai aparecer informando que o sistema deve ser reiniciado. Clique em Restart Now.

Nota 1: enquanto fazia este tutorial, o Ubuntu travou e não reiniciou corretamente. Então, fui até o menu Máquina > Reinicializar para forçar um reset.

Nota 2: a instalação do Ubuntu ejetou automaticamente o disco de instalação virtual do Ubuntu. Se estiver instalando outro sistema operacional que não faça isso, use o menu Dispositivos > Dispositivos de CD/DVD > Remover disco do drive virtual para não iniciar a instalação do sistema novamente por engano.

Pronto, o sistema está instalado e pronto para uso.

ubuntu-installed

Melhorando a integração entre sistema hospedeiro e convidado

Note que a janela do ubuntu ficou bem pequena, quase inutilizável. Vamos resolver isso!

Lembra que instalamos as “extensões do convidado” (Guest Additions) no VirtualBox? Elas facilitarão o uso da máquina virtual de várias formas, mas falta a parte da instalação no sistema convidado. Isso ocorre para que o VirtualBox consiga “conversar” com o SO que está na máquina virtual.

Para fazer isso, devemos seguir as instruções da documentação do VirtualBox que nos dá alguns comandos.

Vamos abrir o terminal de comandos clicando no primeiro botão à esquerda (equivalente ao “Iniciar” do Windows) e pesquisando na caixa de busca por “terminal”.

search-terminal

Se nada mudou no VirtualBox ou no linux desde que escrevi este tutorial, as instruções do Guest Additions para o Ubuntu consistem nos seguintes comandos:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install dkms

Nota: um usuário comentou que não conseguiu executar o último comando com sucesso, tendo substituído por sudo apt-get install virtualbox-guest-dkms. Isso pode ser necessário se estiverem sendo usadas diferentes configurações ou outras versões do Ubuntu ou ainda outras distribuições linux.

O comando sudo que prefixa os demais não está na documentação, mas é necessário se você não está executando o terminal com privilégios de superusuário (administrador).

O primeiro comando é apt-get update. Ele irá atualizar o índice de pacotes do Ubuntu. Dessa forma ele saberá as últimas versões de todos os seus componentes e programas. Após digitar o comando, o sistema irá solicitar a senha do usuário e então executar a ação.

guest-additions-01

O próximo comando é apt-get upgrade. Ele vai efetivamente instalar todas as atualizações do sistema. Após entrar o comando, o Ubuntu vai solicitar algumas confirmações. Pressione Y (yes) para confirmar a atualização e aguarde.

guest-additions-02

Após a atualização do sistema, executaremos o último comando: apt-get install dkms. Este comando vai instalar o pacote dkms, que possibilita a módulos do kernel serem atualizados independentemente. O Guest Additions precisa disso porque ele é um módulo do Kernel e é atualizado com frequência, caso contrário seria necessário recompilar o Kernel do linux a cada atualização.

guest-additions-03

O comando vai pedir a confirmação da instalação. Pressione Y quando necessário.

Neste momento já cumprimos todos os pré-requisitos para a instalação do Guest Additions. Então vamos à instalação em si.

Acesse o menu Dispositivos > Inserir imagem de CD dos Adicionais para Convidado....

guest-additions-04

Ao acionar o menu, uma imagem de CD do VirtualBox será montada no sistema do Ubuntu e a execução automática (auto run) ocorrerá. Uma mensagem de confirmação será exibida.

guest-additions-05

Clique em Run. A senha será novamente solicitada. Digite-a e aguarde o final da instalação.

guest-additions-06

Finalmente, vamos reiniciar o sistema para ativar o módulo que acabamos de instalar. Clique no botão do sistema no canto superior direito do Ubuntu e selecione a opção Shut Down....

guest-additions-07

Na tela que vai abrir, clique no botão da esquerda para reiniciar.

Após a reinicialização, você poderá, entre outras coisas, redimensionar a janela do VirtualBox como quiser e o Ubuntu irá se ajustar a esse tamanho. Legal, né? Esta é a opção Visualizar > Redimensionar Tela Automaticamente que estava desabilitada anteriormente, mas agora veio ativada por padrão.

Palavras finais

Virtualização é um conceito importantíssimo no mundo de hoje. Desenvolvedores de software não precisam ser especialistas em virtualização, mas devem ter bons conceitos sobre como isso funciona e devem saber usar todos os benefícios a seu favor.

Criar máquinas virtuais não é difícil, basta ter uma base sobre o assunto e saber usar as ferramenta já existentes, que estão cada vez mais intuitivas e poderosas.

Os benefícios da criação de máquinas virtuais são inúmeros, a começar por podermos usufruir de uma variedade de ambientes dentro de um único computador.

Em futuros artigos, pretendo trazer tutoriais envolvendo Hadoop, inclusive com a criação de um cluster, cada um em uma máquina virtual, para processamento Big Data.

Dissecando o padrão de projetos Singleton

singleton O padrão de projetos Singleton consiste em uma forma de garantirmos que teremos uma única instância de uma determinada classe no programa atual.

Por exemplo, em um programa Java para desktop podemos criar um Singleton da classe que gerencia a conexão com o banco de dados.

Este é um dos design patterns mais simples que existem, mas ele possui algumas nuances importantes de se entender do ponto de vista de implementação.

Implementação inicial em Java

Para garantirmos uma única instância de uma classe, a abordagem mais comum é criar um método estático que retorne sempre o mesmo objeto. Exemplo:

public class Singlegon {
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
    private Singleton() { }
}

O atributo estático instance armazena o objeto criado para retornar a cada chamada de getInstance().

O trecho private Singleton() { }; é um construtor privado, garantindo que nenhuma outra classe poderá criar inadvertidamente uma instância desta.

Postergando a criação do objeto

Tudo ok, mas nem sempre queremos criar o objeto no modo agressivo (eager), isto é, instanciá-lo logo que a classe é carregada. Em muitas situações, é desejável postergar a criação do objeto até a primeira chamada. Exemplo:

public class Singlegon {
    private static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

No código acima, o primeiro acesso ao método getInstance() irá disparar a criação do objeto, que será então retornada nas demais chamadas.

Problemas de sincronizção

O problema do código acima é que se houver mais de uma chamada concorrente no primeiro acesso ao método getInstance() ele pode criar duas instâncias de Teste. Duas threads poderiam entrar dentro do if, certo?

A solução mais básica para isso é sincronizar o método:

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

O problema desta abordagem é que todas as chamadas estarão sujeitas a bloqueios, deixando a execução geral do programa mais lenta. Imagine um método assim num servidor de aplicação com vários usuários acessando o sistema! É terrível.

Uma solução melhor seria um bloco synchronized dentro do if, assim:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            instance = new Singleton();
        }
    }
    return instance;
}

Isso resolve o problema da sincronização em todos os acessos, mas é uma solução “ingênua”, pois na verdade voltamos ao problema inicial. Como o if não está sincronizado, duas threads diferentes podem entrar no bloco de criação ao mesmo tempo e, mesmo com a sincronização, elas retornarão instâncias diferentes quando instance == null.

Então, a solução mais “pura” para o singleton pattern seria acrescentar uma verificação dupla, assim:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

Com esta última abordagem garantimos que não haverá perda de desempenho por causa de sincronização desnecessária do método inteiro.

Além disso, garantimos uma única instância de Teste, pois mesmo que duas chamadas concorrentes entrem dentro do primeiro if, temos uma nova verificação sincronizada.

No pior caso, se houver duas ou mais chamadas concorrentes no primeiro acesso a getInstance (quando INSTANCE ainda é null), apenas estas primeiras chamadas serão sincronizadas, sendo que após a primeira atribuição de INSTANCE, nenhuma chamada posterior será sincronizada.

Além do Singleton: padrão Registry

Alguns argumentam que o padrão Singleton está depreciado e deve ser abandonado. De certa forma eu concordo, pois em ambientes onde múltiplas threads e múltiplas aplicações executam concorrentemente, ter apenas um objeto quase nunca é desejável.

O padrão de projeto Registry permite armazenarmos uma coleção de objetos, cada um contendo um identificador ou um escopo específicos. É como se limitássemos o escopo do Singleton de “um objeto por programa” para “um objeto por qualquer escopo que quisermos“.

A implementação varia muito, mas podemos encontrar exemplos claros desse padrão em:

  • Threadlocal, que permite armazenar valores para uma thread, ou seja, um Singleton para cada uma.
  • HttpSession, que retorna sempre o mesmo objeto para cada usuário do sistema web, ou seja, um Singleton por usuário.
  • Frameworks de Injeção de Dependências como Spring ou CDI, os quais gerenciam a criação de objetos em diferentes escopos e permitem inclusive usar o padrão Singleton declarativamente.

Não entrarei em detalhes sobre o Registry neste artigo.

Indicações de Leitura

Uma leitura mais completa sobre o assunto está no Head First Design Patterns (Use a Cabeça! Padrões de Projeto). Embora tendo suas falhas, este é um livro muito bom para quem ainda está começando a entender Padrões de Projetos.

Outros detalhes interessantes sobre Singleton, como variações de implementação, podem ser encontrados na Wikipédia (em Inglês).

Uma breve definição do padrão Registry pode se encontra no catálogo de padrões do Martin Fowler.

Is TDD dead?

Test Driven Development is a very popular software development methodology focused (guest what?) on tests.

However, despite of being well known, there’s some criticism on this methodology from prominent professionals and thinkers.

A few days ago, there was a very interesting talk about this issue between Martin Martin Fowler (that elaborated the Dependency Injection definition), Kent Beck (creator of TDD and JUnit framework), and David Heinemeier Hansson (creator of Ruby on Rails).

Let’s review some highlights from that talk in the next topics. But let’s start with a brief review of TDD…

How TDD works

Different from the “traditional” development life-cycle composed by Specification, Implementation and Test, TDD begins testing the interfaces of the system.

At first, all tests will fail. The developer’s goal is to make they succeed. In theory it guarantee the focus on what really should be done and provides a concrete measure of progress.

In general, TDD adopts Unit Tests. Both are different things, but closely related.

Unit Tests

Each Unit Test tests only one function or method of the system. Often they are automated.

It seems too simple, but implies that the routine under test can’t depend on other routines, otherwise the test is subjected to third part failures.

This way, in order to test routines that naturally depend on other classes and methods, resources like databases and files, and other stuff, developers need to create simulations of these elements so the routine works without them during the test.

This is the mock idea, i.e., something that mimics the original dependencies.

For instance, a method that would normally return a value from database could be replaced by a method that returns a fixed value for a particular test. Each language, framework, and tool should provide proper ways for doing that. This is also a manner to assess the quality of these tools!

Benefits of TDD

Writing tests in advance brings various advantages:

  • Helps in the understanding of the requirements, since you have to understand them and also what the system should do in order to write down tests.
  • Provides a clear goal in the development, that is, to make all tests to pass.
  • Increases the visibility of what is or not ready, since the tests are generally more reliable indicators of progress than something “implemented, but not tested”.
  • The automation enables Continuous Integration with some level of guarantee the system won’t break, since all unit tests can be executed over and over as regression tests.

Disadvantages

As a general rule, everything has its advantages and also its negative points. We always have to take care with “evangelists” that will make some technology or methodology looks like it hasn’t problems or has zero impact.

This also applies to TDD. Let’s see some of its reservations:

Increases the effort

It’s necessary to spend much more time creating tests and mocks, not to mention the Continuous Integration infrastructure. Of course it can be considered as an investment in quality instead of a penalty, but in practice not everyone can afford the overhead.

Increases the complexity

The architecture becomes more complex since Dependency Injection and other decoupling techniques are abundantly used everywhere.

Changes in requirements are much more costly, since you need to rewrite the code of the system itself plus all the tests and mocks involved.

If the engineer don’t know well what he’s doing, the system will end with tons of kludges.

Intrusiveness in the main code

In some situations, you’ll have to code in a specific way in order the code can be tested later. The application architecture is then affected by outside unnatural factors.

In short, the design is influenced by tests, what is not desirable since is one more thing to be considered and to grab the architect’s attention.

Testing everything is almost impossible

Many technologies are hard to test independently or even in integration tests.

Think about user interfaces, for example. How many technologies allow us to run it in a decoupled way? There’s tools like Selenium for web apps, but they also brings tons of other limitations and dependencies.

Is TDD dead?

We just saw every benefit comes at a cost. This is the main point about TDD being dead. Is the cost of testing everything and creating mocks for every dependency worthy? Let’s look at some comments about the aforementioned talk.

David Hansson starts arguing that lots of people can’t do TDD because of the nature of their work. He talks about the need of numerous mocks and how it makes coding more difficult.

Kent Beck continues and tell us about situations where the team couldn’t refactor their code because the tests were so coupled to the implementation, with two or three levels of mocks, that any modification affected countless of them. He also asserted TDD fits better certain scenarios, for instance, when requirements are clear enough so you can write them in form of tests directly. On the other hand, there are situations when the developer finds out little by little what he actually should do. This is recurrent in framework development and algorithms for analyzing non-structured data.

Fowler argues the major benefit of TDD is a self-testing code. But this could be achieved in other ways. He also talks about those who say “you aren’t doing unit tests right” because of some dependencies during the test. According to him, the “purity” of the definition is not so important, but the test should work and be useful. Furthermore, there’s alternative definitions of Unit Test.

Conclusions

It’s important to comprehend TDD is not the silver bullet of software development. It brings various potential challenges and problems, mainly if there’s not enough experienced people in the team.

Tests are essential, but for each project we could have different kinds of tests, whose cost-benefit should be evaluated for each individual case.

Tests shouldn’t be the goal in itself, even in TDD. Their true goal is guarantee we’re delivering what the client really needs.

Time and effort invested in tests also should be pondered. It affects directly the quality, but the quality has its price. Who will pay for it?

About unit testing, we shouldn’t pursue only purity. The important is the test tests, that is, it works for that situation.

Finally, a general rule could be: use TDD moderately.


Here is the link of the Hangout with those great guys:

Is TDD dead?

O TDD está morto?

Test Driven Development (Desenvolvimento Orientado a Testes) é uma metodologia de desenvolvimento de software bastante popular, que é centrada (adivinhe) em testes.

No entanto, apesar de muito propagada, há várias críticas a essa metodologia por profissionais e pensadores de destaque.

Por esses dias rolou uma conversa bastante interessante sobre o assunto entre, nada mais nada menos, que Martin Fowler (que elaborou a ideia de Injeção de Dependência), Kent Beck (idealizador do TDD e do framework JUnit) e David Heinemeier Hansson (criador do Ruby on Rails).

Vamos analisar os pontos altos da conversa nos próximos tópicos. Mas antes, uma breve revisão sobre TDD…

Como o TDD funciona

Ao contrário do processo “tradicional” de desenvolvimento que envolve Especificação, Implementação e Testes, o TDD começa pelos testes em cima das interfaces do sistema.

A princípio todos os testes irão falhar. O objetivo do desenvolvedor é fazer com que eles sejam executados com sucesso. Em tese, isso garante o foco no que realmente deve ser feito e fornece uma medida mais real do progresso.

Os testes utilizados no TDD geralmente são Testes Unitários. Ambos os conceitos estão muito relacionados, mas não são sinônimos.

Testes Unitários

Testes Unitários são aqueles onde uma única função ou método do sistema é testada de cada vez. Em geral esses testes são automatizados.

Isso pode parecer simples, mas implica em que a rotina em questão não pode depender de outras rotinas, caso contrário o teste é sujeito a falhas de terceiros.

Dessa maneira, a fim de testar as rotinas que naturalmente possuem dependências de outras classes e métodos, de recursos como bancos de dados ou arquivos e outros, os desenvolvedores criam simulações desses elementos para que a rotina a ser testada funcione, porém não dependa diretamente desses outros elementos durante o teste.

Este é o conceito de mock, isto é, algo que imita o elemento original.

Por exemplo, um método que normalmente retorna um valor do banco de dados poderia ser substituído por um método que retorna um valor fixo num teste em particular. Cada linguagem, framework e ferramenta deve prover formas adequadas para se fazer isso, sendo inclusive uma forma de medir a qualidade da mesma.

Benefícios desta abordagem

Escrever os testes antecipadamente e então desenvolver com base neles traz várias vantagens:

  • Melhora o entendimento dos requisitos, já que para escrever o teste é necessário que o desenvolver entenda logo de início o que o sistema deve fazer.
  • Provê um objetivo claro a ser alcançado durante o desenvolvimento, isto é, fazer os testes executarem sem erros.
  • Aumenta a visibilidade do que está ou não pronto, já que os testes servem como indicadores mais confiáveis do que algo que está implementado e “só falta testar”.
  • A automação dos testes possibilita a Integração Contínua com certa garantia do funcionamento do sistema, já que todos os testes unitários podem ser reexecutados como testes de regressão sempre que necessário.

Desvantagens

Como regra geral, tudo que traz vantagens tem seus pontos negativos. Sempre temos que tomar cuidado com certos “evangelistas” que irão fazer uma tecnologia ou metodologia parecer não ter um lado negativo ou impacto zero.

Isso se aplica também ao TDD. Vejamos algumas considerações sobre ele:

Aumento do esforço

Será necessário gastar muito tempo para criar os testes e todos os mocks, além da infraestrutura de Integração Contínua. É claro que isso pode ser visto como um investimento em qualidade ao invés de uma pena a ser paga, mas na prática nem todos podem se dar a esse luxo.

Aumento da complexidade

A arquitetura acaba sempre mais complexa, usando muita Injeção de Dependências e outras técnicas de desacoplamento.

Mudanças nos requisitos são muito mais custosas, já que além do código do sistema em si, todos os testes e mocks envolvidos deverão ser reescritos.

Se o arquiteto de software não souber o que está fazendo, a arquitetura do sistema pode acabar cheia de gambiarras.

Intrusividade no código principal

Em muitas situações, será necessário programar de uma forma específica para que um certo código possa ser testado.

A arquitetura da aplicação muitas vezes acaba sendo então afetada por elementos e fatores que, de outra forma, não fariam parte de sua natureza.

Enfim, o design da aplicação acaba sendo influenciado pelos testes, o que nem sempre é desejável, já que é mais uma coisa a ser levada em conta e pode desviar o foco do arquiteto.

Testar tudo é quase impossível

Muitas tecnologias são difíceis de testar com testes unitários ou mesmo com testes de integração.

Pense na interface do usuário, por exemplo. Quantas delas possibilitam a execução da interface de forma “desacoplada”? Existem ferramentas como o Selenium para sistemas web, mas com elas também vêm diversas outras limitações e dependências.

O TDD está morto?

Vimos que todo benefício traz um custo associado. É este todo o ponto da discussão sobre o TDD estar morto. O custo de testar tudo e criar os mocks necessários para todos os testes vale a pena? Vejamos alguns comentários a seguir.

David Hansson inicia a argumentação dizendo que muitas pessoas não conseguem trabalhar com TDD porque não faz muito sentido para determinados tipos de trabalho. Ele toca no ponto da necessidade de se ter muitos mocks e que isso acaba tornando a codificação difícil.

Kent Beck aproveita o gancho e relata de casos onde a equipe não conseguia refatorar o código porque os testes estavam tão acoplados com a implementação, com dois ou três níveis de mocks, que qualquer alteração afetava inúmeros deles. Ele também afirma que há cenários onde o TDD se aplica melhor, por exemplo, quando os requisitos do software a ser desenvolvido são claros o suficiente para escrevê-los em testes diretamente. Por outro lado, há situações onde simplesmente o desenvolvedor vai descobrindo aos poucos aquilo que deve fazer. Isso é recorrente na criação de frameworks ou algoritmos que precisam analisar dados não estruturados.

Fowler argumenta ainda que a maior vantagem do TDD é um código que consegue testar a si mesmo. Só isso poderia também ser alcançado de outras formas. Ele também comenta sobre aqueles que dizem que “você não está fazendo teste unitário de verdade” porque existem algumas dependências durante o teste. Segundo ele, o importante não é a pureza de uma definição, mas que o teste funcione e seja útil, até porque há variações da definição de testes unitários.

Conclusões

É importante entender que TDD não é a “bala de prata” do desenvolvimento de software. Ele traz consigo vários desafios e problemas em potencial, principalmente se não há pessoas suficientemente experientes na equipe.

Testar é essencial, mas para cada projeto podem haver diferentes formas de testes, cujo busto-benefício deve ser avaliado para cada caso.

O teste não deve ser um objetivo em si mesmo, ainda que adotemos o TDD. Ele apenas tem o objetivo de garantir que estamos entregando o que o cliente pediu.

O tempo e o esforço que são investidos em testes também devem ser avaliado. Isso afeta diretamente a qualidade, mas a qualidade tem o seu preço. Quem irá pagar o preço da qualidade?

Quanto a testes unitários, não devemos buscar toda a pureza possível. O importante é que ele teste, ou seja, funcione para aquela situação específica.

Concluindo, o conselho em geral é: use TDD com moderação.


Para quem consegue entender Inglês, aqui vai o link do Hangout com essas grandes personalizados:

Is TDD dead?

Análise e Design Orientados a Objetos – uma visão prática

UML_logo Estudantes e praticantes de Engenharia de Software geralmente tem dúvidas sobre como modelar um sistema de forma efetiva.

Atualmente, a abordagem mais recomendada é a Orientada a Objetos, preferencialmente usando UML, já que essas técnicas são amplamente reconhecidas e propagadas.

O problema é que muita confusão ocorre por falta de conhecimento sobre os objetivos da modelagem de sistemas, as fases de um projeto de desenvolvimento de software e os diferentes níveis de modelagem possíveis. Por exemplo: quais classes devem ser incluídas num Diagrama de Classes? Devemos colocar as classes do framework? Como identificar as classes necessárias em um sistema?

O ponto de partida é não misturar a Análise do problema com o Design (projeto) da solução ou com a Implementação tecnológica.

Em resumo, vamos analisar nos tópicos abaixo como aplicar a modelagem orientada a objetos em diferentes fases de um projeto, tendo em mente alcançar objetivos concretos com a modelagem, além das diferentes aplicações dos modelos utilizados.

Análise

O analista é o profissional responsável por identificar um problema a ser resolvido ou necessidade a ser atendida e elicitar os requisitos para a criação de uma solução.

O conjunto de requisitos define o que o sistema deve fazer para atender às necessidades identificadas.

Baseando-se nos requisitos, o analista continua o processo de Análise identificando em alto nível de quais funcionalidades o sistema deverá possuir para atender aos requisitos.

Uma solução comum para mapear cada funcionalidade é através de Casos de Uso (não confundir com o Diagrama da UML). Um Caso de Uso é uma espécie de passo-a-passo da interação entre usuário e sistema, embora esse conceito possa variar um pouco. Além disso, geralmente ele descreve as pré-condições necessárias para a correta execução e as pós-condições, que são os resultados da ação realizada.

Note que ainda estamos em alto nível e nada aqui tem a ver com a solução tecnológica.

Continuando, o analista treinado em Orientação a Objetos e UML irá modelar o conhecimento sobre o domínio e o problema utilizando os diagramas adequados, que geralmente são: Diagrama de Caso de Uso, Diagrama de Atividades, Diagrama de Classes e Diagrama de Estados.

O Diagrama de Caso de Uso é uma representação visual simples das interações do sistema com o mundo externo. Os atores que interagem com o sistema são representações de usuários, outros sistemas ou qualquer entidade externa ao sistema que se comunique com o mesmo. Este diagrama não exclui a necessidade de mapear em detalhes os casos de uso conforme mencionado anteriormente.

O Diagrama de Atividades representa os passos do caso de uso numa espécie de fluxograma, incluindo bifurcações de cenários alternativos, cenários de erro, etc. Nem todos os cenários precisam ser representados no mesmo diagrama.

O Diagrama de Classes, neste estágio de um projeto, deve incluir apenas as classes de domínio, sem qualquer referência à tecnologia que será usada na implementação. Poderíamos chamar este diagrama de Diagrama de Classes de Domínio. A função do diagrama é representar as entidades necessárias e o relacionamento entre elas. Em suma, é a forma moderna e orientada a objetos do Diagrama de Entidade-Relacionamento (DER), embora ambos possam ser usados concomitantemente. No entanto, o DER geralmente é associado à modelagem estruturada.

O Diagrama de Estados é usado para as entidades do sistema que seguem um fluxo de estados. Por exemplo, uma parcela pode estar “em aberto”, “liquidada”, “em atraso”, “em prejuízo”. Este diagrama representa os estados e como ocorrem as transições entre eles.

Com tudo isso, o analista pode validar a solução verificando se as classes e os casos de uso atendem aos requisitos iniciais. Por exemplo, se houver um requisito de que “o gerente poderá extrair um relatório com o total de produtos vendidos no mês”, o analista deve olhar se a classe Produto possui um relacionamento “navegável” com Venda e ItemVenda e se é possível extrair a informação de totalização das vendas de forma lógica. Ele também pode adicionar os métodos e atributos importantes às classes de modo a atender aos requisitos.

Design (Projeto)

Com base em todas essas informações, entram em ação os arquitetos e desenvolvedores para propor uma solução tecnológica para o problema. Isso não necessariamente vem em sequência, muito pode ocorrer em paralelo.

Os projetistas técnicos poderão criar vários outros diagramas para representar o que será implementado. O Design pode ser feito de forma agnóstica, isto é, sem considerar quais tecnologias, frameworks e bibliotecas serão usadas. No entanto, creio que é mais produtivo modelar a divisão de componentes e classes já pensando um pouco na implementação, de forma a não gerar mais um gap de informação.

Os diagramas mais relevantes e geralmente usados são:

  • Diagrama de Componentes: representa a divisão em alto nível dos componentes principais do sistema. A divisão não representa a estrutura de pastas dos arquivos do projetos, mas é uma divisão lógica de responsabilidades.
  • Diagrama de Deployment: uma representação do ambiente onde o sistema será executado, incluindo servidores, bancos de dados, replicação, proxies, etc.
  • Diagrama de Sequência: para uma determinada ação no sistema, este diagrama representa a interação entre diversos objetos através das chamadas executadas e do retorno, permitindo visualizar a sequência de chamadas no decorrer tempo.

Note que cada um dos diagramas citados podem ser produzidos para vários casos diferentes. Quando falamos em Diagrama de Classes ou Diagrama de Componentes, não há necessariamente um único diagrama que representa o sistema como um todo. A representação pode ser feita em níveis diferentes, por exemplo, um mostrando os componentes gerais do sistema e outros diagramas mostrando a estrutura interna de cada componente individualmente. A representação também pode ser feita em contextos diferentes, por exemplo, vários diagramas para representar apenas o necessário para uma funcionalidade importante, ignorando classes e pacotes não relevantes naquele contexto.

Implementação

A implementação deve seguir o que foi definido no Design, porém não significa que cada método, classe, pacote e componente deve ser mapeado um-para-um no projeto “físico”, em seus arquivos e estrutura de diretórios.

O programador deve ter a liberdade de encontrar a melhor solução para atender ao que foi solicitado com a estrutura que ele desejar. Seria péssimo, do ponto de vista de boas práticas, impor cada detalhe do que deve ser implementado. Se isso fosse possível, não precisaríamos de programadores, mas de um gerador de código.

Construindo a ponte

Ao estudar com atenção as “fases” (entre aspas porque não são uma sequência linear) de um projeto de desenvolvimento de software, é possível notar que existe um grande salto (gap) entre cada uma delas. Uma analogia comumente usada nos livros de Engenharia de Software consiste em construir uma ponte entre o que o cliente precisa e a solução tecnológica.

Ainda hoje, a Engenharia de Software é uma disciplina um tanto imatura. Não temos uma forma padronizada de construção como a Civil ou Elétrica para nos apoiar. A validade de um Modelo de Análise, um Modelo de Design ou da solução implementada depende quase exclusivamente de fatores humanos, como a capacidade de comunicação e entendimento dos analistas, além da capacidade técnica dos desenvolvedores.

Não existem regras definitivas sobre como e em qual nível modelar, assim como não há regras sobre como traduzir uma necessidade em um requisito, um requisito em um modelo e um modelo numa implementação.

A UML foi um grande avanço, mas os diversos diagramas sempre variam em nível de detalhe, abrangência e muitos outros fatores de projeto para projeto, de equipe para equipe e de indivíduo para indivíduo.

Princípios de modelagem

Mesmo com as últimas afirmações acima, não quero ser pessimista. Embora não haja uma resposta definitiva para a modelagem de sistemas, existem alguns princípios que podem nos guiar:

  • Coloque a comunicação em primeiro lugar. O objetivo de um diagrama é comunicar informação e não simplesmente ser um espelho do código. Se um diagrama não comunica algo útil, não perca tempo com ele. Considere sua equipe e o seu projeto e faça os diagramas que forem relevantes com os detalhes relevantes para que as pessoas saibam o que estão fazendo. O seu time consegue se reunir numa mesa e discutir um diagrama, rabiscando-o e usando-o como base para a conversa?
  • Não faça diagramas de tecnologias específicas. Se alguém quiser saber como Servlets, Rails ou Django funcionam, é melhor comprar um livro. Você só vai confundir as pessoas. Já vi muitos diagramas por aí que nada mais são do que o modelo MVC com nomes diferentes.
  • Verifique se o diagrama atende os requisitos. O seu diagrama deve ser útil não só para entender o que deve ser feito, mas também para validar se a sua solução atende ao que o cliente precisa. Faça testes mentais lógicos, olhando para as classes, métodos e relacionamentos, verificando se elas tem motivo de estarem ali, se para um certo cenários você consegue extrair os dados necessários, etc.

Como mencionei algumas vezes, a Análise, o Design e a implementação provavelmente serão feitas muitas vezes durante o ciclo de desenvolvimento. Não espere ter tudo certo no começo. Investir tempo demais em detalhamento é ruim, vários autores já alertaram.

Identificando as classes necessárias

Existe um método para identificação de classes de domínio em potencial que consiste na análise de um texto à procura de substantivos. Esta técnica pode ser útil quando você não tem ideia do que está fazendo e quer algumas ideias iniciais. No entanto, é péssima porque em muitos lugares é ensinada como uma forma “burra” de extrair informação.

Volta e meia ouço alguém com a velha ideia de criar um interpretador mágico, um tipo de Inteligência Artificial, capaz de gerar um sistema com base num texto descrevendo as necessidades do usuário. Devaneios à parte, é melhor nos concentrarmos no que é real hoje.

Aliás, é comum usarmos a palavra “extrair” ou “levantar” indevidamente. Quando criamos um sistema, não extraímos ou levantamos os requisitos e as classes necessárias como se elas já existissem ali, ocultas de alguma forma.

Quanto aos requisitos, o termo “elicitar” é mais adequado, com o sentido de descobrir e descrever. Quanto às classes, nós simplesmente decidimos de forma espúria quais delas o sistema deverá conter de modo a atender às necessidades. Se verificarmos que elas não atendem aos requisitos, nós as modificamos para que o façam.

Tudo isso trata-se de um processo criativo e não de um processo de extração como se faz com matéria prima. Por “criativo”, não pense em arte pós-moderna, mas num processo criativo metódico e até científico.

Minha recomendação é identificar, através dos requisitos, quais dados são necessários para que o sistema funcione, assim como o relacionamento entre elos. Já dizem os DBAs: os dados são o coração do sistema. Assim conseguimos as classes que representam as entidades necessárias.

Depois, com base nas funcionalidades que o sistema deve ter, pode-se definir classes que serão responsáveis por tratar essas funcionalidades.

Considerações finais

A Engenharia de Software ainda possui muitos desafios pela frente para se tornar realmente uma engenharia no sentido completo da palavra.

Muitos hoje (inclusive eu) consideram-se mais artesãos tecnológicos do que engenheiros propriamente ditos. Isso é bom em certo sentido, mas também abre muitas brechas para as “artes abstratas”. 😉

Enfim, não há uma resposta definitiva para o sucesso na modelagem de um sistema, mas espero ter exemplificado bem um caminho que aprendi ao longo de alguns anos estudando e refletindo sobre como desenvolver software adequadamente, não apenas do ponto de vista técnico.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Distributed transactions and concurrent processing with Atomikos

distributed Atomikos is a piece of software written in Java that, among other things, implements JTA (Java Transaction API) e XA (eXtended Architecture, supports distributed transaction processing).

In general, a transaction is associated to the current thread, so the methods invoked during a request in a JEE server can share the same transaction.

However, an interesting question on StackOverflow raised the possibility of dividing an atomic operation composed by different tasks between threads with a single global transaction.

Well, in order to bypass the default architecture we’ll need invoke Atomikos XA API directly to include the various Data Sources from each thread in the main transaction.

I did a simple example implementing that. The project is available on my GitHub account.

Implementation

Firstly, we have the DataSource and TransactionManager initialization using Atomikos API in class AtomikosDataSource. Here is the relevant excerpt:

// Atomikos implementations
private static UserTransactionManager utm;
private static AtomikosDataSourceBean adsb;

// initialize resources
public static void init() {
    utm = new UserTransactionManager();
    try {
        utm.init();
        adsb = new AtomikosDataSourceBean();
        adsb.setMaxPoolSize(20);
        adsb.setUniqueResourceName("postgres");
        adsb.setXaDataSourceClassName("org.postgresql.xa.PGXADataSource");
        Properties p = new Properties();
        p.setProperty("user", "postgres");
        p.setProperty("password", "0");
        p.setProperty("serverName", "localhost");
        p.setProperty("portNumber", "5432");
        p.setProperty("databaseName", "postgres");
        adsb.setXaProperties(p);
    } catch (SystemException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
}

After that, I implemented the thread named Processamento (Processing) that receives the main transaction instance (Transaction). The interface Callable defines this thread as some kind of task that returns an Integer value. Here is the code:

private static class Processamento implements Callable<Integer> {

    private int id;
    private boolean falhar; //fail or not?
    private Transaction transaction;

    public Processamento(int id, boolean falhar, Transaction transaction) {
        this.falhar = falhar;
        this.transaction = transaction;
        this.id = id;
    }

    public Integer call() throws Exception {
        if (falhar) {
            //fail unexpectedly
            throw new RuntimeException("Falhou inesperadamente!"); 
        }

        //enlist xa connection
        XAConnection xac = AtomikosDataSource.getDS().getXaDataSource().getXAConnection();
        synchronized (transaction) {
            transaction.enlistResource(xac.getXAResource());
        }

        //normal execution, update row with OK
        Connection c = xac.getConnection();
        Statement s = c.createStatement();
        s.executeUpdate("update teste set processado = 'ok' where id = " + id);
        s.close();
        c.close();

        //delist xa connection
        synchronized (transaction) {
            transaction.delistResource(xac.getXAResource(), XAResource.TMSUCCESS);
        }
        return id;
    }

}

Notice, instead of using JTA, I’m using Atomikos XA API directly.

The call to AtomikosDataSource.getDS().getXaDataSource().getXAConnection() gets a XA connection, which is added to the main transaction with the command transaction.enlistResource(xac.getXAResource()). This operation is called enlistment. At the end of the process, there is a delistment.

I synchronized a few commands because I got random NullPointerException errors during tests. I didn’t investigate if it’s a bug or by design, that is, Transaction object is not synchronized by default.

Finally, I implemented a method to create five instances of the thread above and then get the results. If one of them fail, the global transaction is rolled back. This is the code:

public static int processar(boolean falhar) {
    int ok = 0;
    Transaction transaction = null;
    try {

        //start transaction
        AtomikosDataSource.getTM().begin();
        transaction = AtomikosDataSource.getTM().getTransaction();

        //create thread pool
        ExecutorService executor = Executors.newFixedThreadPool(5);
        List<Callable<Integer>> processos = new ArrayList<Callable<Integer>>();

        //create 5 threads, passing the main transaction as argument
        for (int i = 0; i < 5; i++) {
            //if falhar == true, fail the fifth thread
            processos.add(new Processamento(i + 1, i == 4 && falhar, transaction));
        }

        //execute threads and wait
        List<Future<Integer>> futures = executor.invokeAll(processos);

        //count the result; get() will fail if thread threw an exception
        Throwable ex = null;
        for (Future<Integer> future : futures) {
            try {
                int threadId = future.get();
                System.out.println("Thread " + threadId + " sucesso!");
                ok++; 
            } catch (Throwable e) {
                ex = e;
            }
        }

        if (ex != null) {
            throw ex;
        }

        //finish transaction normally
        transaction.commit();

    } catch (Throwable e) {

        e.printStackTrace();
        try {
            //try to rollback
            if (transaction != null) {
                AtomikosDataSource.getTM().rollback();
            }
        } catch (IllegalStateException e1) {
            e1.printStackTrace();
        } catch (SecurityException e1) {
            e1.printStackTrace();
        } catch (SystemException e1) {
            e1.printStackTrace();
        }

    }
    return ok;
}

Notice that some methods have a parameter named falha (fail). It’ll be used to create a scenario where one of the threads will generate an error and force a rollback of all changes made by other threads.

The processar() (process) method returns the number of “successes”, i.e, threads executed without errors, independently if the transaction was committed or rolled back. It’ll be also used in tests.

Tests

I did tests for both success and error scenarios in order to validate the solution.

In the success scenario, each one of the five threads updates a row from TESTE table with the value ok, then the transaction is committed.

In the error scenario, the last thread always throw an exception, forcing the rollback of all operations. Notice that the last thread created it’s not necessarily the last one executed.

The test code is very simple. Look:

public class AtomikosTest {

    @BeforeClass
    public static void init() {
        //create atomikos transaction manager and data source
        AtomikosDataSource.init();

    }
    @Before
    public void reset() {
        //recreate data of TEST table
        AtomikosDAO.resetTable();
    }

    @AfterClass
    public static void shutdown() {
        //close atomikos resources
        AtomikosDataSource.shutdown();
    }

    @Test
    public void sucesso() {
        //process 5 rows in 5 threads
        int okParcial = AtomikosDAO.processar(false);
        //should return 5 successes
        Assert.assertEquals(5, okParcial);
        //confirms in table, count 5 ok's
        Assert.assertEquals(5, AtomikosDAO.countOk());
    }

    @Test
    public void fail() {
        //process 5 rows in 5 threads, one should fail
        int okParcial = AtomikosDAO.processar(true);
        //should return 4 successes
        Assert.assertEquals(4, okParcial);
        //confirms in table, count zero ok's due to rollback
        Assert.assertEquals(0, AtomikosDAO.countOk());
    }

}

Notes about configuration

In this project, I chose PostgreSQL to be the resource to be modified in the distributed transaction.

It was necessary enable the configuration called max_prepared_transactions in the configuration file postgresql.conf with a value greater than the number of participants in the distributed transaction. Otherwise, PostgreSQL won’t be able of participating in distributed transactions.

Final thoughts

Even though there’s a growing interest about NoSQL and even NewSQL, ACID transactions, as available on traditional RDBMS, are very important in many situations. It’s so true, that there are tutorials on how to simulate a transaction with two-phase commit in non-transactional databases like MongoDB.

Furthermore, it’s important to say that each participant of a distributed transaction must support XA protocol. Unfortunately, some database drivers and other data source implementations aren’t. So do your homework and research before start coding.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Transações distribuídas e processamento paralelo com Atomikos

distributed Atomikos é um software Java que, entre outras coisas, implementa os padrões JTA (Java Transaction API) e XA (eXtended Architecture, que suporta processamento de transações distribuídas).

Em geral, cada transação é associada à thread atual, de modo que os diversos métodos que atendem uma solicitação num servidor JEE podem compartilhá-la.

Entretanto, uma questão interessante do StackOverflow levantou a possibilidade de uma aplicação dividir uma operação atômica em tarefas delegadas a várias threads, porém compartilhando uma única transação global.

Bem, para fazer esse “desvio” da arquitetura original, a solução foi usar diretamente a API XA do Atomikos para incluir os DataSources das diferentes threads na transação principal.

Fiz um exemplo simples que implementa isso. O projeto está disponível no meu GitHub.

Implementação

Antes de mais nada, temos a inicialização do DataSource e do TransactionManager usando a API do Atomikos realizado na class AtomikosDataSource. Eis o trecho relevante:

// Atomikos implementations
private static UserTransactionManager utm;
private static AtomikosDataSourceBean adsb;

// initialize resources
public static void init() {
    utm = new UserTransactionManager();
    try {
        utm.init();
        adsb = new AtomikosDataSourceBean();
        adsb.setMaxPoolSize(20);
        adsb.setUniqueResourceName("postgres");
        adsb.setXaDataSourceClassName("org.postgresql.xa.PGXADataSource");
        Properties p = new Properties();
        p.setProperty("user", "postgres");
        p.setProperty("password", "0");
        p.setProperty("serverName", "localhost");
        p.setProperty("portNumber", "5432");
        p.setProperty("databaseName", "postgres");
        adsb.setXaProperties(p);
    } catch (SystemException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
}

Depois, implementei uma thread chamada Processamento que recebe a instância da transação (Transaction) principal. A interface Callable define que a thread é um tipo de tarefa que retorna um valor Integer. Eis o código:

private static class Processamento implements Callable<Integer> {

    private int id;
    private boolean falhar;
    private Transaction transaction;

    public Processamento(int id, boolean falhar, Transaction transaction) {
        this.falhar = falhar;
        this.transaction = transaction;
        this.id = id;
    }

    public Integer call() throws Exception {
        if (falhar) {
            throw new RuntimeException("Falhou inesperadamente!");
        }

        //enlist xa connection
        XAConnection xac = AtomikosDataSource.getDS().getXaDataSource().getXAConnection();
        synchronized (transaction) {
            transaction.enlistResource(xac.getXAResource());
        }

        //normal execution, update row with OK
        Connection c = xac.getConnection();
        Statement s = c.createStatement();
        s.executeUpdate("update teste set processado = 'ok' where id = " + id);
        s.close();
        c.close();

        //delist xa connection
        synchronized (transaction) {
            transaction.delistResource(xac.getXAResource(), XAResource.TMSUCCESS);
        }
        return id;
    }

}

Note que, ao invés de usar o JTA, estou usando diretamente a API do XA implementada pelo Atomikos.

A chamada AtomikosDataSource.getDS().getXaDataSource().getXAConnection() recupera uma conexão do XA, a qual é adicionada à transação principal com o comando transaction.enlistResource(xac.getXAResource()). Esta operação é chamada de alistamento (enlistment). Ao final do processamento da thread, o alistamento é desfeito.

Sincronizei alguns trechos pois obtive aleatoriamente alguns NullPointerException nos testes. Não cheguei a averiguar se é um bug do Atomikos ou se é by design, isto é, o objeto Transaction não é thread-safe.

Finalmente, implementei um método que inicia cinco instâncias da thread de processamento listada acima e posteriormente colhe os resultados. Se uma delas falhar, a transação global é desfeita (rollback). Veja o código abaixo:

public static int processar(boolean falhar) {
    int ok = 0;
    Transaction transaction = null;
    try {

        //start transaction
        AtomikosDataSource.getTM().begin();
        transaction = AtomikosDataSource.getTM().getTransaction();

        //create thread pool
        ExecutorService executor = Executors.newFixedThreadPool(5);
        List<Callable<Integer>> processos = new ArrayList<Callable<Integer>>();

        //create 5 threads, passing the main transaction as argument
        for (int i = 0; i < 5; i++) {
            processos.add(new Processamento(i + 1, i == 4 && falhar, transaction));
        }

        //execute threads and wait
        List<Future<Integer>> futures = executor.invokeAll(processos);

        //count the result; get() will fail if thread threw an exception
        Throwable ex = null;
        for (Future<Integer> future : futures) {
            try {
                int threadId = future.get();
                System.out.println("Thread " + threadId + " sucesso!");
                ok++; 
            } catch (Throwable e) {
                ex = e;
            }
        }

        if (ex != null) {
            throw ex;
        }

        //finish transaction normally
        transaction.commit();

    } catch (Throwable e) {

        e.printStackTrace();
        try {
            //try to rollback
            if (transaction != null) {
                AtomikosDataSource.getTM().rollback();
            }
        } catch (IllegalStateException e1) {
            e1.printStackTrace();
        } catch (SecurityException e1) {
            e1.printStackTrace();
        } catch (SystemException e1) {
            e1.printStackTrace();
        }

    }
    return ok;
}

Note que vários métodos possuem um parâmetro chamado falha. Ele será usado para gerar um cenário onde uma das threads irá gerar um erro e forçar o rollback das alterações das demais threads.

O método processar() retorna a quantidade de “sucessos”, isto é, threads que executaram sem erro, independentemente se a transação foi efetivada ou desfeita. Isso também será usado nos testes.

Testes

Fiz testes tanto de um cenário de sucesso quanto de falha para validar a solução.

No cenário de sucesso, cada uma das cinco threads atualiza uma linha da tabela TESTE com o valor ok e no final o método principal faz o commit da transação.

No cenário de falha, a última thread sempre lança uma exceção, forçando o rollback das demais. Note que a última thread criada não é necessariamente a última a ser executada.

O código de teste ficou muito simples. Veja:

public class AtomikosTest {

    @BeforeClass
    public static void init() {
        //create atomikos transaction manager and data source
        AtomikosDataSource.init();

    }
    @Before
    public void reset() {
        //recreate data of TEST table
        AtomikosDAO.resetTable();
    }

    @AfterClass
    public static void shutdown() {
        //close atomikos resources
        AtomikosDataSource.shutdown();
    }

    @Test
    public void sucesso() {
        //process 5 rows in 5 threads
        int okParcial = AtomikosDAO.processar(false);
        //should return 5 successes
        Assert.assertEquals(5, okParcial);
        //confirms in table, count 5 ok's
        Assert.assertEquals(5, AtomikosDAO.countOk());
    }

    @Test
    public void fail() {
        //process 5 rows in 5 threads, one should fail
        int okParcial = AtomikosDAO.processar(true);
        //should return 4 successes
        Assert.assertEquals(4, okParcial);
        //confirms in table, count zero ok's due to rollback
        Assert.assertEquals(0, AtomikosDAO.countOk());
    }

}

Notas sobre a configuração

Neste projeto, usei o servidor de banco de dados PostgreSQL como o recurso a participar da transação distribuída.

Foi necessário habilitar a configuração max_prepared_transactions no arquivo de configuração postgresql.conf com um valor maior que o número de participantes na transação distribuída. Sem isso, o PostgreSQL não será capaz de participar de transações desta natureza.

Considerações finais

Embora haja um crescente interesse sobre NoSQL e até NewSQL, transações ACID, como disponíveis nos SGBDRs tradicionais, são importantes em muitos cenários. Até por isso existem tutoriais sobre como simular uma transação com o conceito de two-phase commit em bancos de dados não transacionais como MongoDB.

Além disso, é importante ressaltar que cada participante de uma transação distribuída deve ser compatível com o protocolo XA. Infelizmente, alguns drivers de bancos de dados ou outras fontes de dados podem não ser compatíveis. Então, faça sua lição de casa e pesquise antes de sair implementando.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Creative Commons O blog State of the Art de Luiz Ricardo é licenciado sob uma Licença Creative Commons. Copie, compartihe e modifique, apenas cite a fonte.