Dicas valiosas de programação – Parte 02

Introdução

Continuando na linha de boas práticas e afins, vamos abordar nesse post mais uma dica de ouro, válida para qualquer aplicação que pretende ser escalável, porém com foco no AdvPL

Procure fazer transações curtas

Parece simples, e se olharmos bem, não é assim tão complicado. Primeiro, vamos conceituar uma transação. Na linguagem AdvPL, temos os comandos BEGIN TRANSACTION e END TRANSACTION, que por baixo utilizam recursos do Framework do ERP Microsiga, junto com recursos da linguagem AdvPL, para identificarmos e protegermos uma parte do código responsável por realizar inserção ou atualização de dados em uma tabela.

Quando utilizamos um Banco de Dados relacional no AdvPL, através do DBAccess, proteger a parte do fonte AdvPL que faz alterações no banco de dados é importante, pois evita que, caso ocorra um erro (de qualquer origem)  durante a execução deste trecho, e ele não chegue ao final da transação, nenhuma das operações realizadas com os dados desde o BEGIN TRANSACTION são realmente efetivadas no Banco de Dados, mas sim descartadas. Isso evita por exemplo dados órfãos na base de dados, ou registros incompletos ou inconsistentes.

Por que transações curtas ?

Cada alteração de um registro de qualquer tabela no AdvPL, exige que seja feito um LOCK (ou bloqueio) explícito no registro, para que o mesmo possa ser alterado, e este bloqueio, uma vez adquirido, somente pode ser solto no final da transação. Quanto maior (mais longa) for a duração da transação, por mais tempo este registro permanecerá bloqueado para os demais processos do sistema.

Erros comuns que devem ser evitados

  • “Não abrirás interface com o usuário no meio de uma transação”

Antes de abrir uma transação, a aplicação deve realizar todas as validações possíveis nos conteúdos que estão sendo manipulados. Se qualquer um deles não passar na validação, o programa nem abre a transação. O objetivo da transação é pegar os dados em memória e transferi-los para as tabelas pertinentes, sem interrupções. Ao abrir uma tela ao usuário no meio de uma transação, á transação somente continua depois que o usuário interagir com a interface. Se o operador do sistema submeteu uma operação, virou as costas e foi tomar café, os registros bloqueados pelo processo dele assim permanecerão até que ele volte. Isso pode prejudicar quaisquer outros processos que precisem também atualizar estes mesmos registros.

  •  “Evitarás processamentos desnecessários dentro da transação”

Eu adoro o exemplo do envio de e-mail. Imagine que, durante a gravação de um pedido, exista a necessidade de emissão de um e-mail para inciar um WorkFlow ou um aviso ao responsável pelo setor de compras que um item vendido atingiu um estoque mínimo. Como já visto anteriormente, a solução mais elegante é delegar o envio desse e-mail para um outro processo, preferencialmente notificado de forma assíncrona. Se, em último caso, você precisa que esse email seja disparado na hora, e isso impacta seu processo, não dispare esse email com a transação aberta, lembre-se do tempo que os registros da sua transação vão ficar bloqueados … Grave em um dos campos de STATUS que o envio do e-mail está pendente, e finalize a transação. Então, tente enviar o e-mail, e ao obter sucesso, bloqueie novamente o registro e atualize apenas o campo de STATUS.

  • “Evitarás DEADLOCK”

Ao realizar atualizações concorrentes (mesmo registro tentando ser atualizado por mais de um processo), procure estabelecer uma ordem de obtenção dos bloqueios dos registros, para não fazer a sua aplicação entrar em deadlock — duas transações, que vamos chamar de A e B, já tem cada uma um registro bloqueado de uma determinada tabela. A transação A têm o lock do registro 1, e a transação B têm o lock do registro 2, porém a transação A precisa do lock do registro 2, e a transação B precisa do lock do registro 1, e enquanto nenhuma delas desistir, uma fica esperando a outra e as duas transações “empacam” (efeito conhecido por DEADLOCK).

Se as duas transações ordenassem os registros por uma chave única em ordem crescente, e tentasse obter os locks nesta ordem , o primeiro processo a falhar ainda não teria conseguido pegar o bloqueio de nenhum registro de valor superior ao que o outro processo já tenha obtido, ele apenas esperaria um pouco e tentaria pegar o lock novamente, o que será possível assim que a transação que já tem o lock seja finalizada.

Conclusão

O arroz com feijão é apenas isso. Mas apenas isso já tira um monte de dor de cabeça de DBAs e Administradores de Sistemas, por que mesmo com todo o ferramental disponível, rastrear quem está segurando o lock de quem, em ambientes que passam de centenas de conexões, tome abrir monitor do DBAccess, do Protheus, do SGDB, e caçar a thread que está esperando uma liberação de registro, e a outra que está segurando o lock, não é uma tarefa fácil …

Desejo a todos TERABYTES de SUCESSO 😀

Referências

Entendendo e minimizando deadlocks

DEADLOCK. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2018. Disponível em: <https://pt.wikipedia.org/w/index.php?title=Deadlock&oldid=51816852>. Acesso em: 15 abr. 2018.

 

 

 

 

Dicas valiosas de programação – Parte 01

Introdução

Ao longo do tempo, cada analista de sistemas e programador adquire experiência e proeficiência em algoritmos e soluções de software para atender a necessidade de seus clientes. Normalmente cada linguagem têm os seus “pulos do gato”, muitos são conhecidos e são independentes da linguagem de programação. Neste post vamos abordar os mais conhecidos, e que sempre devem ser lembrados, com ênfase no “porquê” é melhor fazer assim do que assado.

Separe o Processamento da Interface

Basicamente, você escreve um pouco mais de código, visando criar no mínimo duas etapas de processamento em códigos distintos. O primeiro código, responsável pela camada de interface, deve criar para o usuário uma interativa de fornecer os dados e parâmetros para um processamento.

Por exemplo, uma inclusão de novo cliente, ou a emissão de um relatório. Você constrói uma interface onde o usuário possa preencher os campos de cadastro ou os parâmetros a serem considerados na emissão do relatório.

O segundo código é responsável pela realização efetiva do processamento. Porém, este código não deve ter acesso a interface. Ele deve receber os dados para serem processados como argumento da função, e o retorno de sua execução deve indicar se o processamento foi executado com sucesso, e em caso de falha, os detalhes pertinentes da falha — como por exemplo parametrização inválida, tentativa de incluir cliente já existente, etc…

