Chronicle Queue – A microsecond messaging framework

É chover no molhado citar que o mundo da computação se reinventa, avança e expande para novos modelos e tecnologias a cada ano, entretanto, para aqueles um pouco mais “experientes” na área de TI, é perceptível que nos últimos 10 anos um mercado, antes adormecido, tem ressurgido de uma forma que talvez se assemelhe ao boom causado com o surgimento da microinformática em meados dos anos 70.

Estou me referindo a computação de alta performance, onde tudo era desenvolvido de tal forma a se extrair a capacidade máxima das tecnologias existentes da época, uma vez que a própria tecnologia avançava de maneira mais lenta devido às limitações da própria época, obrigando os programadores a pensar em como economizar ciclos e estados de máquina.

A grande limitação, naqueles dias, estava relacionada à memórias de maneira geral, sejam voláteis (RAM) ou não voláteis (ROM, hard disks, Flash memories), sendo esses dispositivos caríssimos, até pouco tempo. Só para se ter uma ideia, em 1980 nos EUA, um hard disk de 26MB custava em média U$5.000 e 64KB de memória RAM custava em média U$405,00.

Memória RAM versus Hard disks

Historicamente as memórias RAM sempre estiveram muito a frente dos hard disks na questão de velocidade de E/S, mas por outro lado o custo por megabyte dos hard disks e a grande capacidade desses dispositivos sempre o colocaram à frente das memórias RAM no quesito capacidade de armazenamento.

Com o avanço e barateamento das tecnologias de armazenamento voláteis e não voláteis, aliado ao aumento gradativo do poder dos processadores, esse cenário mudou fazendo com que novas indústrias de tecnologia surgissem e despontassem para o mundo em pouquíssimo tempo.

Algumas delas tem seu “negócio” fortemente embasado no processamento em memória RAM, como é o caso de empresas desenvolvedoras de engines para jogos, já outras nem precisam de performance extrema sendo seu foco mesmo a venda de sua capacidade de armazenamento não volátil, como o DropBox por exemplo.

Entretanto existem aquelas empresas que necessitam de capacidade e performance extremas nos dispositivos voláteis (RAM) e nos não voláteis (hard disks), como por exemplo redes sociais, sites de busca, sites de comparação de preços, dentre outras que trabalham com informações dinâmicas e em grande quantidade.

Nesse cenário de alta capacidade de armazenamento e performance, aliado ao uso de tecnologias disponíveis nos diversos sistemas operacionais modernos, surgem tecnologias que utilizam as mais variadas técnicas computacionais já conhecidas dos pioneiros da alta performance de décadas passadas.

Chronicle Queue

Chronicle Queue é um componente de IPC (inter-process communication), open source, escrito em Java, que possibilita a transferência de grandes quantidades de dados em alta performance, entre threads, processos e máquinas, com persistência de dados em disco. Segundo a página do projeto no GitHub o Chronicle Queue é definido como “Micro second messaging that stores everything to disk“.

O grande segredo da performance do Chronicle Queue é que ele trabalha justamente utilizando estruturas simples porém rápidas tal qual as utilizadas por diversas aplicações nos primórdios da computação, que necessitavam armazenar e recuperar dados com uma performance aceitável e o faziam utilizando índices otimizados que apontam direto para o dado real no arquivo de dados em disco, entretanto ao contrário das antigas tecnologias, agora os arquivos de dados podem ser manipulados utilizando mecanismos de memory-mapping existente nos sistemas operacionais modernos, otimizando assim a performance das operações de E/S em disco.

Arquitetura e modos de operação

A interface básica de funcionamento da arquitetura do Chronicle Queue é bem simplista, com algumas poucas variações do modo de funcionamento, sendo restrito a um componente de escrita na fila, denominado Appender (producer) e um outro de leitura, denominado Tailer (consumer).

Dentro desse modelo, que é basicamente o tradicional pattern producer-consumer, o Chronicle Queue pode operar em modo sequencial (FIFO) ou aleatório, quando operando como Tailer e apenas em modo sequencial quando em modo Appender.

