CRUD em AdvPL – Parte 07

Introdução

Continuando a sequência de posts relacionados ao CRUD em Advpl, onde criamos um exemplo de programa de Agenda, nesse post vamos olhar mais de perto a geração do ID na inclusão de dados da Agenda, como o ERP Microsiga usa sequenciadores nas tabelas de dados, e como podemos fazer um sequenciador em memória para a Agenda.

Interface de Inclusão – Gerador de ID

Passando rapidamente o olho no fonte, ela parece em ordem. Os identificadores de registro (ID) da Agenda são criados protegidos por um semáforo (ou MUTEX), sempre pegamos o último registro usando o índice por ID — Ordem 1 — e acrescentamos uma unidade. Perfeito, certo ?

Não é bem assim. Ao excluir um registro da agenda usando a aplicação, o registro é marcado para deleção — através da função DbDelete() — e devido ao filtro para ignorar registros marcados para deleção nas operações de busca e navegação da tabela — ligado no inicio da aplicação com o comando SET DELETED ON —  o registro marcado para exclusão não é mais visível nas consultas.  Como o primeiro contato com ID 000004 está deletado — ou marcado para deleção — no momento que eu vou inserir um novo contato, a rotina de geração de ID pega o último registro ATIVO da tabela — estamos desconsiderando deletados — lê o valor do registro, e incrementa uma unidade — gerando novamente o ID 000004.

Podemos arrumar isso de uma forma bem simples, apenas desligando o filtro para ignorar registros deletados — usando o comando SET DELETED OFF — e no final da rotina, depois de gerar o novo número, ligar o filtro novamente.

Como a numeração sequencial é feita no ERP ?

Imagine cada inserção feita em uma tabela com um campo com sequência incremental, precisar ir no Banco de Dados, posicionar e ler o último registro para gerar o próximo identificador da sequência ? É um desperdício de requisições, além de ser necessário um MUTEX bloqueando a inserção até ela ser concluída.

Para contornar isso, foram criadas no ERP duas tabelas de controle e reserva de sequência, chamadas de SXE e SXF. A idéia é simples, uma controla todos os registros de sequenciadores do ERP, e a outra controla as reservas de sequência — um processo pode reservar uma sequência para uso, e não confirmar o uso da sequência, então ela torna-se disponível para a próxima inserção, evitando intervalos vazios ou “buracos” nas sequências.

A implementação e a ideia são excelentes, porém com o aumento do número de tabelas do ERP, e todos os sequenciadores sendo controlados pelo mesmo arquivo, a quantidade de acesso a disco por processos concorrentes usando as sequências poderia gerar filas de acesso ao disco no sistema operacional, deixando o sistema lento.

Para resolver isso, o mecanismo de sequenciamento e reserva de sequências foi transferido para dentro do Servidor de Licenças do ERP, onde as sequências são controladas em memória, sem acesso a disco. Com isso, apenas a primeira geração do registro de controle da sequência precisa fazer um acesso a disco, ir no final da tabela, ler o último código, e criar o registro de sequenciamento. A partir de então, usando as mesmas funções de encapsulamento disponibilizadas pelo Framework AdvPL do ERP, as próximas requisições de um novo identificador da sequência são feitas para o Servidor de Licenças, de forma muito rápida, evitando acesso a disco desnecessário, e como todo o controle de sequenciamento é feito na memória, o tempo que a lista permanece bloqueada para a geração do identificador é ridículo.

Construindo um sequenciador para a Agenda

Partindo ainda da premissa que o programa de AGENDA será executado por hora apenas por um servidor de aplicação, podemos usar por exemplo uma variável global para guardar o último número gerado da agenda, e quando for realizada uma inclusão, resgatamos o número da memória, acrescentamos uma unidade, e atualizamos a variável global. Dessa forma, mesmo com múltiplas threads, cada uma delas ficará bloqueada para gerar um novo identificador por um tempo ínfimo, e a sequência sempre fica na memória, enquanto o servidor estiver no ar. Vamos ver como ficaria a função GetNewID() usando essa abordagem:

 

STATIC Function GetNewID()
Local cLastID,cNewId
Local nRetry := 0
While !GlbNmLock("AGENDA_ID")
   // Espera máxima de 1 segundo, 20 tentativas 
   // com intervalos de 50 milissegundos 
   nRetry++
   If nRetry > 20
      return ""
   Endif
   Sleep(50)
Enddo
cLastID := GetGlbValue("AGENDA_SEQ")
If Empty(cLastID) 
  // Somente busco na Tabela se eu nao tenho o valor na memoria
  DBSelectArea("AGENDA")
  DbsetOrder(1)
  DBGobottom()
  cLastId := AGENDA->ID
Endif
cNewId := StrZero( val(cLastID) + 1 , 6 )
PutGlbValue("AGENDA_SEQ",cNewID)
GlbNmUnlock("AGENDA_ID")
Return cNewId

Reparem como o fonte ficou praticamente do mesmo tamanho que era antes, porém agora seu comportamento está muito — muito mesmo — mais optimizado. Na primeira execução do programa, a variável global AGENDA_SEQ não existe em memória, logo a aplicação busca o último registro do banco de dados. Caso a global já exista, o valor do último ID utilizado é recuperado da memória. Então, criamos o novo ID, atualizamos a variável global de memória com este valor, soltamos o bloqueio nomeado obtido, e retornamos o número para a rotina de inserção.

Diferença de Tempo

Fazendo um teste de inserção usando uma função de teste sem interface, no meu ambiente consegui inserir 10 mil registros em pouco mais de 10 segundos — aproximadamente 1000 registros por segundo — inserindo apenas ID e um nome aleatório. Usando a rotina de geração de ID proposta, lendo os valores da memória, o tempo baixou de 10,5 s. para 5,5 s. — praticamente duas vezes mais rápido. Segue abaixo a função de testes utilizada:

User Function TesteAg1()
Local nI, nTimer
OpenAgenda()
nTimer := seconds()
For nI := 1 to 10000
  cNewId := GetNewID()
  dbappend()
  agenda->ID := cNewId
  agenda->NOME := cvaltochar(str(seconds()*1000))
  DBRUnlock()
Next
conout("Tempo de Insert = "+str(seconds()-nTimer,12,3)+' s.')
return

Esta função foi acrescentada ao fonte AGENDA.PRW, e executada de modo direto. Como a função OpenAgenda() não depende de nada, podemos chamá-la diretamente no nosso teste, para abrir a tabela de AGENDA e usá-la diretamente. Caso você querida fazer o teste comparativo, recomendo renomear a função original GetNewID() para GetNewId1(), inserir a nova como GetNewId2(), e chavear isso no programa de testes, rodando uma vez com dada função para verificar a diferença de tempo.

Restrição Operacional

Naturalmente, devido ao escopo da variável de memória global ser apenas a instância atual do serviço atual do Protheus Server, este tipo de semáforo em memória somente poderia ser usado por exemplo em um ambiente com balanceamento de carga — e consequentemente múltiplos serviços — caso este controle fosse centralizado em um serviço dedicado, e os demais serviços consumissem estas funções fazendo chamadas remotas (RPC Nativo do AdvPL, por exemplo).

Conclusão

Por hora, nada mais a acrescentar. Ao testar a agenda, já encontrei outros pontos que precisam de ajustes, vamos deixar as conclusões para o próximo post.

Desejo a todos novamente TERABYTES de SUCESSO 😀

Referências

 

Deixe um comentário

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

Logotipo do WordPress.com

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

Foto do Google+

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

Imagem do Twitter

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

Foto do Facebook

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

Conectando a %s