Por que fazer dessa forma ?

  1. Uma rotina escrita para não depender de interface pode ser executada SEM interface, em processamentos do tipo “JOB”. Isso torna possível, por exemplo, a criação de JOBs dedicados para realizar várias inclusões ao mesmo tempo, ou por exemplo a execução do relatório através de um programa do tipo “Agendador de Tarefas” (ou Scheduler) sem a dependência de ser iniciado pelo usuário.
  2. A rotina responsável pelo processamento deve possuir as validações adequadas da parametrização envolvida na sua execução, assim você escreve uma única função de processamento, e pode fazer a chamada dela a partir de várias interfaces ( SmartClient, Telnet, WebService, HTML, REST, etc ), onde a regra de negócio é mantida em apenas um código.
  3. As interfaces de chamada da função de processamento não necessariamente precisam apresentar uma tela ao usuário. Por exemplo, a sua função de processamento recebe parâmetros em array, no formato chave=valor. Com algumas linhas de código é possível escrever uma classe de Web Services Server, responsável por publicar uma funcionalidade e tratar os parâmetros (SOAP ou REST) recebidos, converter em array para a chamada da função, e retornar o status da tarefa.
  4. Mesmo utilizando uma arquitetura Client-Server, como o caso do AdvPL, o contexto de execução de um processo de interface — SmartClient por exemplo — somente é mantido no ar enquanto a aplicação Client está sendo executada. Logo, se durante um processamento de código AdvPL no servidor, sendo executado em um contexto (ou Thread) iniciada a partir de um SmartClient, se o APPlication Server perder o contato com o SmartClient — normalmente por instabilidade na rede ou mesmo um problema na máquina onde o Smartclient está sendo executado ), o processo em execução no servidor é encerrado imediatamente — afinal sua existência é baseada na premissa da interface gráfica com o usuário.
  5. Justamente em processamentos mais longos, como por exemplo processos em lote — ou a emissão de um relatório que demora duas horas … Não vale a pena deixar a interface aberta e travada com uma tela de “aguarde”, e rodar o relatório neste processo. Imagine que falta 10 minutos pro relatório efetivamente ser emitido, e a energia “piscou” no terminal, e o terminal perdeu por um instante a conexão de rede .. lá se foi uma hora e 50 minutos de trabalho jogados fora, em uma infra-estrutura onde o servidor de aplicação normalmente está em um ambiente com rede redundante, alimentação de energia redundante, alta disponibilidade e afins .. o relatório teve que ser emitido novamente, e iniciar “do zero” por que a energia “piscou” na sala onde estava o terminal do sistema.

Então, vamos mudar tudo pra JOB ?

Não, não é assim que funciona. Chamar um processo separado do processo atual para realizar uma tarefa têm custo, exige mais controle(s), tratamento(s) de erro, normalmente exige um algoritmo de gerenciamento de processos, para não deixar o sistema subir mais processos do que ele “aguenta” processar. Um erro de lógica neste mecanismo ou a ausência dele pode levar um sistema a um estado de esgotamento de recursos.

O que colocar em JOB ?

Via de regra, normalmente é recomendável colocar em JOB os seguintes tipos de processamento:

  1. Emissões de relatórios pesados e/ou demorados. Aquele resumo financeiro do dia anterior, que só pode ser executado depois da meia note, que demora pelo menos meia hora para ser emitido, e que você precisa ver no dia seguinte as 8 horas da manhã … coloque em um agendador de tarefas (Scheduler)  para rodar as 5 ou 6 da manhã. Quando você entrar no sistema e abrir o Spool de Impressão,  seu relatório deve estar lá. Se não estiver, o Scheduler deve ter registrado alguma falha.
  2. Tarefas complementares de processamentos, onde não existe a necessidade imediata do resultado para a continuidade do processo. Por exemplo, um envio de e-mail informativo de uma determinada operação. Quando utilizadas em pequenas quantidades, subir um jobs adicional na execução do programa é suficiente. Agora, caso múltiplas rotinas de envio de email sejam chamadas ao mesmo tempo, sob pena de haver um esgotamento de recursos, é prudente criar uma fila de envio de emails — por exemplo uma tabela na aplicação contendo os emails pendentes de envio, e um ou mais JOBS dedicados ao envio de email. Assim, enquanto os processos vão inserindo os emails a serem enviados em uma tabela do banco de dados, um ou mais processos dedicados vão enviando esses emails, um a um, sem sobrecarregar o servidor atual com muitos jobs, e sem sobrecarregar o próprio provedor de email.
  3. Tarefas especializadas concorrentes, onde múltiplos processos concorrem pelo mesmo recurso, que só pode ser usado por um processo por vez. Normalmente estes casos são exemplificados por rotinas de atualização que exigem o bloqueio temporário de um registro. Neste caso, seria mais elegante criar um e apenas um JOB dedicado para fazer esta atualização, e este JOB pegar os dados que devem ser atualizados de uma fila. Assim, outros processos que precisem fazer esta tarefa não precisam esperar o processo dedicado terminar de rodar a requisição.

Conclusão

Escrever a rotina de processamento sem depender de interface, além de naturalmente permitir a execução dessa rotina dentro de um processo de interface, lhe abre a oportunidade de executá-la em um processo separado, mas não o obriga a fazer isso. Se uma rotina já existe, e foi escrita para depender de uma interface, você deve decidir se usar um JOB é a melhor alternativa para as suas necessidades, e avaliar o custo de fazer essa separação. Em linhas gerais, para não sofrer amanhã, comece a separar processamento da interface hoje, nas novas funcionalidades que você implementa, assim a utilização delas em Job, se assim conveniente ou necessário for, será menos traumático.

Desejo a todos(as)  TERABYTES de SUCESSO 😀

 

Escalabilidade e Performance – Segredos

Introdução

Outo dia li um post muito interessante, onde o autor menciona alguns “segredos” para uma aplicação escalável, com algumas técnicas comuns utilizadas em aplicações WEB ( Vide post original nas referências no final do Post). Resolvi me basear nele para exemplificar o que cada um dos tópicos elencados poderia agregar ao sistema, e levantar algumas questões de aplicabilidade de cada um, dentro do ambiente AdvPL.

“STATELESS”