Modelo tradicional do Chronicle Queue (producer-consumer)

O Chronicle Queue, além do suporte à filas diretamente acessadas via os arquivos físicos da mesma, também possui outros modos de operação interessantes, dentre eles um modo de fila acessado via TCP/IP, ou seja, diversos Tailers conectados a uma fila via um Appender TCP, o que de fato diminui a performance de processamento das filas mas que por outro lado permite toda a facilidade de inter-process communication do Chronicle Queue, expandindo-a, uma vez que abre possibilidade de comunicação entre máquinas diferentes.

Outra característica do Chronicle Queue é que, diferente das estruturas IPC presentes nos sistemas operacionais modernos, todos os dados trafegados pela fila são persistidos em disco e com isso é possível utilizar a própria fila Chronicle como um sistema de journaling, podendo ser usado para recuperação do sistema em caso de uma eventual queda (crash).

Source Code

A simplicidade de uso e operação do Chronicle, por parte do programador é uma de suas principais características, conforme verificado no exemplo abaixo:

try (ChronicleQueue queue = ChronicleQueueBuilder.single("queue-dir").build()) {
    // Obtain an ExcerptAppender
    ExcerptAppender appender = queue.acquireAppender();

    // write - {msg: TestMessage}
    appender.writeDocument(w -> w.write(() -> "msg").text("TestMessage"));

    // write - TestMessage
    appender.writeText("TestMessage");

    ExcerptTailer tailer = queue.createTailer();

    tailer.readDocument(w -> System.out.println("msg: " + w.read(()->"msg").text()));

    assertEquals("TestMessage", tailer.readText());
}

O código acima demonstra 1 Appender escrevendo e 1 Tailer consumindo na mesma Thread, para fins de demonstração, entretanto a disposição desses producers-consumers pode estar da forma como melhor se adequar ao modelo da aplicação que está sendo desenvolvida, podendo o producer estar em um processo e o consumer em outro, em Threads separadas ou até em máquinas separadas.

Por ser um projeto enxuto, bem estruturado e que utiliza muito bem os recursos computacionais sejam do sistema operacional ou de estruturas de dados internas do projeto, o Chronicle Queue garante uma boa performance e confiabilidade, além de uma vasta comunidade trabalhando em atualizações e melhorias do projeto.

Ainda assim, é possível ter um suporte “enterprise” por parte do grupo que o desenvolve, o que o torna um projeto de vida longa no mercado e comunidade, além da segurança de continuidade e suporte necessário ao ambiente corporativo.

Conclusão

Apesar de bastante resumido, esse artigo visa demonstrar a importância desse retorno às origens da computação, principalmente considerando o atual momento onde vivemos a febre de computação distribuída, vide BlockChain e da necessidade de processamento de dados em grande escala, vide BigData.

Entretanto, para que esses conceitos se tornem realidade, muitas tecnologias de base e que muitas vezes sequer aparecem no desenvolvimento desses conceitos, precisam ser muito bem desenvolvidas, otimizadas e principalmente que sejam confiáveis, portanto vale a pena investir um tempo utilizando tecnologias abertas como o Chronicle Queue.

[]’s
PopolonY2k

10 regras da NASA para desenvolver código seguro.

Recentemente postei nas comunidades GDMSX do FaceBook e do G+ um texto bem interessantante sobre algumas dicas da NASA de como desenvolver código seguro.

Isso é muito interessante principalmente para aqueles que desenvolvem código para sistemas/dispositivos que nunca pode se dar ao luxo de uma exceção não tratada ou um core dump, comuns no mundo UNIX ou os famosos GPF’s bem conhecidos para programadores do mundo Windows.

Alguns membros da comunidade GDMSX me pediram para traduzir, então eu decidi fazer uma tradução livre, com algumas considerações pessoais que estão marcadas no texto como “(*) Notas do tradutor” 🙂 .

Vamos ao texto.

10 regras da NASA para desenvolver código seguro.

NASA Computers

NASA Computers