“Se você quer um sistema ou um serviço escalável, com certeza você quer que todas as requisições para este serviço sejam stateless.
Mas por que? Simplesmente por que caso em um futuro próximo você precise rodar a sua aplicação em um cluster, você não prende um cliente a um nó do cluster, cada requisição pode ir para o nó do cluster que estiver com a menor carga naquele momento, fazendo com que o tempo de resposta aquela requisição seja o menor possível, mantendo o nó do cluster que vai atender a esta requisição ocupado o menor tempo possível.” (Sobre Código: Os 5 segredos para um sistema altamente escalável, por Rodrigo Urubatan)

Esta afirmação casa com os princípios de escalabilidade e performance já mencionados em posts anteriores, mais especificamente sobre uma regra que diz “não estabeleça afinidade”. Basicamente, o “stateless” significa que uma requisição de processamento não deve depender de um estado específico de uma requisição anterior. Isto significa que o agente de processamento da requisição não deve reter nenhuma informação da requisição anterior, o que torna possível distribuir uma requisição de processamento para qualquer agente disponível no cluster.

Isto pode ser aplicado a diversos tipos de processamento, mas normalmente requer que a aplicação seja desenhada para trabalhar desta forma. Por exemplo, hoje quando desenvolvemos uma aplicação AdvPL para ser acessada através da interface do SmartClient, a aplicação trabalha com uma conexão TCP persistente, onde o programa AdvPL responsável pelas instruções de interface, também é responsável por montar o ambiente de execução e executar as aplicações de processamento e acesso a SGDB e meta-dados no mesmo processo.

Por exemplo, uma inclusão de um cliente através de um programa de interface SmartClient no AdvPL, é iniciada ao acionarmos um botão na janela, que executa uma função do código AdvPL pelo Application Server dentro do próprio processo, que acessa o SGDB e executa efetivamente a inserção e demais integrações e gatilhos sistêmicos relacionados (pontos de entrada do ERP), mantendo a interface em “espera” enquanto o processo é executado.

Para esta operação ser executada em um outro nó do cluster, a aplicação precisaria ser desenhada para, no momento de submeter a inclusão de um cliente, a função deveria gerar um request de inclusão de cliente, e despachá-la para um agente de distribuição de processos, que iria alocar um processo em espera no cluster de processamento, para este processo dedicado realizar o acesso ao banco e concluir a operação, liberando a interface para iniciar uma nova inclusão ou outra operação. A dificuldade de lidar com estes eventos é desenvolver um mecanismo resiliente, que permita gerar informações e um “BackLog” do que foi feito, pois procurar um erro de processamento ou comportamento em um ambiente assim, sem rastreabilidade das operações, não será uma tarefa fácil.

Um WebService por natureza é concebido como STATELESS. Existem algumas tecnologias que permitem criar afinidade e persistência em WebServices, porém isto foge ao objetivo principal da natureza das requisições e dos serviços que a implementação original se propõe a fazer. Se uma aplicação em WebServices precisa de afinidade e persistência, o uso de WebServices não parece uma boa escolha. Nem toda a operação pode ser totalmente assíncrona, mas muitas etapas de processo compensam o custo da transformação, revertida em escalabilidade.

“REQUEST RESPONSE TIME” (RRT)

Partindo de um ambiente de serviços distribuídos, onde vários processos podem realizar operações específicas, é fundamental que as requisições importantes ( ou prioritárias) sejam atendidas com o menor tempo de resposta possível. É o mesmo princípio dos caixas de um banco: Quando temos mais clientes do que caixas disponíveis, é formada uma fila, e o primeiro caixa que terminar o atendimento torna-se disponível e chama o primeiro da fila.

Se os caixas forem lentos, quando começar uma fila, as pessoas desta fila vão demorar para serem atendidas. Então, ou você coloca mais caixas, ou você melhora o desempenho dos caixas. Pensando por exemplo na priorização de atendimento a idosos, portadores de necessidades especiais, gestantes e mães com crianças de colo, são criadas duas filas de atendimento, vamos chamá-las de “normal” e “prioritária”. Todos os caixas podem atender a qualquer pessoa da fila, porém ocorre um escalonamento do atendimento.

Quanto mais rápido e eficiente for o atendimento de um caixa, mais clientes por hora ele será capaz de atender. A mesma regra vale para requisições de processamento (SOA/RPC/SOAP/REST). Se você tem um volume diário de X requisições, sujeitas a variações ou picos, e cada caixa aberto consegue atender em média N requisições por minuto, e você tem Y caixas abertos, se todas as X requisições chegarem ao mesmo tempo, as Y primeiras são atendidas imediatamente, e as demais entram na fila. Com todos os caixas atendendo, se ninguém mais entrar na fila, voce vai manter todos os caixas ocupados e trabalhando por M minutos até não ter mais ninguém na fila.

Pegamos o total de requisições (ou clientes) X , dividimos pelo numero de processos disponíveis (ou caixas abertos) Y, e temos uma média de quantas requisições devem ser processadas por cada processo (quantos atendimentos cada caixa deve fazer). Multiplicamos este número pelo tempo médio de processamento da requisição (tempo de atendimento), e teremos uma ideia de quanto tempo será necessário para eliminar a fila. Se chegam 20 requisições, e existem 4 processos disponíveis, cada um deles vai realizar 5 processamentos. Se cada processamento tem um tempo médio de 3 segundos, cada processo vai permanecer ocupado processando por 5×3 = 15 segundos. Se neste meio tempo não chegar mais ninguém na fila, em 15 segundos a fila acaba. Um caixa trabalhando sem parar em um minuto (60 segundos) , consegue atender (sem pausa ou intervalo) 60/3 = 20 requisições em um minuto. Como temos 4 processos, todos juntos conseguem atender 20 x 4 = 80 requisições por minuto.

Se dobrarmos o número de processos (8), conseguimos atender 160 requisições por minuto. Se o tempo de cada processamento (3 s.) diminuir pela metade (1,5 s.), conseguimos atender a 160 requisições por minuto com apenas 4 processos dedicados. Muitas vezes não temos como aumentar mais os processos, por limites de sistema ou esgotamento de recursos, então quanto mais leve e rápido for cada processo, melhor.

Usando AdvPL, existe flexibilidade em se criar pools de processos nativos, e dependendo de sua carga e consumo de memória, colocar múltiplos processos para atender requisições simultâneas. Porém, devem ser observados se os processos aderem aos princípios de paralelismo, senão a aplicação vai jogar memória fora e não vai escalar.

“CACHE”

Lembrando de uma das regras básicas de desempenho de tráfego de informações, buscar uma informação pela rede costuma ser mais lento que ler do disco, e pegar ela da memória é mais rápido do que ler do disco. Mas como não dá pra colocar tudo na memória, e estamos falando de paralelismo em sistemas distribuídos, usar um CACHE das informações mais repetidamente lidas na memória de cada máquina é uma forma bem eficiente de reduzir o RRT. Mas, lembre-se: Não é tudo que voce precisa colocar em um cache … apenas as informações mais acessadas, e com baixa volatilidade. Usar um cache local na memória do servidor, compartilhado pelos demais processos, pode economizar muitas idas e vindas de requisições pela rede.

Um exemplo disso se aplica mais facilmente a uma aplicação WEB dinâmica, onde uma página de promoções acessa o SGBD para mostrar os produtos em promoção. Se as promoções são alteradas diariamente, vale a pena fazer um cache dessa página, e removê-la do cache apenas quando houver alteração das promoções. Para um ERP, um cache interessante seria os meta-dados de algumas tabelas. Se boa parte dos usuários do sistema abre as mesmas tabelas e telas, a leitura destas definições poderiam ser colocadas em um cache na máquina dos slaves, alimentado sob demanda e com uma validade em tempo pré-determinada. O primeiro usuário que abre uma tela de cadastro de produtos ou pedidos alimenta o cache, e todos os demais usuários conectados em serviços naquela máquina pegam as definições do cache local, muito mais rápido, e sem consumir recurso de rede.

Existem meios de se criar caches em AdvPL, usando por exemplo variáveis globais, ou mesmo utilizar um cache de mercado (MemCacheDB, por exemplo), desde que seja escrito um Client em AdvPL que use a api client de Socket’s do Advpl (Classe tSocketClient) para fazer a comunicação. RPC e variáveis globais podem fazer uma bela dupla, mas vão precisar de um certo nível de controle e gerenciamento implementado manualmente, mas isto não inviabiliza esta alternativa.

“REMOTE DATA”

Quando falamos de aplicação Client-Server em Cluster, devemos ter em mente que as informações comuns a determinados processos precisam estar em um lugar onde todos os nós de processamento tenham acesso. Parte da premissa de não criar afinidade. Fica mais fácil citar um exemplo de servidor WEB com Upload de imagens, onde você grava a imagem em uma pasta do servidor: Ao invés de compartilhar esta pasta pelo sistema de arquivos, é possível criar um servidor dedicado a conteúdo estático, e fazer um mecanismo de cache ou réplica nos nós, se ou quando necessário.

A abordagem do AdvPL parte da premissa que cada serviço slave tenha acesso ao RootPath do ambiente, compartilhado pelo sistema operacional do equipamento. Definitivamente um excesso de acessos concorrentes pode prejudicar uma escalabilidade.

“REVERSE PROXY”

O exemplo de proxy reverso também se aplica mais em escalabilidade de ambientes WEB. Ele serve de “Front-End” da sua aplicação, permite a utilização de técnicas de Cache, mascaramento de IP, “esconde” a sua infra-estrutura interna, entre outas funcionalidades. O detalhe importante disso é que um Proxy Reverso, pelo fato de estar “na frente” de tudo, ele passa a ser um SPOF (Single Point of Failure). Se o Proxy Reverso “morrer”, sua aplicação morre junto.

Para aplicações SOA, uma alternativa interessante é usar o já mencionado “controlador” de requisições. Cada nó do cluster pergunta para um controlador de serviço onde está o nó mais próximo disponível, e cada serviço de controle fala com os demais. Assim, você pode deixar serviços ativos em vários nós, e caso um nó saia do ar, o controlador direciona a requisição para outro serviço disponível.

Resumo geral dos conceitos

Cada uma das técnicas ou “segredos” abordados exige que a aplicação seja desenvolvida para aderir a estes paradigmas. Uma aplicação Client-Server monolítica e de conexão persistente não se encaixaria neste modelo sem uma grande refatoração. É mais complicado gerenciar múltiplos recursos em múltiplos equipamentos, é mais complicado desenvolver pensando nestas características, a administração deste tipo de ambiente requer instrumentações, alertas e procedimentos da forma mais automatizada possível. Imagine você ter que conectar e abrir cada uma das máquinas para procurar por um LOG de erro específico, ou tentar descobrir por que ou como uma informação inconsistente foi parar na base de dados. Cada camada precisa estar “cercada” por mecanismos que visem minimizar inconsistências e evitar que elas se propaguem, além de permitir rastrear os caminhos da informação.

No AdvPL são fornecidos recursos como os WebServices (SOAP e REST), RPC nativo entre servidores, IPC entre processos da mesma instância de Application Server. A junção de todos eles pode tornar real e segura uma implementação desta natureza. Agrupando requisições por funcionalidade, e tendo um painel de gerenciamento e monitoramento dos recursos, você consegue medir quanto “custa” cada pedaço do sistema em consumo de recursos, e consegue lidar melhor com estimativas de crescimento, identificação de pontos críticos de melhoria, tornando assim possível a tão sonhada “escalabilidade horizontal” — em grande estilo.

Conclusão

A automatização de processos de Build e testes têm se mostrado muito efetiva em cenários de desenvolvimento e roll-out de produtos. Eu acredito que já esteja mais que na hora de criar mecanismos assistidos de deploy e mecanismos nativos de painéis de gestão de configuração e ambiente. Uma vez estabelecida a confiabilidade nos mecanismos de monitoramento, ações podem ser programadas e realizadas automaticamente, minimizando impactos e reduzindo os riscos de ter uma indisponibilidade total do ambiente computacional.

Criar uma aplicação que desde a sua concepção já considere estes pontos, será tão trabalhosa quando migrar uma aplicação que conta com comportamentos sistêmicos persistentes, mas ambos os esforços devem compensar o potencial de expansão que pode ser adquirido com esta abordagem.

Novamente, agradeço a audiência, e desejo a todos TERABYTES de sucesso 😀

Até o próximo post, pessoal 😉

Referências

Sobre Código: Os 5 segredos para um sistema altamente escalável, por Rodrigo Urubatan. Acessado em 06/12/2015 em <http://sobrecodigo.com/os-4-segredos-para-um-sistema-altamente-escalavel/>.

Stateless protocol. (2015, August 20). In Wikipedia, The Free Encyclopedia. Retrieved 01:33, October 25, 2015, from https://en.wikipedia.org/w/index.php?title=Stateless_protocol&oldid=677029705

Acelerando o AdvPL – Lendo arquivos TXT (ERRATA)