A NASA tem escrito software de missão crítica para a exploração espacial por décadas e agora a organização está transformando esses guias de programação em um padrão de desenvolvimento de software da indústria.

O laboratório de propulsão a jato da NASA para softwares confiáveis (JPL), recentemente publicou um conjunto de guias de programação (code guidelines), “Potência de 10 – Regras para o desenvolvimento seguro de código crítico“. O autor do texto, o cientista chefe Gerard J. Holzmann, explicou que a quantidade de codificação existente é inconsistente e cheia de regras arbitrarias, raramente permitindo tarefas essenciais como verificação de conformidade de código baseado em ferramentas de checagem de código. Os guias existentes, ele disse, inundam os programadores com regras vagas, diminuindo a qualidade do código da maioria das aplicações críticas.

“Projetos mais sérios de desenvolvimento de software usam guias de codificação,” escreveu Holzmann. “Esses guias destinam-se a firmar as regras básicas para o qual o software está sendo escrito: como ele deveria ser estruturado e quais características da linguagem deveriam e não deveriam ser usadas. Curiosamente, existe um pouco de consenso sobre o que é um bom padrão de codificação.”

 Holzmann definiu 10 regras rígidas para o desenvolvimento de software, tendo em mente a segurança do código. As regras foram especificamente escritas levando em consideração a linguagem C (uma linguagem recomendada pela NASA devido a sua longa história no desenvolvimento de software crítico e seguro e também pelo extensivo suporte de ferramentas a essa linguagem como por exemplo analisadores de código, depuradores, ferramentas de testes, dentre outras), embora essas regras possam ser facilmente generalizadas para codificação em qualquer outra linguagem, principalmente as que tem forte similaridade com C no nível de estruturação, como é o caso de Pascal e suas variantes.

  1.  Restrinja toda a construção de seu código a um fluxo de controle muito simples. Não use declarações GOTO, construções com setjmp ou longjmp, ou recursividade direta ou indireta;

    (*) Nota do tradutor:
    Quanto ao GOTO não é preciso nem escrever muito a seu respeito pois um de seus “males”, que é o de deixar o código confuso através de saltos incondicionais, já são amplamente conhecidos ao longo dos anos. O mesmo acontece com a dupla setjmp/longjmp que causam o mesmo efeito do GOTO, mas infelizmente esse recurso ainda é muito defendido por programadores experientes de C que muitas vezes se recusam a abandoná-los.
    Quanto à recursividade direta ou indireta, realmente essas tornam o código mais complexo entretanto por diversas vezes o seu código pode se tornar mais eficaz do que utilizando métodos tradicionais para resolução de um problema. Um exemplo é o algoritmo de multiplicação reconhecidamente veloz conhecido como algoritmo de Karatsuba e que é recursivo.
    Eu particularmente sugiro que se utilize métodos recursivos pequenos e de baixa complexidade, pois dessa forma pode-se evitar possíveis pontos de falhas que são invisíveis na maioria das vezes em tempo de desenvolvimento, mas que são comuns em uma situação de missão crítica.
    Quando se optar por implementar métodos recursivos, tenha em mente que a cobertura de seus testes unitários deverá ser extremamente abrangente a ponto de se testar as mínimas situações de falha.

  2. Todos os loops devem ter um limite superior fixo. Isso deve ser trivial para uma ferramenta de checagem que prove, estaticamente, que um dado limite no número de iterações não pode ser excedido. Se o limite do loop não puder ser comprovado estaticamente, a regra é considerada violada. Caso os limites sejam violados, o método deverá disparar um assert a ser verificado pelo chamador.

    (*) Nota do tradutor:
    Essa regra pode ser checada através da implementação de testes unitários com os devidos cenários mapeados.

  3. Não use alocação dinâmica de memória após a inicialização.

    (*) Nota do tradutor
    Realmente alocação dinâmica em linguagens que não são gerenciadas por um Garbage Collector é algo crítico e que deve ser utilizado de maneira cuidadosa, principalmente para principiantes no desenvolvimento de software. No artigo original, o autor cita diversos dos problemas conhecidos em se utilizar alocação dinâmica como a não liberação de recursos corretamente, o que causam os famosos vazamentos de memória ou memory leaks, e também cita sobre o cuidado de não ultrapassar os limites da memória alocada, esse último sendo também um problema quando se está usando memória alocada estaticamente em C.
    Entretanto existem casos que a alocação dinâmica é necessária e não há como escapar, nesses casos o mais comum a se fazer é alocar o buffer desejado em um ponto único, fazer toda e qualquer referência a esse buffer através de ponteiros passados para as funções que irão manipulá-lo e por fim liberá-lo nesse mesmo ponto único em que foi alocado, de preferência. Dessa forma você restringe toda a construção de seu código a um fluxo de controle muito simples, que é exatamente o primeiro item sugerido por esse guideline.
    Muitos dos desenvolvedores de softwares novatos sequer tem conhecimento de problemas de fragmentação de memória, decorrentes do abuso excessivo de alocação e liberação dinâmica de memória, principalmente em dispositivos de missão crítica que geralmente são hardwares embarcados dedicados e com recursos limitadíssimos de memória e armazenamento.

  4. Nenhuma função deve ser tão extensa que não possa ser impressa em uma única folha de papel (em uma referência ao formato padrão com uma linha por instrução e uma linha por declaração). Tipicamente isso significa não mais do que 60 linhas de código por função.

    (*) Nota do tradutor
    Manter o código com poucas linhas por função é sempre uma excelente prática pois facilita na manutenção do mesmo, entretanto algumas vezes principalmente em se tratando de código que gerencia máquinas de estado complexas, é melhor que a função fique um pouco maior do que as 60 linhas sugeridas ao invés de quebrá-las em mais funções para que comporte as 60 linhas, pois se ganha em performance, uma vez que se reduz o custo das passagens de parâmetros entre funções, ainda mais em se tratando de passagem de parâmetros de grandes estruturas por valor, que nesse caso é também uma péssima prática e que deveria ser substituída por passagem por referência ou ponteiro. Em linguagens orientadas a objeto, a regra de 60 linhas por método (função) é mais plausível e fácil de se atingir por conta da capacidade de encapsulamento dessas linguagens.

  5. A quantidade de asserts por função deve ser de no mínimo duas por função. Asserts não devem causar nenhum efeito colateral no código e deveriam ser definidos como testes booleanos.

    (*) Nota do tradutor
    Linguagens com um rico pré-processador como são C e C++ são propensas a inserção de asserts que estarão ativos dependendo do modelo de compilação em que o  binário/executável foi gerado, com isso pode-se gerar compilações de software específicas para testes.

  6. Dados devem ser declarados no menor nível de escopo possível.

    (*) Nota do tradutor
    O uso de variáveis globais é algo que vem sendo desestimulado no decorrer dos anos, pois é reconhecido que manter os dados encapsulados em escopos cada vez menores é um grande facilitador na hora de se descobrir bugs ou realizar qualquer tipo de manutenção ou melhorias no código, então declarar variáveis específicas de cada escopo é uma boa prática de programação.

  7. Cada função chamadora deve checar o retorno de funções não void (que retornam valores) e a validade dos parâmetros devem ser checadas dentro de cada função.

    (*) Nota do tradutor
    Isso é algo óbvio……bom, pelo menos deveria ser 🙂 .

  8. Uso de pré-processador deve ser limitado a inclusão de arquivos header e definição simples de macros. Macros complexas como as recursivas e com listas de argumentos variáveis, devem ser evitadas.

    (*) Nota do tradutor No texto original o autor descreve que o uso de macros de compilação condicional é algo dúbio entretanto não pode ser sempre ignorado. Eu concordo plenamente com a ideia de que não pode ser ignorado, uma vez que sempre tivemos diversas plataformas diferentes e dominantes na história da computação moderna, e o uso do pré-processador para se ter suporte a compilação condicional é uma excelente técnica que tem possibilitado manter um código portátil entre as diversas plataformas que vão desde os diversos sistemas operacionais disponíveis em PC’s até aos diversos dispositivos mobile existentes hoje.

  9. O uso de ponteiros deve ser restrito. Especificamente, não mais do que um nível de deferenciação é permitido. Operações de deferenciação de ponteiro não podem estar escondidas em macros ou dentro de declarações typedef. Ponteiros de função não são permitidos.

    (*) Nota do tradutor.
    Talvez esse seja o mais polêmico item do guia. Operações com ponteiros são algumas vezes complexas dependendo do nível de deferenciação. São raros os casos em que é necessário o uso de ponteiros duplos ou triplos, entretanto pode ser evitado e sugiro que seja evitado.
    Quanto ao uso de macros para deixar a deferenciação mais “limpa e clara”, para mim isso tem um nome e se chama “armadilha”. Fuja de quaisquer construções que “escondam” a complexidade de alguma operação, e essa regra não deve ser considerada apenas nas operações de ponteiros.
    Entretanto há ressalvas quanto ao uso de typedefs na declaração de um tipo ponteiro,  como por exemplo as definições de ponteiros para funções amplamente utilizadas nas API’s do Windows e UNIXes em geral, nesse caso não há como escapar uma vez que esses sistemas operacionais fazem uso extensivo de callbacks para proporcionar respostas a eventos requeridos e necessários para a aplicação do usuário.
    Quanto a proibição de ponteiros para função, talvez para a maioria das aplicações comuns de desktop seu uso realmente não seja necessário, entretanto para aplicações de missão crítica embarcadas como as que utilizam algum kernel real-time, como por exemplo o uC de Jean Labrosse e que fazem extenso uso de ponteiros para função para implementar as diversas tasks da aplicação do usuário, o uso de ponteiro de função é algo bem comum.
    A construção de softwares kernel real-time é repleta de ponteiros de função, até mesmo para proporcionar uma abstração de tasks, timers dentre outras estruturas típicas desse tipo de software.

  10. Todo código deve ser compilável desde o primeiro dia de desenvolvimento, com todos warnings do compilador ativados. Todo código deve compilar com essa configuração, sem gerar nenhum warning ou erro. Todo código deve ser checado diariamente com pelo menos um – de preferência mais do que um – excelente analisador de código estático (o que ele cita como analisador estado-da-arte), e a análise deve passar sem warning algum.

    (*) Nota do tradutor
    Quanto ao detalhe dos warnings de compilação, acredito que já seja bem comum em grandes projetos, entretanto a parte do analisador “estado-da-arte”, não creio que sequer mercados corporativos em países avançados a utilizem, exceto empresas de altíssima tecnologia que dependam de que seus softwares funcionem 24×7 ou de forma embarcada em algum dispositivo.

 Holzmann incluiu diversos comentários no documento original para cada uma das regras descritas acima, mas a essência do documento é que quando as regras forem utilizadas em conjunto, garantam um fluxo claro e transparente que tornem fácil a construção, testes e análise de código amplamente aceitos como livres de falhas. A JPL tem desenvolvido softwares automatizados para missões espaciais como a Mars Curiosity and a Voyager, e o laboratório já está usando essas regras em uma base experimental para escrever software de missão crítica.

Holzmann acredita que seguir as regras da NASA minuciosamente, pode diminuir a carga sobre os desenvolvedores e levar a uma melhor segurança e clareza do código.

“Se as regras parecerem draconianas no inicio, tenha em mente que elas foram criadas para que se possa checar códigos onde a sua vida pode depender muito de que haja precisão: código utilizado para controlar o avião em que você voa, a usina nuclear a poucas milhas de onde você vive, as aeronaves que carregam os astronautas em órbita”, escreve ele.

“As regras agem como o cinto de segurança em seu carro: Inicialmente eles parecem ser um pouco desconfortáveis, mas depois de um tempo seu uso se torna natural e não usá-los se torna inimaginável.”

O texto acima foi traduzido do original que pode encontrado no link do site SDTimes abaixo:

http://sdtimes.com/nasas-10-rules-developing-safety-critical-code/

[]’s
PopolonY2k