Pessoal,

No post “Acelerando o AdvPL – Lendo arquivos TXT”, foi corrigido um erro que causava um mau comportamento da rotina,  fazendo a leitura de linhas “inconsistentes”. Na chamada da função RAT(), o primeiro parâmetro deve ser a string a ser procurada, e o segundo parâmetro deve ser a string onde a primeira deve ser procurada. A passagem de parâmetros estava ao contrário, fazendo com que a quebra de linha no final de um bloco lido fosse identificada erroneamente, fazendo a rotina retornar linhas com quebras inexistentes. O código-fonte do post original já foi corrigido, segue abaixo o detalhe da correção.

Antes da correção (fonte incorreto)

// Determina a ultima quebra
nRPos := Rat(cBuffer,::cFSep)

Após a correção, o código deve ficar assim:

// Determina a ultima quebra
nRPos := Rat(::cFSep,cBuffer)

Estava trabalhando em um próximo post sobre escalabilidade e performance, e aproveitei a classe para ilustrar um processamento. Quando executei a rotina, percebi que as quebras de linha estavam inconsistentes, pois foram retornadas mais linhas do que eu havia inserido.

Acelerando o AdvPL – Lendo arquivos TXT

Introdução

Alguém me perguntou na sexta-feira, qual era o método mais rápido de ler um arquivo TXT, que utilizava apenas o código ASCII 13 (CR) como quebra de linha… Normalmente eu mesmo usaria as funções FT_Fuse() e FT_FReadLn() para ler o arquivo … mas estas funções não permitem especificar o caractere de quebra… elas trabalham com arquivos TXT onde a quebra pode ser CRLF ou apenas LF.

Como eu não lembrava quem tinha me perguntado, e não achei a pergunta no WhatsApp, e-mail, FaceBook, etc…. resolvi postar a resposta no Facebook mesmo, explicando duas alternativas rápidas de leitura, uma consumindo mais memória e lendo o arquivo inteiro, e outra para arquivos sem limite de tamanho, e consumindo menos memória. Como houve interesse no assunto e diversos comentários, resolvi fazer uma classe de exemplo da segunda abordagem, partindo das premissas do melhor desempenho versus o menor consumo de memória possível.

Fonte da Clase ZFWReadTXT

#include 'protheus.ch'
/* ======================================================================
 Classe ZFWReadTXT
 Autor Júlio Wittwer
 Data 17/10/2015
 Descrição Método de leitura de arquivo TXT
Permite com alto desempenho a leitura de arquivos TXT
 utilizando o identificador de quebra de linha definido
 ====================================================================== */
#define DEFAULT_FILE_BUFFER 4096
CLASS ZFWReadTXT FROM LONGNAMECLASS
  DATA nHnd as Integer
  DATA cFName as String
  DATA cFSep as String
  DATA nFerror as Integer
  DATA nOsError as Integer
  DATA cFerrorStr as String
  DATA nFSize as Integer
  DATA nFReaded as Integer
  DATA nFBuffer as Integer
  DATA _Buffer as Array
  DATA _PosBuffer as Integer
  DATA _Resto as String
  // Metodos Pubicos
  METHOD New()
  METHOD Open()
  METHOD Close()
  METHOD GetFSize()
  METHOD GetError()
  METHOD GetOSError()
  METHOD GetErrorStr()
  METHOD ReadLine()
  // Metodos privados
  METHOD _CleanLastErr()
  METHOD _SetError()
  METHOD _SetOSError()
ENDCLASS
METHOD New( cFName , cFSep , nFBuffer ) CLASS ZFWReadTXT
DEFAULT cFSep := CRLF
DEFAULT nFBuffer := DEFAULT_FILE_BUFFER
::nHnd := -1
::cFName := cFName
::cFSep := cFSep
::_Buffer := {}
::_Resto := ''
::nFSize := 0
::nFReaded := 0
::nFerror := 0
::nOsError := 0
::cFerrorStr := ''
::_PosBuffer := 0
::nFBuffer := nFBuffer
Return self

METHOD Open( iFMode ) CLASS ZFWReadTXT
DEFAULT iFMode := 0
::_CleanLastErr()
If ::nHnd != -1
 _SetError(-1,"Open Error - File already open")
 Return .F.
Endif
// Abre o arquivo
::nHnd := FOpen( ::cFName , iFMode )
If ::nHnd < 0
 _SetOSError(-2,"Open File Error (OS)",ferror())
Return .F.
Endif
// Pega o tamanho do Arquivo
::nFSize := fSeek(::nHnd,0,2)
// Reposiciona no inicio do arquivo
fSeek(::nHnd,0)
Return .T.
METHOD Close() CLASS ZFWReadTXT
::_CleanLastErr()
If ::nHnd == -1
 _SetError(-3,"Close Error - File already closed")
Return .F.
Endif
// Close the file
fClose(::nHnd)
// Clean file read cache 
aSize(::_Buffer,0)
::_Resto := ''
::nHnd := -1
::nFSize := 0
::nFReaded := 0
::_PosBuffer := 0
Return .T.
METHOD ReadLine( /*@*/ cReadLine ) CLASS ZFWReadTXT
Local cTmp := ''
Local cBuffer
Local nRPos
Local nRead
// Incrementa o contador da posição do Buffer
::_PosBuffer++
If ( ::_PosBuffer <= len(::_Buffer) )
 // A proxima linha já está no Buffer ...
 // recupera e retorna
 cReadLine := ::_Buffer[::_PosBuffer]
 Return .T.
Endif
If ( ::nFReaded < ::nFSize )
  // Nao tem linha no Buffer, mas ainda tem partes
  // do arquivo para ler. Lê mais um pedaço
  nRead := fRead(::nHnd , @cTmp, ::nFBuffer)
  if nRead < 0
    _SetOSError(-5,"Read File Error (OS)",ferror())
    Return .F.
  Endif
  // Soma a quantidade de bytes lida no acumulador
  ::nFReaded += nRead
  // Considera no buffer de trabalho o resto
  // da ultima leituraa mais o que acabou de ser lido
  cBuffer := ::_Resto + cTmp
  // Determina a ultima quebra
  nRPos := Rat(::cFSep,cBuffer)
  If nRPos > 0
    // Pega o que sobrou apos a ultima quegra e guarda no resto
    ::_Resto := substr(cBuffer , nRPos + len(::cFSep))
    // Isola o resto do buffer atual
    cBuffer := left(cBuffer , nRPos-1 )
  Else
    // Nao tem resto, o buffer será considerado inteiro
    // ( pode ser final de arquivo sem o ultimo separador )
    ::_Resto := ''
  Endif
 // Limpa e Recria o array de cache
 // Por default linhas vazias são ignoradas
 // Reseta posicionamento de buffer para o primeiro elemento 
 // E Retorna a primeira linha do buffer 
 aSize(::_Buffer,0)
 ::_Buffer := StrTokArr2( cBuffer , ::cFSep )
 ::_PosBuffer := 1
 cReadLine := ::_Buffer[::_PosBuffer]
 Return .T.
Endif
// Chegou no final do arquivo ...
::_SetError(-4,"File is in EOF")
Return .F.
METHOD GetError() CLASS ZFWReadTXT
Return ::nFerror
METHOD GetOSError() CLASS ZFWReadTXT
Return ::nOSError
METHOD GetErrorStr() CLASS ZFWReadTXT
Return ::cFerrorStr
METHOD GetFSize() CLASS ZFWReadTXT
Return ::nFSize
METHOD _SetError(nCode,cStr) CLASS ZFWReadTXT
::nFerror := nCode
::cFerrorStr := cStr
Return
METHOD _SetOSError(nCode,cStr,nOsError) CLASS ZFWReadTXT
::nFerror := nCode
::cFerrorStr := cStr
::nOsError := nOsError
Return
METHOD _CleanLastErr() CLASS ZFWReadTXT
::nFerror := 0
::cFerrorStr := ''
::nOsError := 0
Return

Como funciona

No construtor da classe (método NEW), devemos informar o nome do arquivo a ser aberto, e opcionalmente podemos informar quais são os caracteres que serão considerados como “quebra de linha”. Caso não especificado, o default é CRLF — chr(13)+chr(10). Mas pode ser especificado também apenas Chr(13) (CR) ou Chr(10) (LF). E, como terceiro parâmetro, qual é o tamanho do cache em memória que a leitura pode utilizar. Caso não informado, o valor default são 4 KB (4096 bytes). Caso as linhas de dados do seu TXT tenha em média 1 KB, podemos aumentar seguramente este número para 8 ou 16 KB, para ter um cache em memória por evento de leitura em disco de pelo menos 8 ou 16 linhas.

A idéia é simples: Na classe de encapsulamento de leitura, a linha é lida por referência pelo método ReadLine(), que pode retornar .F. em caso de erros de acesso a disco, ou final de arquivo (todas as linhas já lidas). A abertura do arquivo apenas determina o tamanho do mesmo, para saber quanto precisa ser lido até o final do arquivo. A classe mantém um cache de linhas em um array, e uma propriedade para determinar o ponto atual do cache. Ele começa na primeira linha.

Na primeira leitura, o array de cache está vazio, bem como um buffer temporário chamado “_resto”. A primeira coisa que a leitura faz é incrementar o ponteiro do cache e ver se o ponteiro não passou o final do cache. Como na primeira leitura o cache está vazio, a próxima etapa é verificar se ainda falta ler alguma coisa do arquivo.

Caso ainda exista dados para ler do arquivo, um bloco de 4 KB é lido para a memória, em um buffer de trabalho montado com o resto da leitura anterior (que inicialmente está vazio) mais os dados lidos na operação atual. Então, eu localizo da direita para a esquerda do buffer a ultima quebra do buffer lido, guardo os dados depois da última quebra identificada no buffer “_resto” e removo estes dados do buffer atual de trabalho.

Desse modo, eu vou ter em memória um buffer próximo do tamanho máximo do meu cache (4 KB), considerando a última quebra encontrada neste buffer. Basta eu transformá-lo em um array usando a função StrTokArr2, guardar esse array na propriedade “_Buffer” da classe, e retornar a primeira linha lida.

Quando o método ReadLine() for chamado novamente, o cache vai estar alimentado com “N” linhas do arquivo, eu apenas movo o localizador do cache uma unidade pra frente, e se o localizador ainda está dentro do array, eu retorno a linha correspondente. Eu nem preciso me preocupar em limpar isso a cada leitura, afinal a quantidade de linhas em cache vai ocupar pouco mais de 4 KB mesmo … eu somente vou fazer acesso a disco, e consequente limpeza e realimentação desse cache quando o cache acabar, e ainda houver mais dados no arquivo a ser lido.

Desempenho

Peguei um arquivo TXT aleatório no meu ambiente, que continha um LOG de instalação de um software. O arquivo têm 1477498 bytes, com 12302 linhas de texto. O arquivo foi lido inteiro e todas as linhas identificadas entre 39 e 42 milissegundos (0,039 a 0,042 segundos). Resolvi fazer um outro fonte de testes, lendo este arquivo usando FT_FUSE e FT_FREADLN. Os tempos foram entre 51 e 55 milissegundos (0,051 a 0,055 segundos). E, usando a classe FWFileReader(), os tempos foram entre 101 e 104 milissegundos (0,101 e 0,104 segundos).

Repeti o mesmo teste com um arquivo suficientemente maior, 50 mil linhas e 210 MB .. os tempos foram:

FWFileReader ...... 2,937 s.
FT_FreadLN ........ 1,233 s.
ZFWReadTXT ........ 0,966 s.

Conclusão

Ganhar centésimos de segundo pode parecer pouco … mas centésimos de segundos podem significar uma largada na “pole position”, ou largar na 5a fila … Em uma corrida de 60 voltas, um segundo por volta torna-se um minuto. A máxima do “mais com menos” permanece constante. Quanto menos ciclos de máquina você consumir para realizar um processamento, mais rápido ele será. Quando falamos em aplicações com escalabilidade “estrelar”, para milhões de requisições por segundo, onde temos um limite natural de número de processos dedicados por máquina, quanto mais rápido cada processo for executado, maior “vazão” de processamento pode ser obtido com a mesma máquina. Principalmente quando entramos em ambientes “Cloud”, mesmo tendo grande disponibilidade de processamento, normalmente você será tarifado proporcionalmente ao que consumir. Chegar ao mesmo resultado com menos consumo de recurso fará a diferença 😀

Nos próximos tópicos (aceito sugestões), vou procurar abordar as questões de “como fazer mais rápido” determinada tarefa ou processo, acredito que esta abordagem ajuda a aproximar todos os conceitos de escalabilidade e desempenho que foram abordados apenas na teoria nos posts iniciais sobre este tema !

Agradeço a todos pela audiência do Blog, e não sejam tímidos em dar sugestões, basta enviar a sua sugestão de post siga0984@gmail.com 😀 Para dúvidas pertinentes ao assunto do post, basta postar um comentário no post mesmo 😉

E, pra finalizar, segue abaixo o fonte de teste de desempenho utilizado:

#include 'protheus.ch'
#define TEXT_FILE '\meuarquivo.txt'
/* ======================================================================
Função U_LeFile1, 2 e 3()
Autor Júlio Wittwer
Data 17/10/2015
Descrição Fontes de teste comparativo de desempenho de leitura de arquivo TEXTO
U_LeFile1() - Usa ZFWReadTXT
U_LeFile2() - Usa FT_FREADLN
U_LeFile3() - Usa FWFileReader
====================================================================== */
User Function LeFile1()
Local oTXTFile
Local cLine := ''
Local nLines := 0
Local nTimer
nTimer := seconds()
oTXTFile := ZFWReadTXT():New(TEXT_FILE)
If !oTXTFile:Open()
  MsgStop(oTXTFile:GetErrorStr(),"OPEN ERROR")
  Return
Endif
While oTXTFile:ReadLine(@cLine)
  nLines++
Enddo
oTXTFile:Close()
MsgInfo("Read " + cValToChar(nLines)+" line(s) in "+str(seconds()-nTimer,12,3)+' s.',"Using ZFWReadTXT")
Return
User Function LeFile2()
Local nTimer
Local nLines := 0
nTimer := seconds()
FT_FUSE(TEXT_FILE)
While !FT_FEOF()
  cLine := FT_FReadLN()
  FT_FSkip()
  nLines++
Enddo
FT_FUSE()
MsgInfo("Read " + cValToChar(nLines)+" line(s) in "+str(seconds()-nTimer,12,3)+' s.',"Using FT_FReadLN")
Return
User Function LeFile3()
Local nTimer
Local nLines := 0
Local oFile
nTimer := seconds()
oFile := FWFileReader():New(TEXT_FILE)
If !oFile:Open()
  MsgStop("File Open Error","ERROR")
  Return
Endif
While (!oFile:Eof())
  cLine := oFile:GetLine()
  nLines++
Enddo
oFile:Close()
MsgInfo("Read " + cValToChar(nLines)+" line(s) in "+str(seconds()-nTimer,12,3)+' s.',"Using FWFileReader")
Return

Acelerando o AdvPL – Parte 03

Introdução

Continuando os tópicos de performance e escalabilidade direcionados ao AdvPL, vamos hoje unir o útil ao agradável: Vamos abordar detalhes algumas boas práticas, inclusive algumas já mencionadas na documentação da TDN. Inclusive, enquanto este artigo era redigido, encontrei uma documentação muito interessante no link http://tdn.totvs.com/pages/viewpage.action?pageId=22480352, que abrange assuntos como as convenções da linguagem, padrões de design, práticas e técnicas, e inclusive desempenho.

Macroexecução em AdvPL

Como o foco deste tópico é justamente acelerar o AdvPL, vamos abordar um dos tópicos de desempenho (http://tdn.totvs.com/display/framework/Desempenho). Um dos testes realizados e publicados nesta seção da TDN foi uma comparação entre duas abordagens de exemplo de chamada de função, uma usando a macroexecução e outra usando a função Eval(), onde o teste realizado mostrou um melhor desempenho na macroexecução.

Fonte de testes

Baseado no fonte de testes da TDN, eu criei um fonte que simula quatro formas diferentes da chamada de uma função de processamento de exemplo, onde existe a passagem de dois parâmetros. Vamos ao código:

#INCLUDE "PROTHEUS.CH"
#DEFINE ITERATION_REPEAT 800000 // Repetições de testes
User Function EvalTest()
TesEcom1() // Teste 01 com "&" 
TesEcom2() // Teste 02 com "&"
TesEvalC() // Teste com Eval()
TesDiret() // Teste com chamada direta
Return
/* ---------------------------------------------------------------
Teste TesEcom1()
Usando macro-substituição, passando os parâmetros por fora da macro.
--------------------------------------------------------------- */
Static Function TesEcom1()
Local nX, nSeconds, nTimer
Local cRet := ""
Local cBloco := "FunTesExec"
nSeconds := Seconds()
For nX := 1 To ITERATION_REPEAT 
 cRet := &cBloco.(nX,10)
Next nX
nTimer := Seconds() - nSeconds
ConOut("("+procname(0)+") Tempo de execucao ....: " +str(nTimer,6,2)+' s.' )
Conout("("+procname(0)+") Operacoes por segundo.: "+str(ITERATION_REPEAT/nTimer,10))
Return
/* ---------------------------------------------------------------
Teste TesEcom2()
Usando macro-substituição, passando os parametros DENTRO da macro.
--------------------------------------------------------------- */
Static Function TesEcom2()
Local nX, nSeconds, nTimer
Local cRet := ""
Local cBloco
nSeconds := Seconds()
For nX := 1 To ITERATION_REPEAT 
 cBloco := "FunTesExec("+cValToChar(nX)+",10)"
 cRet := &(cBloco)
Next nX
nTimer := Seconds() - nSeconds
ConOut("("+procname(0)+") Tempo de execucao ....: " +str(nTimer,6,2)+' s.' )
Conout("("+procname(0)+") Operacoes por segundo.: "+str(ITERATION_REPEAT/nTimer,10))
Return
/* ---------------------------------------------------------------
Teste TesEvalC()
Usando Code-Block 
--------------------------------------------------------------- */
Static Function TesEvalC()
Local nX, nSeconds, nTimer
Local cRet := "" // Retorno do Bloco de Codigo
Local bBloco := {|p1,p2| FunTesExec(p1,p2)}
nSeconds := Seconds()
For nX := 1 To ITERATION_REPEAT
 cRet := Eval(bBloco,nx,10)
Next nX
nTimer := Seconds() - nSeconds
ConOut("("+procname(0)+") Tempo de execucao ....: " +str(nTimer,6,2)+' s.' )
Conout("("+procname(0)+") Operacoes por segundo.: "+str(ITERATION_REPEAT/nTimer,10))
Return
/* ---------------------------------------------------------------
Teste TesDiret()
Usando Chamada Direta
--------------------------------------------------------------- */
Static Function TesDiret()
Local nX := 0
Local nSeconds := 0
Local cRet := ""
nSeconds := Seconds()
For nX := 1 To ITERATION_REPEAT
 cRet := FunTesExec(nX,10)
Next nX
nTimer := Seconds() - nSeconds
ConOut("("+procname(0)+") Tempo de execucao ....: " +str(nTimer,6,2)+' s.' )
Conout("("+procname(0)+") Operacoes por segundo.: "+str(ITERATION_REPEAT/nTimer,10))
Return
/* ---------------------------------------------------------------
Funcao de Teste FunTesExec()
Apenas encapsula a função StrZero
--------------------------------------------------------------- */
STATIC Function FunTesExec(nExpr, nTam)
Local cNum
DEFAULT nExpr := 1
DEFAULT nTam := 1
cNum := StrZero(nExpr,nTam)
Return cNum

Resultados do teste

No meu notebook, os resultados obtidos no console do TOTVS AppServer foram estes :

(TESECOM1) Tempo de execucao ....: 3.16 s.
(TESECOM1) Operacoes por segundo.: 253325
(TESECOM2) Tempo de execucao ....: 21.12 s.
(TESECOM2) Operacoes por segundo.: 37879
(TESEVALC) Tempo de execucao ....: 3.16 s.
(TESEVALC) Operacoes por segundo.: 253084
(TESDIRET) Tempo de execucao ....: 2.34 s.
(TESDIRET) Operacoes por segundo.: 341297

Refletindo sobre estes tempos

Inicialmente, vamos ver o tempo mais rápido. Naturalmente, é uma chamada direta da função de processamento. Demorou 2,34 segundos para fazer 800 mil iterações. São aproximadamente 340 mil execuções por segundo. Depois vem os tempos de execução via macro (01) e Eval(), 3,16 segundos, aproximadamente 253 mil execuções por segundo. O tempo ficou 35 % maior para ambos os casos. Agora, o tempo mais lento, do segundo teste de macro-execução, demorou 21 segundos, apresentando um desempenho de 37 mil execuções por segundo, que ficou 9 vezes mais lento que o melhor resultado.

Agora, vamos olhar com uma lupa o “miolo” dos dois casos de teste com macro-execução, que é o foco principal deste post:

No primeiro teste, a chamada da função é realizada com uma forma de macro-substituição, onde apenas o nome da função a ser chamada está dentro da variável cBloco, e os parâmetros são passados explicitamente em cada chamada por fora da Macro.

cBloco := "FunTesExec"
For nX := 1 To ITERATION_REPEAT 
 cRet := &cBloco.(nX,10)
Next nX

No segundo teste, a macro é montada dentro de uma string, onde os parâmetros da função são montados dentro da string. Como a variável nX é local, ela não pode ser especificada diretamente, pois a visibilidade de resolução da macro não consegue pegar as variáveis locais.

For nX := 1 To ITERATION_REPEAT 
 cBloco := "FunTesExec("+cValToChar(nX)+",10)"
 cRet := &(cBloco)
Next nX

No primeiro teste, o texto da macro ( FunTesExec ) não muda durante as interações, o que permite ao AdvPL fazer um cache da resolução da macro, e acelerar o processo. Já no segundo teste, a macro é alterada com novos parâmetros a cada nova execução, o que exige que a macro seja recompilada na memória a cada iteração. Isto dá uma grande diferença de desempenho entre os dois ciclos de iterações.

Conclusão

Existem casos especiais onde a cópia dos parâmetros para uma macro precisam realmente ser feitas na chamada, mas agora não lembro de nenhum exemplo assim, “de cabeça”. Mas, para a maioria dos outros casos, onde você pode precisa fazer dinamicamente apenas a chamada de uma função dinâmica, cujo nome está em uma variável caractere, é mais rápido passar os parâmetros por fora.

Desejo a todos excelentes otimizações, um desempenho fantástico, e um ótimo final de semana 😀

Abraços, e até o próximo post 😉

Acelerando o AdvPL – Parte 02 (ERRATA)

Pessoal, boa tarde,

Hoje eu estava lendo novamente o código do post anterior, referente ao exemplo de um cache em array, com tamanho limitado de elementos, e uma optimização para subir no array cada item pesquisado, para que os itens mais pesquisados sejam encontrados sequencialmente em primeiro lugar, e encontrei um erro de lógica em um ponto da rotina … vamos ver apenas este pedaço do código:

Function GetJurosPad( cTipoCtr )
Local nJuros := -1
Local cChave := xFilial('ZZ1')+cTipoCtr
Local nPos := ascan(aJurosPad,{ |x| x[1] == cChave })
Local aTmp
If nPos > 0 
 // Achou no cache. 
 If nPos > 1 
 // nao esta na primeira posição, sobe uma 
   aTmp := aJurosPad[nPos]
   aJurosPad[nPos] := aJurosPad[nPos-1]
   aJurosPad[nPos-1] := aTmp
 Endif
 // Incrementa cache HITS -- achei no cache, economia de I/O
 nJurosHit++
 Return aJurosPad[nPos][2]
Endif

Reparem que a busca foi feita na declaração da variável nPos, usando Ascan(). Porém, caso o item encontrado não seja o primeiro elemento do array, o elemento encontrado sobe uma posição (usando a variável aTmp para fazer o SWAP). E, no final das contas, o valor retornado é a segunda coluna do elemento após a troca. Neste caso, a função somente vai retornar o valor correto caso o valor buscado seja encontrado no primeiro elemento do array.

Para  corrigir isto, precisamos armazenar o resultado encontrado antes de fazer a troca no array, e retornar este valor, veja a correção abaixo:

Function GetJurosPad( cTipoCtr )
Local nJuros := -1
Local cChave := xFilial('ZZ1')+cTipoCtr
Local nPos := ascan(aJurosPad,{ |x| x[1] == cChave })
Local aTmp
If nPos > 0 
 // Achou no cache. 
 nJuros := aJuros[nPos][2]
 If nPos > 1 
   // nao esta na primeira posição, sobe uma 
   aTmp := aJurosPad[nPos]
   aJurosPad[nPos] := aJurosPad[nPos-1]
   aJurosPad[nPos-1] := aTmp
 Endif
 // Incrementa cache HITS -- achei no cache, economia de I/O
 nJurosHit++
 Return nJuros
Endif

Lembrem-se da importância de revisar e testar o código. Ser um bom programador e ter experiência não vai te livrar de cometer erros, uma revisão de código e um bom teste nunca é uma questão de desconfiança, mas sim procedimento.

Até a próxima, pessoal 😀